UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

229 lines (228 loc) 9.95 kB
import { d as asPositiveSafeInteger, f as asSafeIntegerInRange } from "./number-coercion-CLj0HTDM.js"; import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js"; import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { t as resolveSystemBin } from "./resolve-system-bin-IIzLwiri.js"; import { n as runExec } from "./exec-BIE-3oLG.js"; import fs from "node:fs/promises"; /** Default ffprobe timeout for lightweight metadata probes. */ const MEDIA_FFPROBE_TIMEOUT_MS = 1e4; /** Default ffmpeg timeout for bounded media conversion work. */ const MEDIA_FFMPEG_TIMEOUT_MS = 45e3; /** Maximum audio duration accepted by ffmpeg-backed media flows. */ const MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS = 1200; //#endregion //#region src/media/ffmpeg-exec.ts function resolveExecOptions(defaultTimeoutMs, options) { if (options?.input !== void 0 && options.stdinFileDescriptor !== void 0) throw new Error("media exec accepts either input or stdinFileDescriptor, not both"); return { input: options?.input, ...options?.stdinFileDescriptor !== void 0 ? { stdinFileDescriptor: options.stdinFileDescriptor } : {}, logOutput: false, maxBuffer: options?.maxBufferBytes ?? 10485760, timeoutMs: options?.timeoutMs ?? defaultTimeoutMs }; } function requireSystemBin(name) { const resolved = resolveSystemBin(name, { trust: "standard" }); if (!resolved) { const hint = process.platform === "darwin" ? "e.g. brew install ffmpeg" : "e.g. apt install ffmpeg / dnf install ffmpeg"; throw new Error(`${name} not found in trusted system directories. Install it via your system package manager (${hint}).`); } return resolved; } /** Resolves ffmpeg from trusted system paths before command execution. */ function resolveFfmpegBin() { return requireSystemBin("ffmpeg"); } /** Runs ffprobe with optional stdin input. */ async function runFfprobe(args, options) { const { stdout } = await runExec(requireSystemBin("ffprobe"), args, resolveExecOptions(MEDIA_FFPROBE_TIMEOUT_MS, options)); return stdout; } /** Runs ffmpeg with bounded timeout and buffer settings. */ async function runFfmpeg(args, options) { const { stdout } = await runExec(resolveFfmpegBin(), args, resolveExecOptions(MEDIA_FFMPEG_TIMEOUT_MS, options)); return stdout; } /** Splits ffprobe CSV-ish output into normalized lowercase fields. */ function parseFfprobeCsvFields(stdout, maxFields) { return stdout.trim().split(/[,\r\n]+/, maxFields).map((field) => normalizeLowercaseStringOrEmpty(field)); } function parseFfprobeSampleRateHz(value) { if (!value || !/^\d+$/.test(value)) return null; const sampleRate = Number(value); return Number.isSafeInteger(sampleRate) && sampleRate > 0 ? sampleRate : null; } /** Parses codec and positive sample rate from compact ffprobe stream output. */ function parseFfprobeCodecAndSampleRate(stdout) { const [codecRaw, sampleRateRaw] = parseFfprobeCsvFields(stdout, 2); return { codec: codecRaw ? codecRaw : null, sampleRateHz: parseFfprobeSampleRateHz(sampleRateRaw) }; } //#endregion //#region src/media/media-probe.ts function parseDurationMs(value) { if (typeof value !== "number" && typeof value !== "string") return; const seconds = typeof value === "number" ? value : Number(value.trim()); if (!Number.isFinite(seconds) || seconds <= 0) return; return asPositiveSafeInteger(Math.round(seconds * 1e3)); } function normalizeCodecName(value) { if (typeof value !== "string") return; return value.trim().toLowerCase() || void 0; } function parseStreamIndex(value) { return asSafeIntegerInRange(value, { min: 0 }); } function selectPlaybackStream(streams, codecType) { const candidates = streams.filter((stream) => { if (stream.codec_type !== codecType) return false; const disposition = asOptionalRecord(stream.disposition); return codecType !== "video" || disposition?.attached_pic !== 1; }); return candidates.find((stream) => asOptionalRecord(stream.disposition)?.default === 1) ?? candidates[0]; } function parseFfprobeMediaMetadata(stdout, kind) { let parsed; try { parsed = JSON.parse(stdout); } catch { return null; } const root = asOptionalRecord(parsed); if (!root) return null; const format = asOptionalRecord(root.format); const streams = (Array.isArray(root.streams) ? root.streams : []).map(asOptionalRecord).filter((stream) => Boolean(stream)); const audioStream = selectPlaybackStream(streams, "audio"); const videoStream = selectPlaybackStream(streams, "video"); const selectedDurations = (kind === "video" ? [videoStream, audioStream] : [audioStream]).map((stream) => parseDurationMs(stream?.duration)).filter((duration) => duration !== void 0); const durationMs = (selectedDurations.length > 0 ? Math.max(...selectedDurations) : void 0) ?? parseDurationMs(format?.duration); const width = asPositiveSafeInteger(videoStream?.width); const height = asPositiveSafeInteger(videoStream?.height); const videoRotation = (Array.isArray(videoStream?.side_data_list) ? videoStream.side_data_list : []).map((sideData) => asSafeIntegerInRange(asOptionalRecord(sideData)?.rotation, {})).find((rotation) => rotation !== void 0); const audioCodec = normalizeCodecName(audioStream?.codec_name); const videoCodec = normalizeCodecName(videoStream?.codec_name); const videoPixelFormat = normalizeCodecName(videoStream?.pix_fmt); const videoProfile = normalizeCodecName(videoStream?.profile); const audioStreamIndex = parseStreamIndex(audioStream?.index); const videoStreamIndex = parseStreamIndex(videoStream?.index); return { ...durationMs ? { durationMs } : {}, ...kind === "video" && width && height ? { width, height } : {}, ...videoRotation !== void 0 ? { videoRotation } : {}, ...audioCodec ? { audioCodec } : {}, ...audioStreamIndex !== void 0 ? { audioStreamIndex } : {}, ...videoCodec ? { videoCodec } : {}, ...videoPixelFormat ? { videoPixelFormat } : {}, ...videoProfile ? { videoProfile } : {}, ...videoStreamIndex !== void 0 ? { videoStreamIndex } : {} }; } function buildFfprobeMetadataArgs(protocol) { const isFileDescriptor = protocol === "fd"; return [ "-v", "error", "-protocol_whitelist", protocol, "-show_entries", "format=duration:stream=index,codec_type,codec_name,profile,pix_fmt,duration,width,height:stream_disposition=default,attached_pic:stream_side_data=rotation", "-of", "json", ...isFileDescriptor ? ["-fd", "0"] : [], isFileDescriptor ? "fd:" : "pipe:0" ]; } function isMissingFdProtocolError(error) { if (!error || typeof error !== "object") return false; const stderr = error.stderr; const message = typeof stderr === "string" ? stderr : error instanceof Error ? error.message : ""; return /(?:fd:.*protocol not found|protocol not found.*fd|unrecognized option ['"]?fd|option fd not found)/is.test(message); } async function probeMediaSource(source, kind, options = {}) { const runProbe = async (protocol) => await runFfprobe(buildFfprobeMetadataArgs(protocol), source.kind === "buffer" ? { input: source.buffer, ...options } : { stdinFileDescriptor: source.fd, ...options }); try { return parseFfprobeMediaMetadata(await runProbe(source.kind === "fileDescriptor" ? "fd" : "pipe"), kind); } catch (error) { if (source.kind === "fileDescriptor" && isMissingFdProtocolError(error)) try { return parseFfprobeMediaMetadata(await runProbe("pipe"), kind); } catch { return null; } return null; } } /** Keep encoded playback facts separate from attachment display dimensions. */ function toMediaProbeResult(result) { if (!result) return {}; const swapsAxes = Math.abs(result.videoRotation ?? 0) % 180 === 90; return { ...result.durationMs ? { durationMs: result.durationMs } : {}, ...result.width && result.height ? { width: swapsAxes ? result.height : result.width, height: swapsAxes ? result.width : result.height } : {} }; } /** Probes a local audio or video file; every failure degrades to absent fields. */ async function probeMediaFile(filePath, kind, options = {}) { try { const handle = await fs.open(filePath, "r"); try { return toMediaProbeResult(await probeMediaSource({ kind: "fileDescriptor", fd: handle.fd }, kind, options)); } finally { await handle.close().catch(() => {}); } } catch { return {}; } } /** Probes a bounded batch under one elapsed-time budget, unaffected by wall-clock steps. */ async function probeMediaFilesWithinBudget(inputs, options) { const results = inputs.map(() => ({})); const deadlineMs = performance.now() + options.budgetMs; const probeCount = Math.min(inputs.length, options.maxProbes); for (let offset = 0; offset < probeCount; offset += options.concurrency) { const timeoutMs = deadlineMs - performance.now(); if (timeoutMs <= 0) break; const batchEnd = Math.min(offset + options.concurrency, probeCount); const batch = inputs.slice(offset, batchEnd); const batchResults = await Promise.all(batch.map((input) => probeMediaFile(input.filePath, input.kind, { timeoutMs }))); for (const [batchIndex, metadata] of batchResults.entries()) results[offset + batchIndex] = metadata; } return results; } /** Probes duration and first-stream codecs from an already validated local descriptor. */ async function probePlaybackMediaFileDescriptor(fd, kind, options = {}) { return await probeMediaSource({ kind: "fileDescriptor", fd }, kind, options); } /** Probes a video buffer while preserving the existing public media-runtime API. */ async function probeVideoDimensions(buffer) { const { width, height } = toMediaProbeResult(await probeMediaSource({ kind: "buffer", buffer }, "video")); return width && height ? { width, height } : void 0; } //#endregion export { parseFfprobeCodecAndSampleRate as a, runFfprobe as c, toMediaProbeResult as i, MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS as l, probePlaybackMediaFileDescriptor as n, resolveFfmpegBin as o, probeVideoDimensions as r, runFfmpeg as s, probeMediaFilesWithinBudget as t };