← Files Cecil-IA Labs FFmpegARCHIVED FILE

dist/streaming/plan.js

9.54 KB · Sep 30, 2026 · 23:17 UTC

↓ Download file

import { ToolkitRuntimeError } from "../core/errors.js";
const VIDEO_SIZE = /^\d+x\d+$/;
const BITRATE = /^\d+(?:\.\d+)?[kKmMgG]?$/;
function schemeOf(value) {
    try {
        return new URL(value).protocol.replace(/:$/, "").toLowerCase();
    }
    catch (error) {
        throw new ToolkitRuntimeError("E_USAGE_INVALID_ARGUMENT", "Streaming destination must be a valid URL.", {
            details: { url: value },
            cause: error,
        });
    }
}
export function inferTransport(url) {
    const scheme = schemeOf(url);
    switch (scheme) {
        case "http":
        case "https":
            return "http";
        case "rtmp":
        case "rtmps":
            return "rtmp";
        case "rtsp":
            return "rtsp";
        case "srt":
            return "srt";
        case "udp":
            return "udp";
        case "tcp":
            return "tcp";
        case "ws":
        case "wss":
            throw new ToolkitRuntimeError("E_OPERATION_UNSUPPORTED", "Stock FFmpeg does not provide the WebSocket relay required by this toolkit. Stream to an HTTP/TCP/UDP/SRT endpoint or place an explicit WebSocket relay in front of the destination.", { details: { url, transport: "websocket" } });
        default:
            throw new ToolkitRuntimeError("E_USAGE_INVALID_ARGUMENT", `Unsupported streaming URL scheme: ${scheme || "(none)"}.`, {
                details: { url, supported: ["http", "https", "rtmp", "rtmps", "rtsp", "srt", "udp", "tcp"] },
            });
    }
}
function allowedSchemes(transport) {
    switch (transport) {
        case "http":
            return ["http", "https"];
        case "rtmp":
            return ["rtmp", "rtmps"];
        case "rtsp":
            return ["rtsp"];
        case "srt":
            return ["srt"];
        case "udp":
            return ["udp"];
        case "tcp":
            return ["tcp"];
    }
}
function defaultContainer(transport) {
    if (transport === "rtmp")
        return "flv";
    if (transport === "rtsp")
        return "rtsp";
    return "mpegts";
}
function validateContainer(transport, container) {
    if (transport === "rtmp" && container !== "flv") {
        throw new ToolkitRuntimeError("E_CONFIG_CONFLICT", "RTMP output requires the FLV container.", {
            details: { transport, container },
        });
    }
    if (transport === "rtsp" && container !== "rtsp") {
        throw new ToolkitRuntimeError("E_CONFIG_CONFLICT", "RTSP output requires the RTSP muxer.", {
            details: { transport, container },
        });
    }
    if (!["rtmp", "rtsp"].includes(transport) && container !== "mpegts") {
        throw new ToolkitRuntimeError("E_CONFIG_CONFLICT", `${transport.toUpperCase()} streaming currently uses MPEG-TS in Milestone 9.`, { details: { transport, container } });
    }
}
export function resolveDestination(options) {
    if (options.transport === "websocket") {
        throw new ToolkitRuntimeError("E_OPERATION_UNSUPPORTED", "Direct WebSocket streaming is intentionally not implemented. Use an HTTP/TCP/UDP/SRT relay and let that service bridge to WebSocket clients.", { details: { transport: "websocket", url: options.url } });
    }
    const inferred = inferTransport(options.url);
    const transport = options.transport ?? inferred;
    const scheme = schemeOf(options.url);
    if (!allowedSchemes(transport).includes(scheme)) {
        throw new ToolkitRuntimeError("E_CONFIG_CONFLICT", "Destination URL scheme does not match --transport.", {
            details: { url: options.url, scheme, transport, allowedSchemes: allowedSchemes(transport) },
        });
    }
    const container = options.container ?? defaultContainer(transport);
    validateContainer(transport, container);
    return {
        url: options.url,
        transport,
        container,
        ...(transport === "rtsp" ? { rtspTransport: options.rtspTransport ?? "tcp" } : {}),
    };
}
function validateEncodingValue(value, name) {
    if (value !== undefined && !BITRATE.test(value)) {
        throw new ToolkitRuntimeError("E_USAGE_INVALID_ARGUMENT", `${name} must be a FFmpeg bitrate such as 500k, 2M, or 1500000.`, {
            details: { name, value },
        });
    }
}
export function resolveEncoding(options, defaults) {
    validateEncodingValue(options.videoBitrate, "videoBitrate");
    validateEncodingValue(options.audioBitrate, "audioBitrate");
    if (options.gop !== undefined && (!Number.isInteger(options.gop) || options.gop <= 0)) {
        throw new ToolkitRuntimeError("E_USAGE_INVALID_ARGUMENT", "GOP size must be a positive integer.", {
            details: { gop: options.gop },
        });
    }
    return {
        videoCodec: options.videoCodec ?? "h264",
        audioCodec: options.audioCodec ?? defaults.audio,
        ...(options.videoBitrate !== undefined ? { videoBitrate: options.videoBitrate } : {}),
        ...(options.audioBitrate !== undefined ? { audioBitrate: options.audioBitrate } : {}),
        ...(options.preset !== undefined ? { preset: options.preset } : {}),
        ...(options.gop !== undefined ? { gop: options.gop } : {}),
        ...(options.pixelFormat !== undefined ? { pixelFormat: options.pixelFormat } : {}),
    };
}
export function resolveCameraInputFormat(requested, platform = process.platform) {
    if (requested !== undefined)
        return requested;
    if (platform === "linux")
        return "v4l2";
    if (platform === "darwin")
        return "avfoundation";
    if (platform === "win32")
        return "dshow";
    throw new ToolkitRuntimeError("E_OPERATION_UNSUPPORTED", `Automatic camera capture format is not defined for platform ${platform}; pass --input-format explicitly.`, { details: { platform } });
}
function encodingArgs(encoding) {
    const args = [];
    if (encoding.videoCodec === "copy") {
        args.push("-c:v", "copy");
    }
    else if (encoding.videoCodec === "mpeg1video") {
        args.push("-c:v", "mpeg1video", "-bf", "0");
    }
    else {
        args.push("-c:v", "libx264", "-preset", encoding.preset ?? "veryfast", "-tune", "zerolatency", "-pix_fmt", encoding.pixelFormat ?? "yuv420p", "-g", String(encoding.gop ?? 60));
    }
    if (encoding.videoBitrate !== undefined)
        args.push("-b:v", encoding.videoBitrate);
    if (encoding.audioCodec === "none") {
        args.push("-an");
    }
    else if (encoding.audioCodec === "copy") {
        args.push("-c:a", "copy");
    }
    else {
        args.push("-c:a", "aac", "-b:a", encoding.audioBitrate ?? "128k");
    }
    return args;
}
function destinationArgs(destination) {
    const args = [];
    if (destination.transport === "rtsp") {
        args.push("-rtsp_transport", destination.rtspTransport ?? "tcp");
    }
    args.push("-f", destination.container, destination.url);
    return args;
}
function compatibilityWarnings(destination, encoding) {
    const warnings = [];
    if (encoding.videoCodec === "copy") {
        warnings.push({
            code: "W_STREAM_COPY_COMPATIBILITY",
            message: "Stream-copy mode depends on the source codec being compatible with the selected container and receiver.",
            details: { container: destination.container, transport: destination.transport },
        });
    }
    if (destination.transport === "http") {
        warnings.push({
            code: "W_HTTP_RECEIVER_REQUIRED",
            message: "HTTP output requires an endpoint that accepts a streaming FFmpeg request body; a normal static web server is not sufficient.",
            details: { url: destination.url },
        });
    }
    return warnings;
}
export function buildFileStreamPlan(source, media, options) {
    const destination = resolveDestination(options);
    const defaultAudio = media !== undefined && media.audio.length > 0 ? "aac" : "none";
    const encoding = resolveEncoding(options, { audio: defaultAudio });
    const args = ["-hide_banner"];
    if (options.realtime ?? true)
        args.push("-re");
    args.push("-i", source, "-map", "0:v:0?");
    if (encoding.audioCodec !== "none")
        args.push("-map", "0:a:0?");
    args.push(...encodingArgs(encoding), ...destinationArgs(destination));
    return { destination, encoding, args, warnings: compatibilityWarnings(destination, encoding) };
}
export function buildCameraStreamPlan(options) {
    if (options.device.trim().length === 0) {
        throw new ToolkitRuntimeError("E_USAGE_INVALID_ARGUMENT", "Camera device must not be empty.");
    }
    if (options.videoSize !== undefined && !VIDEO_SIZE.test(options.videoSize)) {
        throw new ToolkitRuntimeError("E_USAGE_INVALID_ARGUMENT", "Camera video size must use WIDTHxHEIGHT syntax.", {
            details: { videoSize: options.videoSize },
        });
    }
    if (options.framerate !== undefined && (!Number.isFinite(options.framerate) || options.framerate <= 0)) {
        throw new ToolkitRuntimeError("E_USAGE_INVALID_ARGUMENT", "Camera frame rate must be greater than zero.", {
            details: { framerate: options.framerate },
        });
    }
    const inputFormat = resolveCameraInputFormat(options.inputFormat);
    const destination = resolveDestination(options);
    const encoding = resolveEncoding(options, { audio: "none" });
    const args = ["-hide_banner", "-thread_queue_size", "512", "-f", inputFormat];
    if (options.framerate !== undefined)
        args.push("-framerate", String(options.framerate));
    if (options.videoSize !== undefined)
        args.push("-video_size", options.videoSize);
    args.push("-i", options.device, "-map", "0:v:0", ...encodingArgs(encoding), ...destinationArgs(destination));
    return { inputFormat, destination, encoding, args, warnings: compatibilityWarnings(destination, encoding) };
}
//# sourceMappingURL=plan.js.map

SHA-256: 0fa4a6d45b93ba6321e4ca61a9ab01a76cfae85ccc6a3ea6fd160b7ea9d72c1a