"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LipsyncBase = void 0;
const crypto_1 = require("crypto");
const __1 = require("../..");
class LipsyncBase {
    constructor(audioOutput, videoOutput, options = {}) {
        Object.defineProperty(this, "audioOutput", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: audioOutput
        });
        Object.defineProperty(this, "videoOutput", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: videoOutput
        });
        Object.defineProperty(this, "options", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: options
        });
        Object.defineProperty(this, "id", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: (0, crypto_1.randomUUID)().toString()
        });
        Object.defineProperty(this, "bufferLength", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**ms */
        Object.defineProperty(this, "baseTime", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "audioBuffer", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "videoBuffer", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "stopped", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: false
        });
        /**ms */
        Object.defineProperty(this, "interval", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**ms */
        Object.defineProperty(this, "bufferDuration", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "ptime", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "index", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 0
        });
        Object.defineProperty(this, "currentTimestamp", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**ms */
        Object.defineProperty(this, "lastCommittedTime", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 0
        });
        Object.defineProperty(this, "lastExecutionTime", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 0
        });
        Object.defineProperty(this, "internalStats", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: {}
        });
        /**ms */
        Object.defineProperty(this, "lastFrameReceivedAt", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 0
        });
        Object.defineProperty(this, "processAudioInput", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: ({ frame, eol }) => {
                if (!frame) {
                    this.audioOutput({ eol });
                    this.stop();
                    this.audioOutput = undefined;
                    return;
                }
                this.processInput(frame, this.audioBuffer, "audio");
            }
        });
        Object.defineProperty(this, "processVideoInput", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: ({ frame, eol }) => {
                if (!frame) {
                    this.videoOutput({ eol });
                    this.stop();
                    this.videoOutput = undefined;
                    return;
                }
                this.processInput(frame, this.videoBuffer, "video");
            }
        });
        Object.defineProperty(this, "processInput", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: (frame, buffer, kind) => {
                if (this.stopped) {
                    return;
                }
                if (this.baseTime == undefined) {
                    this.baseTime = frame.time;
                    this.currentTimestamp = this.baseTime;
                    this.lastExecutionTime = this.baseTime;
                    this.lastCommittedTime = this.baseTime;
                    this.lastFrameReceivedAt = Date.now();
                }
                // 過去のフレームを捨てる
                if (frame.time < this.lastCommittedTime) {
                    return;
                }
                // NTPの同期ずれが疑われるので捨てる
                const now = Date.now();
                const gap = 5000; // RTCP SR interval;
                const lastCommittedElapsed = frame.time - this.lastCommittedTime;
                const lastFrameReceivedElapsed = now - this.lastFrameReceivedAt;
                if (gap < lastFrameReceivedElapsed && lastCommittedElapsed < gap) {
                    this.internalStats["invalidFrameTime"] = {
                        count: (this.internalStats["invalidFrameTime"]?.count ?? 0) + 1,
                        at: new Date().toISOString(),
                        lastCommittedElapsed,
                        lastFrameReceivedElapsed,
                    };
                    return;
                }
                this.lastFrameReceivedAt = now;
                const elapsed = frame.time - this.baseTime;
                const index = (0, __1.int)(elapsed / this.bufferDuration) % this.bufferLength;
                buffer[index].push({
                    frame,
                    kind,
                });
                const diff = frame.time - this.lastExecutionTime;
                if (diff >= this.interval) {
                    const times = (0, __1.int)(diff / this.bufferDuration) - 1;
                    this.lastExecutionTime = this.currentTimestamp;
                    for (let i = 0; i < times; i++) {
                        this.executeTask();
                        this.lastExecutionTime += this.bufferDuration;
                    }
                }
                return;
            }
        });
        this.interval = this.options.syncInterval ?? 500;
        this.bufferDuration = this.interval / 2;
        this.bufferLength = (this.options.bufferLength ?? 10) * 2;
        this.audioBuffer = [...new Array(this.bufferLength)].map(() => []);
        this.videoBuffer = [...new Array(this.bufferLength)].map(() => []);
        this.ptime = this.options.ptime ?? 20;
    }
    toJSON() {
        return {
            ...this.internalStats,
            id: this.id,
            audioBufferLength: this.audioBuffer.flat().length,
            videoBufferLength: this.videoBuffer.flat().length,
            baseTime: this.baseTime,
            lastCommittedTimeSec: this.lastCommittedTime / 1000,
        };
    }
    executeTask() {
        const audioBuffer = this.audioBuffer[this.index].sort((a, b) => a.frame.time - b.frame.time);
        if (this.options.fillDummyAudioPacket) {
            const last = audioBuffer.at(-1);
            const expect = last
                ? last.frame.time +
                    // offset
                    this.ptime
                : this.currentTimestamp;
            // パケット間の損失/muteはdtxプラグインでダミーパケットを挿入する
            // interval中のパケットが途中から無いもしくはinterval中にパケットが無い場合はここでダミーパケットを挿入する
            const audioDiff = this.currentTimestamp + this.bufferDuration - expect;
            if (audioDiff > 0) {
                for (let time = expect; time < this.currentTimestamp + this.bufferDuration; time += this.ptime) {
                    audioBuffer.push({
                        frame: {
                            time,
                            data: this.options.fillDummyAudioPacket,
                            isKeyframe: true,
                        },
                        kind: "audio",
                    });
                    this.internalStats["pushDummyPacket"] = {
                        count: (this.internalStats["pushDummyPacket"]?.count ?? 0) + 1,
                        at: new Date().toISOString(),
                        time,
                    };
                }
            }
        }
        this.currentTimestamp += this.bufferDuration;
        const joined = [...audioBuffer, ...this.videoBuffer[this.index]].filter((b) => b.frame.time >= this.lastCommittedTime);
        const sorted = joined.sort((a, b) => a.frame.time - b.frame.time);
        this.audioBuffer[this.index] = [];
        this.videoBuffer[this.index] = [];
        for (const output of sorted) {
            if (output.kind === "audio") {
                this.audioOutput(output);
            }
            else {
                this.videoOutput(output);
            }
            this.internalStats["lipsync"] = new Date().toISOString();
            this.lastCommittedTime = output.frame.time;
        }
        this.index++;
        if (this.index === this.bufferLength) {
            this.index = 0;
        }
    }
    stop() {
        this.stopped = true;
        this.audioBuffer = [];
        this.videoBuffer = [];
    }
}
exports.LipsyncBase = LipsyncBase;
//# sourceMappingURL=lipsync.js.map