UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

628 lines (627 loc) 25 kB
import { i as getOrCreatePromise } from "./lazy-promise-DGqyc4Y4.js"; import { u as openLocalFileSafely } from "./fs-safe-B6pvPGnf.js"; import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DnyL0lW9.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { r as withTempWorkspace } from "./private-temp-workspace-Dh7teFP0.js"; import { a as maxBytesForKind } from "./constants-DUxuqQz8.js"; import { d as normalizeMimeType, l as kindFromMime, r as extensionForMime } from "./mime-CVpcq9ju.js"; import { h as writePlaybackTranscodeCache, n as PLAYBACK_TRANSCODE_SUBDIR, s as getMediaDir, v as fileStore } from "./store-Ph57PZ9P.js"; import { n as probePlaybackMediaFileDescriptor, s as runFfmpeg } from "./media-probe-Dbs998-L.js"; import { t as matchesHttpIfNoneMatch } from "./http-conditional-BWrY1Un1.js"; import path from "node:path"; import fs from "node:fs/promises"; import { createHash } from "node:crypto"; import { parseRetryAfterHttpDateMs } from "@openclaw/ai/internal/retry-after"; //#region src/media/playback-codec-policy.ts /** Combines selected audio/video codec facts without letting unknown facts hide incompatibility. */ function resolveNativePlaybackCodecCompatibility(kind, mimeType, probe) { if (kind === "audio") { const codec = probe.audioCodec; if (probe.audioStreamIndex === void 0 || !codec) return; if (/^audio\/(?:x-wav|wav|wave)$/.test(normalizeMimeType(mimeType) ?? "")) return codec === "pcm_s16le" || codec === "pcm_u8"; return codec === "mp3" || normalizeMimeType(mimeType) !== "audio/mpeg" && codec === "aac"; } const audioCompatible = probe.audioStreamIndex === void 0 ? probe.audioCodec ? void 0 : true : probe.audioCodec ? probe.audioCodec === "aac" || probe.audioCodec === "mp3" : void 0; let videoCompatible; if (probe.videoCodec && probe.videoStreamIndex !== void 0) { const portableProfile = probe.videoProfile === "baseline" || probe.videoProfile === "constrained baseline" || probe.videoProfile === "main" || probe.videoProfile === "high"; const portablePixelFormat = probe.videoPixelFormat === "yuv420p" || probe.videoPixelFormat === "yuvj420p"; videoCompatible = probe.videoCodec === "h264" && probe.videoProfile && probe.videoPixelFormat ? portableProfile && portablePixelFormat : probe.videoCodec === "h264" ? void 0 : false; } return audioCompatible === false || videoCompatible === false ? false : audioCompatible === true && videoCompatible === true ? true : void 0; } //#endregion //#region src/media/playback-transcode.ts /** * Native means safe across the supported browser, AVPlayer, and ExoPlayer clients. * Client-specific formats stay in the transcode path because metadata cannot know its consumer. */ const PLAYBACK_TRANSCODE_POLICY = { audio: { nativeMimeTypes: [ "audio/m4a", "audio/mp3", "audio/mp4", "audio/mpeg", "audio/wav", "audio/wave", "audio/x-m4a", "audio/x-wav" ], codecProbeInputFormats: { "audio/m4a": "mov", "audio/mpeg": "mp3", "audio/mp4": "mov", "audio/wav": "wav", "audio/wave": "wav", "audio/x-m4a": "mov", "audio/x-wav": "wav" }, transcodeInputFormats: { "audio/aac": "aac", "audio/aiff": "aiff", "audio/amr": "amr", "audio/amr-wb": "amr", "audio/flac": "flac", "audio/ogg": "ogg", "audio/opus": "ogg", "audio/vorbis": "ogg", "audio/webm": "matroska,webm", "audio/x-aiff": "aiff", "audio/x-caf": "caf", "audio/x-ms-asf": "asf", "audio/x-ms-wma": "asf" }, target: { contentType: "audio/mp4", extension: ".m4a" } }, video: { nativeMimeTypes: ["video/mp4"], codecProbeInputFormats: { "video/mp4": "mov" }, transcodeInputFormats: { "video/avi": "avi", "video/flv": "flv", "video/matroska": "matroska,webm", "video/quicktime": "mov", "video/webm": "matroska,webm", "video/x-flv": "flv", "video/x-matroska": "matroska,webm", "video/x-ms-asf": "asf", "video/x-ms-wmv": "asf", "video/x-msvideo": "avi" }, target: { contentType: "video/mp4", extension: ".mp4" } } }; const PLAYBACK_TRANSCODE_CACHE_VERSION = "v2"; const MAX_PLAYBACK_TRANSCODE_JOBS = 2; const PLAYBACK_TRANSCODE_MAX_ALLOC_BYTES = 268435456; const PLAYBACK_TRANSCODE_MAX_DURATION_SECS = 1200; const PLAYBACK_TRANSCODE_MAX_INPUT_PIXELS = 16777216; const PLAYBACK_TRANSCODE_THREADS = 2; const PLAYBACK_TRANSCODE_FAILURE_COOLDOWN_MS = 6e4; const MAX_PLAYBACK_ENTRIES = { failures: 32, inspections: 32, inspectionJobs: 2 }; const playbackJobs = /* @__PURE__ */ new Map(); const playbackFailures = /* @__PURE__ */ new Map(); const playbackInspections = /* @__PURE__ */ new Map(); const playbackInspectionJobs = /* @__PURE__ */ new Map(); const log = createSubsystemLogger("media/playback"); /** Hashes the immutable source identity used by playback cache file names. */ function createPlaybackTranscodeCacheKey(source) { return createHash("sha256").update(JSON.stringify([ source.path, source.size, source.mtimeMs, source.ctimeMs, source.dev, source.ino ])).digest("hex"); } async function readPlaybackSourceBounded(handle, expectedSize, maxBytes) { const maxReadBytes = Math.min(maxBytes + 1, expectedSize + 1); const buffer = Buffer.allocUnsafe(maxReadBytes); let totalBytes = 0; while (totalBytes < maxReadBytes) { const { bytesRead } = await handle.read(buffer, totalBytes, maxReadBytes - totalBytes, totalBytes); if (bytesRead === 0) break; totalBytes += bytesRead; } if (totalBytes > maxBytes || totalBytes !== expectedSize) throw new Error("Playback source changed during bounded read"); return buffer.subarray(0, totalBytes); } /** Returns whether a sniffed audio/video type needs the cross-client playback target. */ function resolvePlaybackMode(mimeType, policy) { const mime = normalizeMimeType(mimeType); if (!mime) return; if (policy.nativeMimeTypes.includes(mime)) return "native"; return policy.transcodeInputFormats[mime] ? "transcode" : void 0; } if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.playbackTranscodeTestApi")] = { createPlaybackTranscodeCacheKey, readPlaybackSourceBounded, getPlaybackTranscodeJobs: () => [...playbackJobs.values()] }; function playbackSourceIdentity(params) { return { path: params.sourcePath, ...params.sourceStat }; } function playbackSourceIdentityMatches(source, opened) { return opened.realPath === source.path && opened.stat.size === source.size && opened.stat.mtimeMs === source.mtimeMs && opened.stat.ctimeMs === source.ctimeMs && opened.stat.dev === source.dev && opened.stat.ino === source.ino; } function readPlaybackInspection(cacheKey) { const inspection = playbackInspections.get(cacheKey); if (inspection) cachePlaybackInspection(cacheKey, inspection); return inspection; } function cachePlaybackInspection(cacheKey, inspection) { playbackInspections.delete(cacheKey); playbackInspections.set(cacheKey, inspection); pruneMapToMaxSize(playbackInspections, MAX_PLAYBACK_ENTRIES.inspections); } function playbackInspectionCacheKey(params) { return `${params.sourceCacheKey}:${params.kind}:${normalizeMimeType(params.mimeType) ?? "unknown"}`; } async function probePlaybackSource(source, kind) { const opened = await openLocalFileSafely({ filePath: source.path }).catch(() => null); if (!opened) return null; try { if (!playbackSourceIdentityMatches(source, opened)) return null; return await probePlaybackMediaFileDescriptor(opened.handle.fd, kind); } finally { await opened.handle.close().catch(() => {}); } } async function inspectPlaybackSource(params) { const policy = PLAYBACK_TRANSCODE_POLICY[params.kind]; const containerMode = resolvePlaybackMode(params.mimeType, policy); if (!containerMode) return { mode: "fallback" }; const source = playbackSourceIdentity(params); const cacheKey = playbackInspectionCacheKey({ sourceCacheKey: createPlaybackTranscodeCacheKey(source), kind: params.kind, mimeType: params.mimeType }); const cached = readPlaybackInspection(cacheKey); if (cached) return cached; const computeInspection = async () => { const mimeType = normalizeMimeType(params.mimeType); const needsCodecProbe = Boolean(mimeType && policy.codecProbeInputFormats[mimeType]); if (containerMode === "native" && !needsCodecProbe) { const inspection = { mode: "native" }; cachePlaybackInspection(cacheKey, inspection); return inspection; } const probe = params.probe !== void 0 ? params.probe : await probePlaybackSource(source, params.kind); if (containerMode === "native") { const nativeCodecs = probe ? resolveNativePlaybackCodecCompatibility(params.kind, params.mimeType, probe) : void 0; if (nativeCodecs === true) { const inspection = { mode: "native" }; cachePlaybackInspection(cacheKey, inspection); return inspection; } if (nativeCodecs === void 0) return { mode: "native" }; } if (source.size > maxBytesForKind(params.kind)) return { mode: "fallback" }; const maxDurationMs = PLAYBACK_TRANSCODE_MAX_DURATION_SECS * 1e3; const primaryStreamIndex = params.kind === "audio" ? probe?.audioStreamIndex : probe?.videoStreamIndex; if (!probe?.durationMs || primaryStreamIndex === void 0) return { mode: "fallback" }; const inspection = probe.durationMs <= maxDurationMs ? { mode: "transcode", durationMs: probe.durationMs, ...probe.audioStreamIndex !== void 0 ? { audioStreamIndex: probe.audioStreamIndex } : {}, ...probe.videoStreamIndex !== void 0 ? { videoStreamIndex: probe.videoStreamIndex } : {} } : { mode: "fallback" }; cachePlaybackInspection(cacheKey, inspection); return inspection; }; if (params.probe !== void 0) return await computeInspection(); const existingJob = playbackInspectionJobs.get(cacheKey); if (existingJob) return await existingJob; if (playbackInspectionJobs.size >= MAX_PLAYBACK_ENTRIES.inspectionJobs) return { mode: "fallback" }; return await getOrCreatePromise(playbackInspectionJobs, cacheKey, computeInspection, { evictOnSettled: true }); } /** Resolves source-aware playback metadata and caches codec classification by file identity. */ async function resolvePlaybackModeForSource(params) { const inspection = await inspectPlaybackSource(params); return inspection.mode === "transcode" ? "transcode" : inspection.mode === "native" ? "native" : void 0; } /** Replaces the original container suffix for a transcoded response filename. */ function replacePlaybackFileExtension(fileName, extension) { const currentExtension = path.extname(fileName); return `${(currentExtension ? fileName.slice(0, -currentExtension.length) : fileName) || "media"}${extension}`; } function playbackCacheRelativePath(cacheKey, extension) { return `${PLAYBACK_TRANSCODE_SUBDIR}/${PLAYBACK_TRANSCODE_CACHE_VERSION}-${cacheKey}${extension}`; } async function resolveCachedPlaybackPath(params) { const opened = await fileStore({ rootDir: getMediaDir(), dirMode: 448, mode: 384, maxBytes: params.maxBytes }).open(playbackCacheRelativePath(params.cacheKey, params.extension)).catch(() => null); if (!opened?.stat.isFile()) { await opened?.handle.close().catch(() => {}); return null; } try { return opened.realPath; } finally { await opened.handle.close().catch(() => {}); } } function makePlaybackInputFileName(sourcePath, mimeType) { const sourceExtension = path.extname(sourcePath).toLowerCase(); return `input${/^\.[a-z0-9]{1,12}$/u.test(sourceExtension) ? sourceExtension : extensionForMime(mimeType) ?? ".media"}`; } function resolvePlaybackInputFormat(policy, mimeType) { const normalized = normalizeMimeType(mimeType); return normalized ? policy.transcodeInputFormats[normalized] ?? policy.codecProbeInputFormats[normalized] : void 0; } function playbackDurationsMatch(sourceDurationMs, outputDurationMs) { const toleranceMs = Math.min(2e3, Math.max(1e3, Math.ceil(sourceDurationMs * .02))); return outputDurationMs <= PLAYBACK_TRANSCODE_MAX_DURATION_SECS * 1e3 && Math.abs(outputDurationMs - sourceDurationMs) <= toleranceMs; } function buildPlaybackFfmpegArgs(params) { const common = [ "-hide_banner", "-loglevel", "error", "-max_alloc", String(PLAYBACK_TRANSCODE_MAX_ALLOC_BYTES), "-filter_threads", String(PLAYBACK_TRANSCODE_THREADS), "-y", "-protocol_whitelist", "file", "-f", params.inputFormat, "-max_pixels", String(PLAYBACK_TRANSCODE_MAX_INPUT_PIXELS), "-threads", String(PLAYBACK_TRANSCODE_THREADS), "-i", params.inputPath, "-map_metadata", "-1", "-map_chapters", "-1" ]; if (params.kind === "audio") { if (params.audioStreamIndex === void 0) throw new Error("Playback audio stream is missing"); return [ ...common, "-map", `0:${params.audioStreamIndex}`, "-vn", "-sn", "-dn", "-t", String(PLAYBACK_TRANSCODE_MAX_DURATION_SECS), "-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart", "-f", "ipod", "-fs", String(params.maxOutputBytes + 1), params.outputPath ]; } if (params.videoStreamIndex === void 0) throw new Error("Playback video stream is missing"); return [ ...common, "-map", `0:${params.videoStreamIndex}`, ...params.audioStreamIndex === void 0 ? [] : ["-map", `0:${params.audioStreamIndex}`], "-sn", "-dn", "-t", String(PLAYBACK_TRANSCODE_MAX_DURATION_SECS), "-vf", "scale=w='min(1920,iw)':h='min(1080,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2", "-c:v", "libx264", "-threads", String(PLAYBACK_TRANSCODE_THREADS), "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart", "-f", "mp4", "-fs", String(params.maxOutputBytes + 1), params.outputPath ]; } async function transcodePlaybackSource(params) { const policy = PLAYBACK_TRANSCODE_POLICY[params.kind]; const opened = await openLocalFileSafely({ filePath: params.source.path }); try { if (!playbackSourceIdentityMatches(params.source, opened)) throw new Error("Playback source changed before transcode"); const sourceBuffer = await readPlaybackSourceBounded(opened.handle, params.source.size, params.maxBytes); const postReadStat = await opened.handle.stat(); if (!playbackSourceIdentityMatches(params.source, { realPath: opened.realPath, stat: postReadStat })) throw new Error("Playback source changed during transcode read"); const outputBuffer = await withTempWorkspace({ rootDir: resolvePreferredOpenClawTmpDir(), prefix: "playback-transcode-" }, async (workspace) => { const inputPath = await workspace.write(makePlaybackInputFileName(params.source.path, params.mimeType), sourceBuffer); const outputPath = workspace.path(`output${policy.target.extension}`); const inputFormat = resolvePlaybackInputFormat(policy, params.mimeType); if (!inputFormat) throw new Error("Playback transcode input format is not allowed"); await runFfmpeg(buildPlaybackFfmpegArgs({ ...params.audioStreamIndex !== void 0 ? { audioStreamIndex: params.audioStreamIndex } : {}, inputPath, inputFormat, kind: params.kind, maxOutputBytes: params.maxBytes, outputPath, ...params.videoStreamIndex !== void 0 ? { videoStreamIndex: params.videoStreamIndex } : {} })); const outputStat = await fs.stat(outputPath); if (!outputStat.isFile() || outputStat.size === 0 || outputStat.size > params.maxBytes) throw new Error("Playback transcode output exceeds its media limit"); const outputHandle = await fs.open(outputPath, "r"); let outputProbe; try { outputProbe = await probePlaybackMediaFileDescriptor(outputHandle.fd, params.kind); } finally { await outputHandle.close().catch(() => {}); } if (!outputProbe?.durationMs || !playbackDurationsMatch(params.sourceDurationMs, outputProbe.durationMs)) throw new Error("Playback transcode output duration does not match its source"); return await fs.readFile(outputPath); }); await writePlaybackTranscodeCache({ buffer: outputBuffer, fileName: path.basename(playbackCacheRelativePath(params.cacheKey, policy.target.extension)), maxBytes: params.maxBytes, tempPrefix: `.${params.cacheKey}` }); } finally { await opened.handle.close().catch(() => {}); } } /** Resolves a native, pending, cached, or failed playback rendition without blocking on ffmpeg. */ async function resolvePlaybackTranscode(params) { const policy = PLAYBACK_TRANSCODE_POLICY[params.kind]; if (!resolvePlaybackMode(params.mimeType, policy)) return { kind: "fallback" }; const maxBytes = maxBytesForKind(params.kind); const source = playbackSourceIdentity(params); const cacheKey = createPlaybackTranscodeCacheKey(source); const target = policy.target; const operationKey = playbackCacheRelativePath(cacheKey, target.extension); const cachedPath = await resolveCachedPlaybackPath({ cacheKey, extension: target.extension, maxBytes }); if (cachedPath) return { kind: "transcoded", path: cachedPath, contentType: target.contentType, extension: target.extension }; const inspection = await inspectPlaybackSource(params); if (inspection.mode === "native") return { kind: "passthrough" }; if (inspection.mode === "fallback") return { kind: "fallback" }; if (playbackJobs.has(operationKey)) return { kind: "preparing" }; const failedAtMs = playbackFailures.get(operationKey); if (failedAtMs !== void 0) { const nowMs = Date.now(); if (failedAtMs <= nowMs && nowMs - failedAtMs < PLAYBACK_TRANSCODE_FAILURE_COOLDOWN_MS) return { kind: "fallback" }; } if (playbackJobs.size >= MAX_PLAYBACK_TRANSCODE_JOBS) return { kind: "preparing" }; const job = transcodePlaybackSource({ ...inspection.audioStreamIndex !== void 0 ? { audioStreamIndex: inspection.audioStreamIndex } : {}, source, mimeType: params.mimeType, kind: params.kind, cacheKey, maxBytes, sourceDurationMs: inspection.durationMs, ...inspection.videoStreamIndex !== void 0 ? { videoStreamIndex: inspection.videoStreamIndex } : {} }); playbackJobs.set(operationKey, job); job.then(() => { playbackJobs.delete(operationKey); playbackFailures.delete(operationKey); }, (reason) => { playbackJobs.delete(operationKey); if (!playbackFailures.has(operationKey)) log.warn(`Playback transcode failed for ${params.sourcePath}: ${formatErrorMessage(reason)}`); playbackFailures.delete(operationKey); playbackFailures.set(operationKey, Date.now()); pruneMapToMaxSize(playbackFailures, MAX_PLAYBACK_ENTRIES.failures); }); return { kind: "preparing" }; } //#endregion //#region src/gateway/assistant-media-content-disposition.ts function buildAssistantMediaContentDisposition(filename, mime) { const sanitizedInput = truncateFilenamePreservingExtension(toWellFormedFilename(filename.replace(/[\r\n]/g, "_")), 200); const fallback = sanitizedInput.replace(/[^\x20-\x7e]|[%"\\]/g, "_").trim() || "download"; const extended = encodeURIComponent(sanitizedInput).replace(/[\x27()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`); const kind = kindFromMime(mime); return `${kind === "image" || kind === "audio" || kind === "video" ? "inline" : "attachment"}; filename="${fallback}"; filename*=UTF-8''${extended}`; } function toWellFormedFilename(value) { let result = ""; for (const char of value) { const code = char.charCodeAt(0); result += char.length === 1 && code >= 55296 && code <= 57343 ? "�" : char; } return result; } function truncateFilenamePreservingExtension(value, maxCodePoints) { const chars = Array.from(value); if (chars.length <= maxCodePoints) return value; const extension = shortFilenameExtension(chars); if (extension.length === 0 || extension.length >= maxCodePoints - 1) return chars.slice(0, maxCodePoints).join(""); return `${chars.slice(0, maxCodePoints - extension.length).join("")}${extension.join("")}`; } function shortFilenameExtension(chars) { const lastDot = chars.lastIndexOf("."); if (lastDot <= 0 || lastDot === chars.length - 1) return []; const extension = chars.slice(lastDot); return extension.length <= 32 ? extension : []; } //#endregion //#region src/gateway/http-byte-range.ts function createImmutableFileValidators(file) { return { etag: `"${createHash("sha256").update(`${file.size}:${file.mtimeMs}`).digest("base64url")}"`, mtimeMs: file.mtimeMs }; } function parseByteRange(value, size) { const normalized = value.trim(); if (normalized.includes(",")) return "invalid"; const match = /^bytes=(\d*)-(\d*)$/i.exec(normalized); if (!match || !match[1] && !match[2]) return "invalid"; const [, rangeStart = "", rangeEnd = ""] = match; const fileSize = BigInt(size); if (!rangeStart) { const suffixLength = BigInt(rangeEnd); if (suffixLength === 0n || fileSize === 0n) return "unsatisfiable"; const start = suffixLength >= fileSize ? 0n : fileSize - suffixLength; return { start: Number(start), end: size - 1 }; } const start = BigInt(rangeStart); if (start >= fileSize) return "unsatisfiable"; const requestedEnd = rangeEnd ? BigInt(rangeEnd) : fileSize - 1n; if (requestedEnd < start) return "unsatisfiable"; const end = requestedEnd >= fileSize ? fileSize - 1n : requestedEnd; return { start: Number(start), end: Number(end) }; } function resolveByteResponse(params) { const etag = params.validators?.etag; const originatedAtMs = params.nowMs ?? Date.now(); const lastModifiedMs = params.validators ? Math.floor(Math.min(params.validators.mtimeMs, originatedAtMs) / 1e3) * 1e3 : void 0; const lastModified = lastModifiedMs === void 0 ? void 0 : new Date(lastModifiedMs).toUTCString(); const headers = params.request?.headers; const ifNoneMatch = headers?.["if-none-match"]; const ifModifiedSinceValues = params.request?.headersDistinct["if-modified-since"]; const ifModifiedSince = ifModifiedSinceValues?.length === 1 ? ifModifiedSinceValues[0] : void 0; if ((params.method === "GET" || params.method === "HEAD") && (matchesHttpIfNoneMatch(ifNoneMatch, etag) || ifNoneMatch === void 0 && lastModifiedMs !== void 0 && typeof ifModifiedSince === "string" && (parseRetryAfterHttpDateMs(ifModifiedSince, originatedAtMs) ?? Number.NEGATIVE_INFINITY) >= lastModifiedMs)) return { kind: "not-modified", statusCode: 304, etag, lastModified }; const full = { kind: "full", statusCode: 200, contentLength: params.file.size, etag, lastModified }; const rangeHeader = headers?.range; if (params.method !== "GET" || typeof rangeHeader !== "string") return full; const ifRangeHeader = headers?.["if-range"]; if (ifRangeHeader !== void 0 && ifRangeHeader !== etag && ifRangeHeader !== lastModified) return full; const range = parseByteRange(rangeHeader, params.file.size); if (range === "invalid") return full; if (range === "unsatisfiable") return { kind: "unsatisfiable", statusCode: 416, contentLength: 0, etag, lastModified, size: params.file.size }; return { kind: "partial", statusCode: 206, contentLength: range.end - range.start + 1, etag, lastModified, range, size: params.file.size }; } function writeByteHeaders(res, plan) { res.statusCode = plan.statusCode; res.setHeader("Accept-Ranges", "bytes"); if (plan.etag !== void 0) res.setHeader("ETag", plan.etag); if (plan.lastModified !== void 0) res.setHeader("Last-Modified", plan.lastModified); if (plan.kind === "not-modified") return; res.setHeader("Content-Length", String(plan.contentLength)); if (plan.kind === "partial") res.setHeader("Content-Range", `bytes ${plan.range.start}-${plan.range.end}/${plan.size}`); else if (plan.kind === "unsatisfiable") res.setHeader("Content-Range", `bytes */${plan.size}`); } function createGatewayByteStream(res, handle, onReadError) { let stream; let closed = false; const close = async () => { if (closed) return; closed = true; if (stream) { stream.destroy(); return; } await handle.close().catch(() => {}); }; const release = () => { close(); }; res.once("close", release); return { close, async pipe(plan, method) { if (method === "HEAD" || !("contentLength" in plan) || plan.contentLength === 0) { await close(); res.end(); return; } if (closed || res.destroyed || res.writableEnded) { await close(); return; } stream = handle.createReadStream({ start: plan.kind === "partial" ? plan.range.start : 0, end: plan.kind === "partial" ? plan.range.end : plan.contentLength - 1, autoClose: true }); stream.once("end", release).once("close", release); stream.once("error", () => { release(); if (!res.destroyed && !res.writableEnded) { if (res.headersSent) res.destroy(); else onReadError(); } }); stream.pipe(res); } }; } //#endregion export { buildAssistantMediaContentDisposition as a, resolvePlaybackTranscode as c, writeByteHeaders as i, createImmutableFileValidators as n, replacePlaybackFileExtension as o, resolveByteResponse as r, resolvePlaybackModeForSource as s, createGatewayByteStream as t };