import ffmpeg from "ffmpeg-static"; import ffprobe from "ffprobe-static"; import fluent_ffmpeg, { type FfprobeData } from "fluent-ffmpeg"; import createError from "http-errors"; import { type Context } from "elysia"; import { NS_misc } from "./misc"; import { NS_promise } from "./promise"; import { NS_error } from "./error"; import { exec, execSync, spawn } from "node:child_process"; import { IS_GH, NS_runtime } from "@x/types/env.runtime"; import { type T_live, type T_probe_playback } from "@x/ff-dl/types"; import { NS_ff } from "@x/ff-dl/helper"; const APP_ENV = () => NS_runtime.ENV_VAR_DB?.APP_ENV || process.env.APP_ENV; const FFMPEG_V = () => NS_runtime.ENV_VAR_DB?.FFMPEG_V || process.env.FFMPEG_V; const FFPROBE_V = () => NS_runtime.ENV_VAR_DB?.FFPROBE_V || process.env.FFPROBE_V; const CPU_0V1 = () => NS_runtime.ENV_VAR_DB?.CPU_0V1 || process.env.CPU_0V1; const is_prod = process.env.NODE_ENV === "production" || (APP_ENV() && ["prod", "pprod", "pre_prod"].includes(APP_ENV())); const ffmpeg_path = is_prod ? "ffmpeg" : ffmpeg || "ffmpeg"; const ffprobe_path = is_prod ? "ffprobe" : ffprobe.path || "ffprobe"; console.log("ffmpeg_path", ffmpeg_path, "\n"); console.log("ffprobe_path", ffprobe_path, "\n"); fluent_ffmpeg.setFfmpegPath(ffmpeg_path); fluent_ffmpeg.setFfprobePath(ffprobe_path); let hw_encoder: any = await get_hardware_encoder(); hw_encoder = typeof hw_encoder !== "string" || !hw_encoder.trim() ? "libx264" : hw_encoder.trim(); // hw_encoder = "libx264"; console.log("\n"); console.log(`🚀 Using hardware encoder: ${hw_encoder}`, "\n"); const TIMEOUT_MS = 30_000; // probe + transcode timeout const PROBE_TIMEOUT_MS = 30_000; // probe + transcode timeout function get_hardware_encoder() { check_if_fFmpeg_active(); return new Promise((resolve) => { // We wrap ffmpeg path in quotes in case there are spaces in the path const checkCmd = `"${ffmpeg_path}" -encoders | grep -E "nvenc|qsv|vaapi"`; exec(checkCmd, (error, stdout) => { if (error || !stdout) { return resolve(null); } const output = stdout.toLowerCase(); if (output.includes("nvenc")) return resolve("h264_nvenc"); if (output.includes("qsv")) return resolve("h264_qsv"); if (output.includes("vaapi")) return resolve("h264_vaapi"); resolve(null); }); }); } function check_if_fFmpeg_active() { console.log("\n"); try { const version = execSync(`${ffmpeg_path} -version`, { encoding: "utf8" }); console.log("⚙️ ", version.split("\n")[0], "\n"); return true; } catch { console.error("❗FFmpeg not found in PATH", "\n"); return false; } } export const NS_ffmpeg_v2 = { F_probe_t1, F_build_ffmpeg_flags_t1, proc_error_pattern, ffmpeg_path, ffprobe_path, TIMEOUT_MS }; async function F_probe_t1(arg0: { playback: Omit; context?: Context; just_cmd?: boolean; timeout?: number; read_intervals?: { duration: number; }; }) { const { context, playback, just_cmd, timeout, read_intervals } = arg0; if (!NS_misc.isValidUrlFormat(playback.url)) { if (playback.protocol !== "local") { throw createError(400, "invalid playback URL"); } } const controller = new AbortController(); function kill_controller() { controller.abort(); console.log("[CONTROLLER] Request aborted"); } const _PROBE_TIMEOUT_MS = typeof timeout === "number" ? timeout : PROBE_TIMEOUT_MS; const t = _PROBE_TIMEOUT_MS <= 0 ? undefined : setTimeout(() => kill_controller(), _PROBE_TIMEOUT_MS); const abort_signal = context?.request.signal; if (abort_signal) { if (abort_signal.aborted) { kill_controller(); } else { abort_signal.addEventListener( "abort", () => { kill_controller(); }, { once: true } ); } } const flags = []; flags.push("-v", "debug"); flags.push("-protocol_whitelist", "file,http,https,tcp,tls,crypto"); flags.push("-probesize", `${1_000_000 * 5}`); // Limit probe size (default is 5MB) flags.push("-analyzeduration", `${1_000_000 * 5}`); // Analyze only seconds (default is 5s) // flags.push("-nobuffer"); // flags.push("-count_packets"); if (read_intervals?.duration) { const windowSeconds = 30; const start = Math.max(0, read_intervals.duration - windowSeconds); flags.push("-read_intervals", `${start}%+${windowSeconds}`); flags.push("-select_streams", "v:0"); flags.push("-show_packets"); } flags.push("-show_format"); flags.push("-show_streams"); flags.push("-print_format", "json"); (() => { const playback_headers = playback.headers; if ( typeof playback_headers !== "object" || Array.isArray(playback_headers) ) { return; } const headers_str = Object.entries(playback_headers) .map(([k, v]) => `${k}: ${v}`) .join("\r\n"); flags.push("-headers", headers_str + "\r\n"); })(); flags.push("-i", playback.url); if (APP_ENV() === "dev") { console.log("[PROBE FLAGS]", flags, "\n"); } if (just_cmd) { clearTimeout(t); return { cmd: flags.join(" ") }; } const timeoutPromise = () => new Promise((_, reject) => { setTimeout(() => { clearTimeout(t); reject(new Error("PROBE is taking way to long")); }, _PROBE_TIMEOUT_MS); }); const _p = probe_runner({ ffprobe_path, args: flags, controller: t ? controller : undefined }); const [E_, R_] = await NS_promise.F_catch_promise({ promise: !t ? _p : Promise.race([_p, timeoutPromise()]) }); clearTimeout(t); if (E_) { // timeout / hard failure -> this IS your "slow" bucket per your own rule throw E_; } const { data: probe_info, bytesRead, durationMs } = R_ as any; const throughputMbps = (bytesRead * 8) / (durationMs / 1000) / 1_000_000; // PACKET RATIO (() => { try { const format_name = probe_info.format.format_name; if (format_name && format_name.includes("hls")) return; const s = probe_info.streams[0]; const declared = parseFloat(probe_info.format.duration); const [n, d] = s.avg_frame_rate.split("/").map(Number); const fps = d ? n / d : 0; const realPackets = parseInt(s.nb_read_packets, 10); const realDuration = fps ? realPackets / fps : 0; const ratio = realDuration / declared; const bitrate = probe_info.format.bit_rate || (probe_info.format.size * 8) / declared; const suspicious = bitrate < 50_000; probe_info.bitrate = bitrate; probe_info.suspicious_bitrate = suspicious; } catch {} })(); // CHECK END PACKETS (() => { const declaredDuration = parseFloat(probe_info.format.duration); const timestamps = (probe_info.packets ?? []) .map((packet: any) => Number(packet.pts_time)) .filter(Number.isFinite); if (timestamps.length === 0) { return; } const latestTimestamp = Math.max(...timestamps); const tolerance = Math.max(5, declaredDuration * 0.01); const _ = latestTimestamp >= declaredDuration - tolerance; probe_info.suspicious_end_packets = _; })(); // SPEED TEST (() => { const bit_rate = probe_info?.format?.bit_rate; const streams = probe_info?.streams; let safetyFactor = 1.5; let sourceBitrateMbps = bit_rate ? Number(bit_rate) / 1_000_000 : undefined; // HLS variant playlists often don't set format.bit_rate — fall back to summing streams if (!sourceBitrateMbps) { const streamSum = (streams ?? []).reduce( (s: number, st: { bit_rate: any }) => s + (st.bit_rate ? Number(st.bit_rate) : 0), 0 ); sourceBitrateMbps = streamSum > 0 ? streamSum / 1_000_000 : undefined; } const _ = { tier: "unknown", throughputMbps }; if (!sourceBitrateMbps) { _.tier = "unknown"; return; } const requiredMbps = sourceBitrateMbps * safetyFactor; const tier = throughputMbps >= requiredMbps * 1.5 ? "fast" : throughputMbps >= requiredMbps ? "medium" : "slow"; _.tier = tier; probe_info.speedtest = _; })(); return probe_info; } function probe_runner(arg0: { ffprobe_path: string; args: string[]; controller?: AbortController; }) { const { ffprobe_path, args, controller } = arg0; return new Promise((resolve, reject) => { const start = performance.now(); const probe = spawn(ffprobe_path, args, { signal: controller?.signal }); let dataOutput = ""; let errorOutput = ""; probe.stdout.on("data", (data) => { dataOutput += data; }); probe.stderr.on("data", (data) => { errorOutput += data; }); probe.on("close", (code) => { if (code !== 0) { reject( new Error( JSON.stringify({ code, message: `ffprobe exited with code ${code}: ${errorOutput}` }) ) ); return; } const durationMs = performance.now() - start; const bytesRead = extract_bytes_read(errorOutput); try { resolve({ data: JSON.parse(dataOutput), bytesRead, durationMs }); } catch (err: any) { reject( new Error(`PROBE Failed to parse ffprobe output: ${err.message}`) ); } }); probe.on("error", (err) => { // This will be called with err being an AbortError if the controller aborts const { message } = NS_error.F_collect_error_message({ ERR: err }); reject(new Error(`PROBE FAILED: ${message}`)); }); }); function extract_bytes_read(stderr: string): number { // robust to ffmpeg version differences in exact line formatting const matches = [...stderr.matchAll(/(\d+)\s*bytes read/gi)]; return matches.reduce((sum, m) => sum + Number(m[1]), 0); } } async function F_build_ffmpeg_flags_t1(arg0: { playback: Omit; subtitles?: T_live["subtitles"]; output_destination: NonNullable; delivery_mode: T_live["delivery_mode"]; delivery_format: T_live["delivery_format"]; probe_metadata: Record; flash_ping?: boolean; dl_to_s3?: boolean; input_type?: string; phase_input?: boolean; }) { const { playback, subtitles, output_destination, delivery_format, delivery_mode, probe_metadata, dl_to_s3, input_type } = arg0; let { flash_ping, phase_input } = arg0; (() => { if (typeof phase_input === "boolean") return; if ( playback.type === "video" && delivery_mode === "download" && input_type === "ts" ) { phase_input = true; } })(); function force_ts() { let _ = false; if (input_type === "ts") _ = true; if (playback.type === "hls") _ = false; return _; } function force_mp4() { let _ = false; if (input_type === "mp4") _ = true; if (playback.type === "hls") _ = false; return _; } const flags: string[] = ["-hide_banner", "-loglevel", "info", "-stats"]; // ------------------------------- // ADD THREAD LIMIT (Crucial for 0.25 vCPU) // ------------------------------- flags.push("-threads", "0"); if (force_ts()) { flags.push("-f", "mpegts"); } else if (force_mp4()) { flags.push("-f", "mp4"); } flags.push("-reconnect", "1"); flags.push("-reconnect_streamed", "1"); if (playback.type === "hls" && delivery_mode === "stream") { flags.push("-reconnect_at_eof", "1"); } flags.push("-reconnect_delay_max", "30"); flags.push("-rw_timeout", `${1_000_000 * 30}`); flags.push("-reconnect_on_network_error", "1"); // flags.push("-reconnect_on_http_error", "429,500,502,503,504"); flags.push("-reconnect_on_http_error", "500,502,503,504"); flags.push("-fflags", "+discardcorrupt+genpts"); // flags.push("-fflags", "+discardcorrupt+genpts+igndts"); // Don't hang on truncated segments // flags.push("-live_start_index", "-2"); // For live streams: start near the end if (playback.type === "hls") { if (flash_ping === false) { flags.push("-http_persistent", "0"); flags.push("-http_multiple", "0"); flags.push("-http_seekable", "0"); } else { flags.push("-http_persistent", "1"); flags.push("-http_multiple", "1"); flags.push("-http_seekable", "1"); flash_ping = true; } } flags.push("-protocol_whitelist", "file,http,https,tcp,tls,crypto"); if (playback.type === "hls") flags.push("-allowed_extensions", "ALL"); flags.push("-analyzeduration", "100M"); flags.push("-probesize", "100M"); (() => { const playback_headers = playback.headers; if ( typeof playback_headers !== "object" || Array.isArray(playback_headers) ) { return; } const headers_str = Object.entries(playback_headers) .map(([k, v]) => `${k}: ${v}`) .join("\r\n"); flags.push("-headers", headers_str + "\r\n"); })(); flags.push("-thread_queue_size", "1024"); flags.push("-i", playback.url); const clean_metadata = NS_ff.F_scan_raw_probe_metadata({ metadata: probe_metadata }); let subs: any[] | undefined = undefined; (() => { (() => { if (delivery_format === "ts") { subs = undefined; } if (clean_metadata?.subStreams && clean_metadata.subStreams.length) { subs = clean_metadata.subStreams; return; } if (!subtitles || !subtitles.length) return; subs = subtitles; for (let i = 0; i < subtitles.length; i++) { const sub = subtitles[i]; if (sub?.url) { flags.push("-f", "webvtt"); flags.push("-i", sub.url); } } })(); if (subs?.length) { const limit = 5; subs = subs.slice(0, limit); } })(); const videoCodec = clean_metadata?.videoStreams[0]?.codec_name; const audioCodec = clean_metadata?.audioStreams[0]?.codec_name; const supported_codecs = (() => { switch (delivery_format) { case "mp4": { return { video: ["h264", "hevc", "vp9", "av1", "mpeg2video"], audio: ["aac", "mp3", "opus", "ac3"], subtitle: ["mov_text"] }; } break; case "ts": { return { video: ["h264", "hevc", "mpeg2video"], audio: ["aac", "mp3", "ac3"], subtitle: [] }; } break; case "mkv": { return { video: [ "h264", "hevc", "vp9", "av1", "mpeg2video", "vp8", "theora" ], audio: [ "aac", "mp3", "opus", "ac3", "flac", "vorbis", "dts", "truehd" ], subtitle: ["srt", "ass", "ssa", "vobsub", "pgs", "webvtt"] }; } break; } })(); let must_re_encode = !supported_codecs.video.includes(videoCodec) || (audioCodec && !supported_codecs.audio.includes(audioCodec)); // const re_encode_va = delivery_format !== "ts"; const re_encode_va = false; let re_encode_a = false; let re_encode_v = false; // const re_encode_va = IS_RENDER_COM() && dl_to_s3; // const re_encode_va = !IS_RENDER_COM(); if (phase_input) must_re_encode = false; if (phase_input) re_encode_a = true; flags.push("-map", "0:v"); flags.push("-map", "0:a?"); if (subs) { if (clean_metadata?.subStreams.length) { flags.push("-map", "0:s"); } else { subs.forEach((_, i) => flags.push("-map", String(i + 1))); } } if (must_re_encode || re_encode_va || (re_encode_v && re_encode_a)) { flags.push("-c:v", hw_encoder || "libx264"); flags.push("-c:a", "aac", "-b:a", "256k"); } else if (re_encode_v) { flags.push("-c:v", hw_encoder || "libx264"); flags.push("-c:a", "copy"); } else if (re_encode_a) { flags.push("-c:v", "copy"); flags.push("-c:a", "aac", "-b:a", "256k"); } else { flags.push("-c:v", "copy"); flags.push("-c:a", "copy"); } if ( (playback.type === "hls" && (must_re_encode || re_encode_va)) || delivery_format !== "ts" ) { flags.push("-bsf:a", "aac_adtstoasc"); } if (subs) { // flags.push("-async", "1"); flags.push("-c:s", "mov_text"); subs.forEach((s, i) => { let lang = s.lang || s.tags?.language; if (!lang) { lang = "und"; } lang = lang.slice(0, 3).toLowerCase(); flags.push(`-metadata:s:s:${i}`, `language=${lang}`); }); } if (delivery_mode === "stream") { flags.push("-tune", "zerolatency"); } if (must_re_encode) { const use_ultrafast = delivery_mode === "stream"; if (hw_encoder.includes("h264_qsv")) { flags.push("-preset", use_ultrafast ? "7" : "4"); // QSV uses numbers: 7 is fastest speed (ultrafast), 4 is balanced (medium) } else { flags.push("-crf", "23"); flags.push("-preset", use_ultrafast ? "ultrafast" : "medium"); // ultrafast veryfast faster medium } } if (must_re_encode || re_encode_va || delivery_format !== "ts") { flags.push("-pix_fmt", "yuv420p"); } // flags.push("-max_interleave_delta", "0"); if (delivery_format) { flags.push("-f", delivery_format === "ts" ? "mpegts" : delivery_format); } flags.push("-avoid_negative_ts", "make_zero"); if (delivery_mode === "download") { // flags.push("-max_muxing_queue_size", "9999"); if (dl_to_s3) { if (delivery_format !== "ts") { flags.push( "-movflags", "frag_keyframe+empty_moov+default_base_moof+skip_trailer" ); flags.push("-min_frag_duration", "5000000"); } } else { flags.push("-movflags", "+faststart"); } if (dl_to_s3) { flags.push("-y", "pipe:1"); } else { flags.push("-y", output_destination); } } else { flags.push("-fps_mode", "passthrough"); if (delivery_format !== "ts") { flags.push("-movflags", "frag_keyframe+dash+delay_moov"); // flags.push("-movflags", "frag_keyframe+empty_moov"); // flags.push("-movflags", "frag_keyframe+empty_moov+default_base_moof"); // flags.push("-movflags", "frag_keyframe+empty_moov+default_base_moof+skip_trailer"); } flags.push("-y", "pipe:1"); } return { flash_ping, flags }; } function proc_error_pattern() { const errorPatterns = [ /^(\[error\]|\[fatal\]|\[panic\])/i, /error/i, /failed/i, /failure/i, /invalid/i, /unknown/i, /unsupported/i, /not found/i, /cannot/i, /could not/i, /unable to/i, /permission denied/i, /no such file/i, /broken pipe/i, /protocol not found/i, /codec not found/i, /encoder not found/i, /decoder not found/i, /filter not found/i, /option not found/i, /conversion failed/i, /av_interleaved_write_frame/i, /out of memory/i, /segmentation fault/i, /assertion failed/i, /corrupt/i, /truncated/i, /malformed/i, /incompatible/i, /mismatch/i, /overflow/i, /underflow/i, /timeout/i, /connection refused/i, /connection reset/i, /network is unreachable/i, /no route to host/i, /dns/i, // DNS resolution failures /unrecognized/i ]; return errorPatterns; } interface T_rendition { width: number; height: number; video_bytes: number; audio_bytes: number; bytes: number; } type T_segment_type = "ts" | "fmp4"; interface T_estimate { bytes: number; duration: number; /** Segment container the estimate was calculated for */ segment_type?: T_segment_type; renditions: T_rendition[]; /** Where each number came from, useful for judging how much to trust the estimate */ basis: { segment_type?: "option" | "codec" | "default"; video_bps: "stream" | "packets" | "format" | "none"; audio_bps: "stream" | "format" | "none"; overhead: "packets" | "frame_rate" | "spec_minimum"; }; } interface T_probe_stream { index: number; codec_type?: string; width?: number; height?: number; bit_rate?: string; duration?: string; avg_frame_rate?: string; has_b_frames?: number; disposition?: { attached_pic?: number }; } interface T_probe_packet { stream_index: number; size?: string; pts_time?: string; dts_time?: string; duration_time?: string; } /** * Estimate total HLS output size from an ffprobe JSON result. * Works with whatever is present: stream/format bitrates, and (optionally) a sampled packet window. */ export function F_estimate_hls_size(arg0: { probe_info: any; segment_type?: T_segment_type; /** Output heights if you plan more than one rendition, e.g. [1080, 720]. Omit for a single rendition. */ heights?: number[]; }): T_estimate { /* * Container framing sizes. Defined by the MPEG-TS / fMP4 specs (not tuning values). */ const TS_PACKET = 188; // fixed TS packet size const TS_PAYLOAD = 184; // 188 minus the 4-byte TS header const PES_BASE = 9; // PES header without timestamps const PES_TIMESTAMP = 5; // each PTS / DTS field const FMP4_SAMPLE_ENTRY = 16; // per-sample entry in the fMP4 'trun' box // ffprobe prints numbers as strings and uses 'N/A' / missing keys freely const num = (v: unknown): number => { const n = typeof v === "number" ? v : typeof v === "string" ? parseFloat(v) : NaN; return Number.isFinite(n) ? n : 0; }; function fps_of(rate?: string): number { if (!rate) return 0; const [a, b] = rate.split("/"); return num(a) / (num(b ?? "1") || 1); } // Bytes a packet occupies once wrapped in the segment container function wire_bytes( type: T_segment_type, size: number, has_dts: boolean ): number { if (type === "fmp4") return size + FMP4_SAMPLE_ENTRY; const pes = PES_BASE + PES_TIMESTAMP * (has_dts ? 2 : 1); return Math.ceil((size + pes) / TS_PAYLOAD) * TS_PACKET; } // Lowest possible framing ratio for the container (no per-packet info available) const spec_minimum = (type: T_segment_type) => type === "ts" ? TS_PACKET / TS_PAYLOAD : 1; const { heights } = arg0; const data: any = typeof arg0.probe_info === "string" ? JSON.parse(arg0.probe_info) : arg0.probe_info; const streams = data.streams ?? []; const fmt = data.format ?? {}; const video = streams.find( (s: any) => s.codec_type === "video" && !s.disposition?.attached_pic ); const audio = streams.find((s: any) => s.codec_type === "audio"); /* * Codecs that HLS players generally expect in fragmented MP4 rather than MPEG-TS. * The source's codecs are the only hint the probe gives about the container you'd want. */ const FMP4_CODECS = new Set(["hevc", "av1", "vp9", "opus", "flac"]); function detect_segment_type( video?: T_probe_stream, audio?: T_probe_stream ): { type: T_segment_type; source: "codec" | "default" } { const needs_fmp4 = [video, audio].some( (s: any) => s?.codec_name && FMP4_CODECS.has(s.codec_name) ); return needs_fmp4 ? { type: "fmp4", source: "codec" } : { type: "ts", source: "default" }; } // Explicit option wins; otherwise infer from the codecs in the probe data const detected = detect_segment_type(video, audio); const segment_type: T_segment_type = arg0.segment_type ?? detected.type; const segment_src: T_estimate["basis"]["segment_type"] = arg0.segment_type ? "option" : detected.source; const duration = num(fmt.duration) || num(video?.duration) || num(audio?.duration); if (!duration) throw new Error("Could not determine duration from probe data"); const video_packets = (data.packets ?? []).filter( (p: any) => p.stream_index === video?.index ); const total_bps = num(fmt.bit_rate) || (num(fmt.size) ? (num(fmt.size) * 8) / duration : 0); // VIDEO bitrate: whole-file stream value > sampled packets > what's left of the total let video_bps = 0; let video_src: T_estimate["basis"]["video_bps"] = "none"; if (video) { if (num(video.bit_rate)) { video_bps = num(video.bit_rate); video_src = "stream"; } else if (packets_bps(video_packets)) { video_bps = packets_bps(video_packets); video_src = "packets"; } else if (total_bps) { video_bps = Math.max(total_bps - num(audio?.bit_rate), 0); video_src = "format"; } } // AUDIO bitrate: stream value, else the leftover of the total. // (With `-select_streams v:0` the audio stream isn't listed, so the leftover is how we see it.) let audio_bps = 0; let audio_src: T_estimate["basis"]["audio_bps"] = "none"; if (num(audio?.bit_rate)) { audio_bps = num(audio?.bit_rate); audio_src = "stream"; } else if (total_bps) { audio_bps = video ? Math.max(total_bps - video_bps, 0) : total_bps; audio_src = audio_bps ? "format" : "none"; } if (!video_bps && !audio_bps) throw new Error("No bitrate information in probe data"); const { ratio: video_ratio, source: overhead_src } = video ? video_overhead(segment_type, video, video_packets, video_bps) : { ratio: spec_minimum(segment_type), source: "spec_minimum" as const }; // ffmpeg packs many audio frames into each TS PES packet, so audio only pays the base TS framing const audio_ratio = spec_minimum(segment_type); const src_width = video?.width ?? 0; const src_height = video?.height ?? 0; const targets = heights?.length && src_height ? heights : [src_height]; const renditions: T_rendition[] = targets.map((h) => { // Same bits-per-pixel as the source: video size scales with pixel count const scale = src_height ? h / src_height : 1; const video_bytes = Math.round( (video_bps / 8) * duration * video_ratio * scale * scale ); const audio_bytes = Math.round((audio_bps / 8) * duration * audio_ratio); return { width: Math.round(src_width * scale), height: h, video_bytes, audio_bytes, bytes: video_bytes + audio_bytes }; }); return { bytes: renditions.reduce((sum, r) => sum + r.bytes, 0), duration, segment_type, renditions, basis: { segment_type: segment_src, video_bps: video_src, audio_bps: audio_src, overhead: overhead_src } }; // Bitrate measured from the sampled packet window (bits per second) function packets_bps(packets: T_probe_packet[]): number { if (packets.length < 2) return 0; const bytes = packets.reduce((sum, p) => sum + num(p.size), 0); let span = packets.reduce((sum, p) => sum + num(p.duration_time), 0); if (!span) { const pts = packets.map((p) => num(p.pts_time)); span = ((Math.max(...pts) - Math.min(...pts)) * packets.length) / (packets.length - 1); } return span > 0 ? (bytes * 8) / span : 0; } // Framing ratio (container bytes / payload bytes) for the video stream function video_overhead( type: T_segment_type, video: T_probe_stream, packets: T_probe_packet[], video_bps: number ): { ratio: number; source: T_estimate["basis"]["overhead"] } { // Best: use the real packet sizes in the sampled window const sized = packets.filter((p) => num(p.size) > 0); if (sized.length) { let payload = 0; let wire = 0; for (const p of sized) { const pts = num(p.pts_time); const dts = num(p.dts_time); payload += num(p.size); wire += wire_bytes(type, num(p.size), Math.abs(pts - dts) > 1e-6); } return { ratio: wire / payload, source: "packets" }; } // Next: average frame size from bitrate and frame rate const fps = fps_of(video.avg_frame_rate); if (fps && video_bps) { const frame = video_bps / 8 / fps; if (type === "fmp4") return { ratio: (frame + FMP4_SAMPLE_ENTRY) / frame, source: "frame_rate" }; const pes = PES_BASE + PES_TIMESTAMP * (video.has_b_frames ? 2 : 1); const wire = ((frame + pes + TS_PAYLOAD / 2) / TS_PAYLOAD) * TS_PACKET; // half a payload of padding on average return { ratio: wire / frame, source: "frame_rate" }; } return { ratio: spec_minimum(type), source: "spec_minimum" }; } }