"use strict";
/**
 * VideoEncoder - Encodes VideoFrame to encoded video data
 * Implements the W3C WebCodecs VideoEncoder interface
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.VideoEncoder = void 0;
const EncodedVideoChunk_1 = require("./EncodedVideoChunk");
const codec_registry_1 = require("./codec-registry");
const types_1 = require("./types");
// Load native addon
let native;
try {
    native = require('../build/Release/webcodecs_node.node');
}
catch {
    native = null;
}
class VideoEncoder {
    static async isConfigSupported(config) {
        const supported = (0, codec_registry_1.isVideoCodecSupported)(config.codec) &&
            config.width > 0 &&
            config.height > 0;
        return { supported, config };
    }
    constructor(init) {
        this._state = 'unconfigured';
        this._encodeQueueSize = 0;
        this._config = null;
        this._sentDecoderConfig = false;
        this._listeners = new Map();
        if (!init.output || typeof init.output !== 'function') {
            throw new TypeError('output callback is required');
        }
        if (!init.error || typeof init.error !== 'function') {
            throw new TypeError('error callback is required');
        }
        this._outputCallback = init.output;
        this._errorCallback = init.error;
        if (native) {
            this._native = new native.VideoEncoderNative(this._onChunk.bind(this), this._onError.bind(this));
        }
    }
    get state() {
        return this._state;
    }
    get encodeQueueSize() {
        return this._encodeQueueSize;
    }
    /**
     * Minimal EventTarget-style API for 'dequeue' events.
     * Mediabunny uses encoder.addEventListener('dequeue', fn, { once: true }).
     */
    addEventListener(type, listener, options) {
        if (typeof listener !== 'function')
            return;
        const once = !!(options && options.once);
        const wrapper = once
            ? () => {
                this.removeEventListener(type, wrapper);
                listener();
            }
            : listener;
        let set = this._listeners.get(type);
        if (!set) {
            set = new Set();
            this._listeners.set(type, set);
        }
        set.add(wrapper);
    }
    removeEventListener(type, listener) {
        const set = this._listeners.get(type);
        if (!set)
            return;
        if (set.has(listener)) {
            set.delete(listener);
        }
        if (set.size === 0) {
            this._listeners.delete(type);
        }
    }
    _dispatchEvent(type) {
        const set = this._listeners.get(type);
        if (!set)
            return;
        for (const listener of Array.from(set)) {
            try {
                listener();
            }
            catch {
                // Swallow listener errors
            }
        }
    }
    configure(config) {
        if (this._state === 'closed') {
            throw new types_1.DOMException('Encoder is closed', 'InvalidStateError');
        }
        if (!(0, codec_registry_1.isVideoCodecSupported)(config.codec)) {
            throw new types_1.DOMException(`Unsupported codec: ${config.codec}`, 'NotSupportedError');
        }
        if (config.width <= 0 || config.height <= 0) {
            throw new types_1.DOMException('Invalid dimensions', 'NotSupportedError');
        }
        if (!native) {
            throw new types_1.DOMException('Native addon not available', 'NotSupportedError');
        }
        const ffmpegCodec = (0, codec_registry_1.getFFmpegVideoCodec)(config.codec);
        const codecParams = {
            codec: ffmpegCodec,
            width: config.width,
            height: config.height,
        };
        // Parse H.264 codec string for profile/level
        if (config.codec.startsWith('avc1.')) {
            const avcInfo = (0, codec_registry_1.parseAvcCodecString)(config.codec);
            if (avcInfo) {
                codecParams.profile = avcInfo.profile;
                codecParams.level = avcInfo.level;
            }
            codecParams.avcFormat = config.avc?.format ?? 'annexb';
        }
        if (config.bitrate)
            codecParams.bitrate = config.bitrate;
        if (config.framerate)
            codecParams.framerate = config.framerate;
        if (config.bitrateMode)
            codecParams.bitrateMode = config.bitrateMode;
        if (config.latencyMode)
            codecParams.latencyMode = config.latencyMode;
        this._native.configure(codecParams);
        this._config = config;
        this._state = 'configured';
        this._sentDecoderConfig = false;
    }
    encode(frame, options) {
        if (this._state !== 'configured') {
            throw new types_1.DOMException('Encoder is not configured', 'InvalidStateError');
        }
        const nativeFrame = frame._getNative();
        if (!nativeFrame) {
            throw new types_1.DOMException('VideoFrame has no native handle', 'InvalidStateError');
        }
        const keyFrame = options?.keyFrame ?? false;
        this._encodeQueueSize++;
        this._native.encode(nativeFrame, frame.timestamp, keyFrame);
    }
    async flush() {
        if (this._state !== 'configured') {
            throw new types_1.DOMException('Encoder is not configured', 'InvalidStateError');
        }
        return new Promise((resolve, reject) => {
            this._native.flush((err) => {
                if (err) {
                    reject(new types_1.DOMException(err.message, 'EncodingError'));
                }
                else {
                    resolve();
                }
            });
        });
    }
    reset() {
        if (this._state === 'closed') {
            throw new types_1.DOMException('Encoder is closed', 'InvalidStateError');
        }
        if (this._native) {
            this._native.reset();
        }
        this._encodeQueueSize = 0;
        this._state = 'unconfigured';
        this._sentDecoderConfig = false;
        this._config = null;
    }
    close() {
        if (this._state === 'closed')
            return;
        if (this._native) {
            this._native.close();
        }
        this._state = 'closed';
        this._encodeQueueSize = 0;
        this._config = null;
    }
    _onChunk(data, isKeyframe, timestamp, duration, extradata) {
        this._encodeQueueSize = Math.max(0, this._encodeQueueSize - 1);
        this._dispatchEvent('dequeue');
        const chunk = new EncodedVideoChunk_1.EncodedVideoChunk({
            type: isKeyframe ? 'key' : 'delta',
            timestamp,
            duration: duration > 0 ? duration : undefined,
            data,
        });
        let metadata;
        // Send decoder config with first keyframe
        if (isKeyframe && !this._sentDecoderConfig && this._config) {
            metadata = {
                decoderConfig: {
                    codec: this._config.codec,
                    codedWidth: this._config.width,
                    codedHeight: this._config.height,
                    description: extradata ? new Uint8Array(extradata).buffer : undefined,
                },
            };
            this._sentDecoderConfig = true;
        }
        try {
            this._outputCallback(chunk, metadata);
        }
        catch (e) {
            // Don't propagate callback errors
        }
    }
    _onError(message) {
        try {
            this._errorCallback(new types_1.DOMException(message, 'EncodingError'));
        }
        catch (e) {
            // Don't propagate callback errors
        }
    }
}
exports.VideoEncoder = VideoEncoder;
//# sourceMappingURL=VideoEncoder.js.map