// This Source Code Form is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/
import { fromMicros } from "@foxglove/rostime";
import { estimateObjectSize } from "@foxglove/studio-base/players/messageMemoryEstimation.js";
import { ensureWebCodecsSupported } from "../../../utils/compatibility.js";
import { DecoderRuntimeError } from "../../../utils/error.js";
import { Logger } from "../../../utils/logger";
import { parseMessage } from "../../utils.js";
const logger = Logger.getLogger();
function configureDecoder(decoder) {
    decoder.configure({
        codec: "avc1.42001E",
        optimizeForLatency: true,
    });
}
function closeDecoder(decoder) {
    if (!decoder) {
        return;
    }
    if (decoder.state === "closed") {
        return;
    }
    try {
        decoder.close();
    }
    catch (error) {
        logger.warn("Failed to close H264 decoder cleanly.", error);
    }
}
function closeTopicDecoder(topic, topicDecoders) {
    const decoderInfo = topicDecoders.get(topic);
    closeDecoder(decoderInfo?.decoder);
    decoderInfo?.timing.clear();
    decoderInfo?.transformations.clear();
}
function isIgnorableInitialDecodeError(error) {
    const message = error instanceof Error ? error.message : String(error);
    return message.includes("A key frame is required after configure() or flush()");
}
export function packedToI420(packed, width, height, stride, format, cropLeft = 0, cropTop = 0) {
    if ((width & 1) !== 0 || (height & 1) !== 0) {
        throw new Error("I420 requires even width and height.");
    }
    const ySize = width * height;
    const uvSize = (width * height) >> 2;
    const out = new Uint8Array(ySize + uvSize + uvSize);
    const Y = out.subarray(0, ySize);
    const U = out.subarray(ySize, ySize + uvSize);
    const V = out.subarray(ySize + uvSize);
    const isRgbLayout = format === "RGBX" || format === "RGBA";
    const clamp8 = (x) => (x < 0 ? 0 : x > 255 ? 255 : x | 0);
    for (let y = 0; y < height; y += 2) {
        const row0 = (y + cropTop) * stride;
        const row1 = (y + cropTop + 1) * stride;
        for (let x = 0; x < width; x += 2) {
            const srcX = x + cropLeft;
            const p00 = row0 + srcX * 4;
            const p01 = row0 + (srcX + 1) * 4;
            const p10 = row1 + srcX * 4;
            const p11 = row1 + (srcX + 1) * 4;
            const R00 = packed[p00 + (isRgbLayout ? 0 : 2)];
            const G00 = packed[p00 + 1];
            const B00 = packed[p00 + (isRgbLayout ? 2 : 0)];
            const R01 = packed[p01 + (isRgbLayout ? 0 : 2)];
            const G01 = packed[p01 + 1];
            const B01 = packed[p01 + (isRgbLayout ? 2 : 0)];
            const R10 = packed[p10 + (isRgbLayout ? 0 : 2)];
            const G10 = packed[p10 + 1];
            const B10 = packed[p10 + (isRgbLayout ? 2 : 0)];
            const R11 = packed[p11 + (isRgbLayout ? 0 : 2)];
            const G11 = packed[p11 + 1];
            const B11 = packed[p11 + (isRgbLayout ? 2 : 0)];
            const Y00 = (77 * R00 + 150 * G00 + 29 * B00 + 128) >> 8;
            const Y01 = (77 * R01 + 150 * G01 + 29 * B01 + 128) >> 8;
            const Y10 = (77 * R10 + 150 * G10 + 29 * B10 + 128) >> 8;
            const Y11 = (77 * R11 + 150 * G11 + 29 * B11 + 128) >> 8;
            const yRow0 = (y * width) + x;
            const yRow1 = ((y + 1) * width) + x;
            Y[yRow0 + 0] = Y00;
            Y[yRow0 + 1] = Y01;
            Y[yRow1 + 0] = Y10;
            Y[yRow1 + 1] = Y11;
            const Ravg = (R00 + R01 + R10 + R11) >> 2;
            const Gavg = (G00 + G01 + G10 + G11) >> 2;
            const Bavg = (B00 + B01 + B10 + B11) >> 2;
            const Uval = clamp8(((-43 * Ravg - 85 * Gavg + 128 * Bavg) >> 8) + 128);
            const Vval = clamp8(((128 * Ravg - 107 * Gavg - 21 * Bavg) >> 8) + 128);
            const uvCol = x >> 1;
            const uvRow = y >> 1;
            const uvW = width >> 1;
            const uvPos = uvRow * uvW + uvCol;
            U[uvPos] = Uval;
            V[uvPos] = Vval;
        }
    }
    return out;
}
export function convertNv12ToI420(nv12Buffer, width, height, yStride, uvStride, yOffset = 0, uvOffset = yStride * height, cropLeft = 0, cropTop = 0) {
    const ySize = width * height;
    const uvSize = ySize / 4;
    const i420Buffer = new Uint8Array(ySize + 2 * uvSize);
    for (let row = 0; row < height; row++) {
        const srcOffset = yOffset + (row + cropTop) * yStride + cropLeft;
        const dstOffset = row * width;
        i420Buffer.set(nv12Buffer.subarray(srcOffset, srcOffset + width), dstOffset);
    }
    const uPlaneOffset = ySize;
    const vPlaneOffset = ySize + uvSize;
    const uvHeight = height / 2;
    const uvWidth = width / 2;
    const uvCropLeft = cropLeft >> 1;
    const uvCropTop = cropTop >> 1;
    for (let row = 0; row < uvHeight; row++) {
        for (let col = 0; col < uvWidth; col++) {
            const srcIndex = uvOffset + (row + uvCropTop) * uvStride + (col + uvCropLeft) * 2;
            const dstIndex = row * uvWidth + col;
            i420Buffer[uPlaneOffset + dstIndex] = nv12Buffer[srcIndex] ?? 0; // U
            i420Buffer[vPlaneOffset + dstIndex] = nv12Buffer[srcIndex + 1] ?? 0; // V
        }
    }
    return i420Buffer;
}
export function repackI420(source, width, height, yLayout, uLayout, vLayout, cropLeft = 0, cropTop = 0) {
    const ySize = width * height;
    const uvWidth = width >> 1;
    const uvHeight = height >> 1;
    const uvSize = uvWidth * uvHeight;
    const out = new Uint8Array(ySize + uvSize * 2);
    const uvCropLeft = cropLeft >> 1;
    const uvCropTop = cropTop >> 1;
    for (let row = 0; row < height; row++) {
        const srcOffset = yLayout.offset + (row + cropTop) * yLayout.stride + cropLeft;
        out.set(source.subarray(srcOffset, srcOffset + width), row * width);
    }
    for (let row = 0; row < uvHeight; row++) {
        const uSrcOffset = uLayout.offset + (row + uvCropTop) * uLayout.stride + uvCropLeft;
        const vSrcOffset = vLayout.offset + (row + uvCropTop) * vLayout.stride + uvCropLeft;
        const dstOffset = row * uvWidth;
        out.set(source.subarray(uSrcOffset, uSrcOffset + uvWidth), ySize + dstOffset);
        out.set(source.subarray(vSrcOffset, vSrcOffset + uvWidth), ySize + uvSize + dstOffset);
    }
    return out;
}
export async function copyVideoFrameToI420(frame, logPrefix) {
    const visibleRect = frame.visibleRect;
    const frameWidth = visibleRect?.width ?? frame.codedWidth;
    const frameHeight = visibleRect?.height ?? frame.codedHeight;
    const cropLeft = visibleRect?.x ?? 0;
    const cropTop = visibleRect?.y ?? 0;
    let finalBuffer = new Uint8Array(frame.allocationSize());
    logger.warn(`${logPrefix} decoder output format: ${frame.format}, coded=${frame.codedWidth}x${frame.codedHeight}, display=${frame.displayWidth}x${frame.displayHeight}, visible=${frameWidth}x${frameHeight}@${cropLeft},${cropTop}`);
    switch (frame.format) {
        case "I420": {
            const i420Buffer = new Uint8Array(frame.allocationSize());
            const layouts = await frame.copyTo(i420Buffer);
            const [yLayout, uLayout, vLayout] = layouts;
            finalBuffer =
                yLayout != undefined && uLayout != undefined && vLayout != undefined
                    ? repackI420(i420Buffer, frameWidth, frameHeight, yLayout, uLayout, vLayout, cropLeft, cropTop)
                    : i420Buffer;
            break;
        }
        case "NV12": {
            const nv12Buffer = new Uint8Array(frame.allocationSize());
            const layouts = await frame.copyTo(nv12Buffer);
            const [yLayout, uvLayout] = layouts;
            finalBuffer = convertNv12ToI420(nv12Buffer, frameWidth, frameHeight, yLayout?.stride ?? frame.codedWidth, uvLayout?.stride ?? frame.codedWidth, yLayout?.offset ?? 0, uvLayout?.offset ?? (yLayout?.stride ?? frame.codedWidth) * frame.codedHeight, cropLeft, cropTop);
            break;
        }
        case "RGBX":
        case "RGBA":
        case "BGRX":
        case "BGRA": {
            const packedBuffer = new Uint8Array(frame.allocationSize());
            const [layout] = await frame.copyTo(packedBuffer);
            logger.warn(`${logPrefix} decoder received ${frame.format} frame.`);
            finalBuffer = packedToI420(packedBuffer, frameWidth, frameHeight, layout?.stride ?? frame.codedWidth * 4, frame.format, cropLeft, cropTop);
            break;
        }
        default:
            logger.warn(`${logPrefix} decoder received unexpected frame format:`, frame.format);
            break;
    }
    return {
        data: finalBuffer,
        width: frameWidth,
        height: frameHeight,
    };
}
function createVideoDecoder({ topic, callback, topicDecoders, onError, }) {
    let hasProducedFrame = false;
    return new VideoDecoder({
        output: async (frame) => {
            hasProducedFrame = true;
            const { data: finalBuffer, width: frameWidth, height: frameHeight } = await copyVideoFrameToI420(frame, "H264");
            const receiveTime = topicDecoders.get(topic)?.timing.get(frame.timestamp)
                ??
                    fromMicros(frame.timestamp);
            const transformation = topicDecoders.get(topic)?.transformations.get(frame.timestamp);
            topicDecoders.get(topic)?.transformations.delete(frame.timestamp);
            const foxgloveMessage = {
                timestamp: receiveTime,
                frame_id: `h264-${topic}-frame`,
                width: frameWidth,
                height: frameHeight,
                data: finalBuffer,
                encoding: "yuv420p",
                step: frameWidth
            };
            callback({
                topic,
                receiveTime,
                message: foxgloveMessage,
                sizeInBytes: estimateObjectSize(foxgloveMessage),
                schemaName: "foxglove.RawImage",
                depthaiTransformation: transformation,
            });
            frame.close();
        },
        error: (error) => {
            if (!hasProducedFrame && isIgnorableInitialDecodeError(error)) {
                logger.warn(`Ignoring initial H264 decoder error for topic ${topic}: ${error.message}`);
                closeTopicDecoder(topic, topicDecoders);
                return;
            }
            logger.error(error);
            closeTopicDecoder(topic, topicDecoders);
            onError(new DecoderRuntimeError(topic, error.message));
        },
    });
}
export async function deserializeEncodedFrameH264({ topic, message, topicDecoders, callback, onError, }) {
    const { receiveTime } = parseMessage(message);
    const supported = ensureWebCodecsSupported("VideoDecoder");
    if (!supported) {
        return;
    }
    const existingDecoderInfo = topicDecoders.get(topic);
    if (!existingDecoderInfo || existingDecoderInfo.decoder.state === "closed") {
        const decoder = createVideoDecoder({ topic, callback, topicDecoders, onError });
        try {
            configureDecoder(decoder);
        }
        catch (error) {
            closeDecoder(decoder);
            onError(new DecoderRuntimeError(topic, error instanceof Error ? error.message : "Failed to configure H.264 decoder."));
            return;
        }
        if (existingDecoderInfo) {
            existingDecoderInfo.decoder = decoder;
            existingDecoderInfo.timing.clear();
            existingDecoderInfo.transformations.clear();
        }
        else {
            topicDecoders.set(topic, {
                decoder,
                timing: new Map(),
                transformations: new Map(),
            });
        }
    }
    else if (existingDecoderInfo.decoder.state === "unconfigured") {
        try {
            configureDecoder(existingDecoderInfo.decoder);
            existingDecoderInfo.timing.clear();
            existingDecoderInfo.transformations.clear();
        }
        catch (error) {
            closeTopicDecoder(topic, topicDecoders);
            onError(new DecoderRuntimeError(topic, error instanceof Error ? error.message : "Failed to reconfigure H.264 decoder."));
            return;
        }
    }
    const decoderInfo = topicDecoders.get(topic);
    if (!decoderInfo) {
        return;
    }
    if (decoderInfo.decoder.decodeQueueSize > 60) {
        decoderInfo.decoder.reset();
        decoderInfo.timing.clear();
        decoderInfo.transformations.clear();
        return;
    }
    const microTimestamp = receiveTime.sec * 1_000_000 + Math.floor(receiveTime.nsec / 1_000);
    decoderInfo.timing.set(microTimestamp, receiveTime);
    decoderInfo.transformations.set(microTimestamp, message.transformation);
    const frame = {
        type: 'key',
        data: message.data,
        timestamp: microTimestamp
    };
    try {
        const chunk = new EncodedVideoChunk(frame);
        decoderInfo.decoder.decode(chunk);
    }
    catch (error) {
        if (isIgnorableInitialDecodeError(error)) {
            closeTopicDecoder(topic, topicDecoders);
            return;
        }
        closeTopicDecoder(topic, topicDecoders);
        onError(new DecoderRuntimeError(topic, error instanceof Error ? error.message : "Failed to decode H.264 frame."));
    }
}
//# sourceMappingURL=h264.js.map