import ffmpeg from "ffmpeg-static"; import ffprobe from "ffprobe-static"; import fluent_ffmpeg, { type FfprobeData } from "fluent-ffmpeg"; import createError from "http-errors"; import { Context } from "elysia"; import { T_Probe } from "./types"; import { NS_misc } from "./misc"; import { NS_promise } from "./promise"; import { NS_error } from "./error"; import UserAgent from "user-agents"; import { exec, execSync } from "node:child_process"; const APP_ENV = process.env.APP_ENV; const FFMPEG_V = process.env.FFMPEG_V; const FFPROBE_V = process.env.FFPROBE_V; const CPU_0V1 = process.env.CPU_0V1; const is_prod = process.env.NODE_ENV === "production" || APP_ENV === "prod" || APP_ENV === "pre_prod"; const ffmpeg_path = is_prod ? "ffmpeg" : ffmpeg || "ffmpeg"; const ffprobe_path = is_prod ? "ffprobe" : ffprobe.path || "ffprobe"; // const ffmpeg_path = ffmpeg || ""; // const ffprobe_path = ffprobe.path; // const ffmpeg_path = "/usr/local/bin/ffmpeg"; // const ffprobe_path = "/usr/local/bin/ffprobe"; 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(`🚀 Using hardware encoder: ${hw_encoder}`); const TIMEOUT_MS = 20_000; // probe + transcode timeout const PROBE_TIMEOUT_MS = 30_000; // probe + transcode timeout function getFfmpegEncoderConfig() { try { // Quick 0.5-second test to see if Intel QSV hardware initializes successfully execSync(`${ffmpeg_path} -y -f lavfi -i testsrc=duration=0.5 -c:v h264_qsv -f null -`, { stdio: 'ignore', timeout: 2000 // 2 second safety timeout }); // QSV is active and working return { videoCodec: 'h264_qsv', preset: '7', // QSV fastest speed numeric preset extraFlags: [] }; } catch (error) { console.error({error}); // Fallback to CPU software encoding if hardware check fails return { videoCodec: 'libx264', preset: 'ultrafast', // Software fastest speed preset extraFlags: ['-crf', '23'] // CRF is ideal for libx264 }; } } function get_hardware_encoder() { 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); }); }); } async function F_probe_v1(arg0: { playback_headers?: Record | null; playback_url: URL; context?: Context; }) { const { playback_headers, playback_url, context } = arg0; const [E_, R_] = await NS_promise.F_catch_promise({ promise: NS_misc.F_test_connection({ url: playback_url, headers: playback_headers }) }); if (E_) { const { status, message } = NS_error.F_collect_error_status_n_message({ ERR: E_ }); if (["522", "510", "502", "503", "404", "410"].includes(`${status}`)) throw E_; } const command = fluent_ffmpeg(); // Add network options command.addOption("-timeout", (30 * 1_000_000).toString()); // 30 seconds command.addOption("-reconnect", "1"); // Auto-reconnect command.addOption("-reconnect_streamed", "1"); command.addOption("-reconnect_delay_max", "5"); command.addOption("-multiple_requests", "1"); // For HLS/DASH // command.addOption("-user_agent", "Mozilla/5.0"); if (is_prod && FFPROBE_V === "latest") { command.addOption("-allowed_segment_extensions", "ALL"); // Explicitly allow 'none' and 'jpg' in addition to ALL "ts,m4s,jpg,jpeg,none,ALL" command.addOption("-allowed_extensions", "ALL"); command.addOption("-hls_flags", "+nocheck_content_type"); // This tells the HLS demuxer to stop validating extensions against content command.addOption("-extension_picky", "0"); command.addOption("-protocol_whitelist", "file,http,https,tcp,tls,crypto"); command.addOption("-strict", "experimental"); } command.input(playback_url.href); if (playback_headers && Object.keys(playback_headers).length > 0) { const header_string = Object.entries(playback_headers) .map(([k, v]) => `${k}: ${v}`) .join("\r\n"); command.addOption("-headers", header_string); } command.addOption("-v", "error"); command.addOption("-probesize", "5000000"); command.addOption("-analyzeduration", "5000000"); command.addOption( "-show_entries", "format=duration,size,format_name:stream=index,codec_type,codec_name,height,width,bit_rate" ); command.addOption("-show_entries", "stream_tags=language"); command.addOption("-of", "json"); const metadata = await new Promise((resolve, reject) => { command.ffprobe((err, data) => { if (err) { console.error("[PROBE ERROR]:", err.message); // Better error message if (err.message.includes("resolve hostname")) { return reject( createError( 502, `Cannot resolve hostname: ${playback_url.hostname}. Check network/DNS.` ) ); } if (err.message.includes("I/O error")) { return reject( createError( 502, `Network I/O error: Cannot reach ${playback_url.href}` ) ); } return reject(createError(500, `Probe failed: ${err.message}`)); } resolve(data); }); }); const streams = metadata.streams; let hasSubs = false; const audioStreams: any[] = []; const videoStreams: any[] = []; const subStreams: any[] = []; // Single pass through streams for (const stream of streams) { if (stream.codec_type === "video") { // if (!videoStream) videoStream = stream; videoStreams.push(stream); } else if (stream.codec_type === "audio") { // if (!audioStream) audioStream = stream; audioStreams.push(stream); } else if (stream.codec_type === "subtitle") { hasSubs = true; subStreams.push(stream); } } const videoStream = streams.find((s) => s.codec_type === "video"); const audioStream = streams.find((s) => s.codec_type === "audio"); // --- Track Selection Logic --- // Find English Audio or default to first audio (index 0 of audio type) const englishAudio = audioStreams.find((s) => s.tags?.language === "eng"); const audioIndex = englishAudio?.index ?? audioStreams[0]?.index ?? 1; // Find English Subtitles or default to first subtitles const englishSub = subStreams.find((s) => s.tags?.language === "eng"); const subIndex = englishSub?.index ?? subStreams[0]?.index ?? null; // Find 720p video or highest resolution const target720 = videoStreams.find((s) => s.height === 720); const bestVideo = target720 || videoStreams.reduce( (best, current) => (current.height || 0) > (best.height || 0) ? current : best, videoStreams[0] ); const bestVideoIndex = bestVideo?.index; const probe_data: T_Probe = { videoCodec: videoStream?.codec_name || "none", audioCodec: audioStream?.codec_name || "none", hasSubs, duration: metadata.format.duration, size: metadata.format.size, format_name: metadata.format.format_name, audioIndex, subIndex, bestVideoIndex }; return { probe_data, metadata }; } async function F_probe_v2(arg0: { playback_headers?: Record | null; playback_url: URL; playback_type?: string; context?: Context; _log?: boolean; file_mode?: { path: string; }; }) { const { playback_url, context, file_mode } = arg0; let { playback_headers, _log } = arg0; if (typeof _log !== "boolean") { _log = true; } if (!playback_headers) { playback_headers = {}; } if (!playback_url && !file_mode?.path) { throw Error("Invalid playback endpoint"); } if (playback_url && !file_mode?.path) { const [E_, R_] = await NS_promise.F_catch_promise({ promise: NS_misc.F_test_connection({ url: playback_url, headers: playback_headers }) }); if (E_) { const { status, message } = NS_error.F_collect_error_status_n_message({ ERR: E_ }); if ( ["522", "510", "502", "503", "404", "410", "429"].includes(`${status}`) ) throw E_; } } const controller = new AbortController(); function kill_controller() { controller.abort(); console.log("[CONTROLLER] Request aborted"); } const t = setTimeout(() => kill_controller(), PROBE_TIMEOUT_MS); const abort_signal = context?.request.signal; /* forward browser abort -> our controller */ if (abort_signal) { if (abort_signal.aborted) { kill_controller(); } else { abort_signal.addEventListener( "abort", () => { kill_controller(); }, { once: true } ); } } console.log("\n", "PING PROBE NOW", "\n"); const command = [ffprobe_path, "-v", "error"]; command.push("-probesize", "5000000"); // Limit probe size to 5MB (default is 5MB, but explicit) command.push("-analyzeduration", "5000000"); // Analyze only 5 seconds (default is 5s) command.push( "-show_entries", "format=duration,size,format_name:stream=index,codec_type,codec_name,height,width,bit_rate" ); // Show format and stream information command.push("-show_entries", "stream_tags=language"); // Get language tags command.push("-of", "json"); // Output format as JSON if (is_prod && FFPROBE_V === "latest") { command.push("-allowed_segment_extensions", "ALL"); // Explicitly allow 'none' and 'jpg' in addition to ALL "ts,m4s,jpg,jpeg,none,ALL" command.push("-allowed_extensions", "ALL"); command.push("-hls_flags", "+nocheck_content_type"); // This tells the HLS demuxer to stop validating extensions against content command.push("-extension_picky", "0"); command.push("-protocol_whitelist", "file,http,https,tcp,tls,crypto"); command.push("-strict", "experimental"); } // Add headers if needed function add_headers_if_needed() { // Helper to find a header key ignoring case const ua = NS_misc.F_get_obj_kv({ name: "User-Agent", obj: playback_headers }); if (!ua) { try { // Generate a realistic desktop Chrome User-Agent const userAgent = UserAgent.random({ deviceCategory: "desktop" }) || new UserAgent(); if (!playback_headers) { playback_headers = {}; } playback_headers["User-Agent"] = userAgent.toString(); } catch (err: any) {} } if (playback_headers && Object.keys(playback_headers).length > 0) { const ua = NS_misc.F_get_obj_kv({ name: "User-Agent", obj: playback_headers }); const ref = NS_misc.F_get_obj_kv({ name: "Referer", obj: playback_headers }); // Separate specific flags for better reliability (Pass Headers to Sub-demuxers) // if (ua) command.push("-user_agent", ua); // if (ref) command.push("-referer", ref); // Filter out UA/Referer and join the rest for the -headers flag const other_headers = Object.entries(playback_headers) .filter(([k]) => !["user-agentx", "refererx"].includes(k.toLowerCase())) .map(([k, v]) => `${k}: ${v}`) // .map(([k, v]) => `${k}: ${v}\r\n`) .join("\r\n"); if (other_headers) { command.push("-headers", other_headers + "\r\n"); } /* const header_string = Object.entries(playback_headers) .map(([k, v]) => `${k}: ${v}`) .join("\r\n"); command.push("-headers", header_string); */ } } if (!file_mode) add_headers_if_needed(); // add the input flag and URL command.push("-i", file_mode?.path || playback_url.href); if (_log) console.log("[PROBE CMD]", command, "\n"); const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe", signal: controller.signal }); // 🚀 Use Promise.race to timeout faster const probePromise = (async () => { const [txtResponse, stderrResponse, exitCode] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited ]); return { txt: txtResponse, stderr: stderrResponse, exitCode }; })(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error("Probe timeout")), TIMEOUT_MS); }); let result; try { result = await Promise.race([probePromise, timeoutPromise]); } catch (err) { clearTimeout(t); kill_controller(); throw err; } clearTimeout(t); const { txt, stderr, exitCode } = result; if (exitCode !== 0) { clearTimeout(t); throw new Error( JSON.stringify({ message: `Probe failed with code [${exitCode}]: ${stderr}`, code: exitCode, stderr: stderr }) ); } const metadata: FfprobeData = JSON.parse(txt); const streams = metadata.streams; let hasSubs = false; const audioStreams: any[] = []; const videoStreams: any[] = []; const subStreams: any[] = []; // Single pass through streams for (const stream of streams) { if (stream.codec_type === "video") { // if (!videoStream) videoStream = stream; videoStreams.push(stream); } else if (stream.codec_type === "audio") { // if (!audioStream) audioStream = stream; audioStreams.push(stream); } else if (stream.codec_type === "subtitle") { hasSubs = true; subStreams.push(stream); } } const videoStream = streams.find((s) => s.codec_type === "video"); const audioStream = streams.find((s) => s.codec_type === "audio"); // --- Track Selection Logic --- // Find English Audio or default to first audio (index 0 of audio type) const englishAudio = audioStreams.find((s) => s.tags?.language === "eng"); const audioIndex = englishAudio?.index ?? audioStreams[0]?.index ?? 1; // Find English Subtitles or default to first subtitles const englishSub = subStreams.find((s) => s.tags?.language === "eng"); const subIndex = englishSub?.index ?? subStreams[0]?.index ?? null; // Find 720p video or highest resolution const target720 = videoStreams.find( (s) => s.height === 720 || s.width === 1280 ); const bestVideo = target720 || videoStreams.reduce( (best, current) => (current.height || 0) > (best.height || 0) ? current : best, videoStreams[0] ); const bestVideoIndex = bestVideo?.index; const bestVideoBox = bestVideo ? { height: bestVideo.height, width: bestVideo.width } : undefined; const probe_data: T_Probe = { videoCodec: videoStream?.codec_name || "none", audioCodec: audioStream?.codec_name || "none", hasSubs, duration: metadata.format.duration, size: metadata.format.size, format_name: metadata.format.format_name, audioIndex, subIndex, bestVideoIndex, bestVideoBox }; return { probe_data, metadata }; } async function F_build_ffmpeg_command_v1(arg0: { playback_headers?: Record; playback_url: URL; context?: Context; is_net_src?: boolean; has_tcp_progress?: boolean; subtitles?: Record[]; probe_data: T_Probe; output_format: string; live: string; }) { const { playback_url, context, is_net_src, has_tcp_progress, subtitles, probe_data, output_format, live } = arg0; let { playback_headers } = arg0; // ------------------------------- // Logging options // ------------------------------- const command: string[] = ["-hide_banner", "-loglevel", "info", "-stats"]; // ------------------------------- // Network reconnect options (network resilience) // ------------------------------- if (is_net_src) { command.push("-reconnect", "1"); command.push("-reconnect_streamed", "1"); command.push("-reconnect_delay_max", "5"); if (has_tcp_progress) { command.push("-timeout", "0"); command.push("-rw_timeout", "0"); command.push("-multiple_requests", "1"); } else { command.push("-rw_timeout", (120 * 1_000_000).toString()); command.push("-timeout", (120 * 1_000_000).toString()); } } if (is_prod && FFMPEG_V === "latest") { command.push("-allowed_segment_extensions", "ALL"); command.push("-allowed_extensions", "ALL"); command.push("-extension_picky", "0"); command.push("-protocol_whitelist", "file,http,https,tcp,tls,crypto"); command.push("-strict", "experimental"); } // ------------------------------- // Playback headers // ------------------------------- function add_headers_if_needed() { const ua = NS_misc.F_get_obj_kv({ name: "User-Agent", obj: playback_headers }); if (!ua) { try { // Generate a realistic desktop Chrome User-Agent const userAgent = UserAgent.random({ deviceCategory: "desktop" }) || new UserAgent(); if (!playback_headers) { playback_headers = {}; } playback_headers["User-Agent"] = userAgent.toString(); } catch (err: any) {} } if (playback_headers && Object.keys(playback_headers).length > 0) { const ua = NS_misc.F_get_obj_kv({ name: "User-Agent", obj: playback_headers }); const ref = NS_misc.F_get_obj_kv({ name: "Referer", obj: playback_headers }); // if (ua) command.push("-user_agent", ua); // if (ref) command.push("-referer", ref); const other_headers = Object.entries(playback_headers) .filter(([k]) => !["user-agentx", "refererx"].includes(k.toLowerCase())) .map(([k, v]) => `${k}: ${v}`) // .map(([k, v]) => `${k}: ${v}\r\n`).join("") .join("\r\n"); if (other_headers) { command.push("-headers", other_headers + "\r\n"); } /* const header_string = Object.entries(playback_headers) .map(([k, v]) => `${k}: ${v}`) .join("\r\n"); command.push("-headers", header_string); */ } } add_headers_if_needed(); command.push("-http_persistent", "0"); /// ------------------------------- // Input // ------------------------------- command.push("-i", playback_url.href); // ------------------------------- // Subtitle Logic // ------------------------------- let has_native_subs = false; await handle_subtitles(); async function handle_subtitles() { if (typeof probe_data.subIndex === "number") { // SOFT/NONE: Use 'copy' if possible to save CPU // Note: Browsers usually ignore these in a fragmented MP4 stream command.push("-map", `0:${probe_data.subIndex}`); command.push( "-c:s", output_format === "ts" ? "dvb_subtitle" : "mov_text" ); // "mov_text" MP4 compatible soft-sub format has_native_subs = true; } if (!(!has_native_subs && subtitles && subtitles.length)) return; let default_sub_index = subtitles.findIndex((xx) => xx.default); default_sub_index = default_sub_index < 0 ? 0 : default_sub_index; let burnt = false; const limit = 1; for (var i = 0; i < subtitles.length; i++) { if (i > limit) { break; } const subtitle = subtitles[i]; const burn = subtitle.burn; const path = subtitle.file_path; const url = subtitle.url; let lang = subtitle.lang; // iso 639_2 prefeered const title = subtitle.title || subtitle.name; const is_default = default_sub_index === i; const subtitle_headers = subtitle.subtitle_headers; const n_ = i + 1; if (typeof lang === "string") { lang = lang.trim().toLowerCase(); if (lang.length < 2) { lang = undefined; } if (lang.length > 3) { lang = lang.slice(0, 3); } } else { lang = undefined; } if (burn && path && (!burnt || is_default)) { // burn (local file only) const ext = path.extname(path).toLowerCase(); if ([".vtt", ".srt", ".ass", ".ssa"].includes(ext)) { // Always normalize separators first let normalized = path .normalize(path) .replace(/\\/g, "/") // back-slashes → forward .replace(":", "\\:"); // escape the drive colon command.push("-i", path); command.push("-sub_charenc", "UTF-8"); command.push("-vf", `subtitles='${normalized}'`); has_native_subs = true; burnt = true; continue; } } if ( !url || !NS_misc.isValidUrlFormat(url) || typeof lang !== "string" || !(lang.length > 0 && lang.length < 4) ) { continue; } if (subtitle_headers && Object.keys(subtitle_headers).length) { const headers_string = Object.entries(subtitle_headers) .map(([key, value]) => `${key}: ${value}`) .join("\r\n"); command.push("-headers", headers_string); } command.push("-i", url); command.push("-map", `${i}:0`); command.push( `-c:s:${i}`, output_format === "ts" ? "dvb_subtitle" : "mov_text" ); command.push(`-metadata:s:s:${i}`, `language=${lang}`); if (title) { command.push(`-metadata:s:s:${i}`, `title=${title}`); } if (is_default) { command.push(`-disposition:s:${i}`, `default`); } } } // ------------------------------- // Video Audio Logic // ------------------------------- if ( probe_data.videoCodec === "h264" && probe_data.audioCodec === "aac" && !has_native_subs && (output_format === "mp4" || output_format === "ts") && typeof probe_data.audioIndex !== "number" ) { command.push("-c:v", "copy", "-c:a", "copy", "-bsf:a", "aac_adtstoasc"); } else { if (typeof probe_data.bestVideoIndex === "number") { command.push("-map", `0:${probe_data.bestVideoIndex}?`); } else { if (typeof probe_data.audioIndex === "number") { command.push("-map", "0:v:0?"); } } // command.push("-c:v", "libx264"); command.push("-c:v", hw_encoder || "libx264"); } if (typeof probe_data.audioIndex === "number") { command.push("-map", `0:${probe_data.audioIndex}?`); } command.push("-tune", "zerolatency"); command.push("-crf", "23"); command.push("-preset", live === "stream" ? "ultrafast" : "medium"); command.push("-pix_fmt", "yuv420p"); command.push("-c:a", "aac"); command.push("-b:a", "256k"); command.push("-max_interleave_delta", "0"); // ------------------------------- // Output format // ------------------------------- command.push("-f", output_format === "ts" ? "mpegts" : output_format); // ------------------------------- // Flags // ------------------------------- command.push("-avoid_negative_ts", "make_zero"); command.push("-fflags", "+genpts+nobuffer"); // command.push("-fflags", "+genpts+igndts"); // Generate PTS and ignore invalid DTS if (live === "stream") { // command.push("-use_wallclock_as_timestamps", "1"); // Use wallclock for live streams } command.push("-fps_mode", "passthrough"); // Don't duplicate/drop frames for timestamp sync if (is_net_src) { command.push("-movflags", "frag_keyframe+empty_moov+default_base_moof"); } else { command.push("-movflags", "+faststart"); } return command; } async function F_build_ffmpeg_command_v2(arg0: { playback_headers?: Record; playback_url: URL; context?: Context; is_net_src?: boolean; has_tcp_progress?: boolean; subtitle?: Record; probe_data: T_Probe; output_format: string; live: string; }) { const { playback_headers, playback_url, context, is_net_src, has_tcp_progress, subtitle, probe_data, output_format, live } = arg0; const command = fluent_ffmpeg(); // ------------------------------- // Logging options // ------------------------------- command .addOption("-hide_banner") .addOption("-loglevel", "info") .addOption("-stats"); // ------------------------------- // Network reconnect options (network resilience) // ------------------------------- if (is_net_src) { command.addOption("-reconnect", "1"); command.addOption("-reconnect_streamed", "1"); command.addOption("-reconnect_delay_max", "5"); if (has_tcp_progress) { command.addOption("-timeout", "0"); command.addOption("-rw_timeout", "0"); command.addOption("-multiple_requests", "1"); } else { command.addOption("-rw_timeout", (120 * 1_000_000).toString()); // 1 second=1,000,000 microseconds command.addOption("-timeout", (120 * 1_000_000).toString()); // 1 second=1,000,000 microseconds } } if (is_prod && FFMPEG_V === "latest") { command.addOption("-allowed_segment_extensions", "ALL"); command.addOption("-allowed_extensions", "ALL"); command.addOption("-extension_picky", "0"); command.addOption("-protocol_whitelist", "file,http,https,tcp,tls,crypto"); command.addOption("-strict", "experimental"); } // ------------------------------- // Playback headers // ------------------------------- if (playback_headers && Object.keys(playback_headers).length > 0) { const header_string = Object.entries(playback_headers) .map(([k, v]) => `${k}: ${v}`) .join("\r\n"); command.addOption("-headers", header_string); } command.addOption("-http_persistent", "0"); // ------------------------------- // Input // ------------------------------- command.input(playback_url.href); // ------------------------------- // Subtitle Logic // ------------------------------- let has_native_subs = false; if (typeof probe_data.subIndex === "number") { // SOFT/NONE: Use 'copy' if possible to save CPU // Note: Browsers usually ignore these in a fragmented MP4 stream command.addOption("-map", `0:${probe_data.subIndex}`); command.addOption( "-c:s", output_format === "ts" ? "dvb_subtitle" : "mov_text" ); // "mov_text" MP4 compatible soft-sub format has_native_subs = true; } await (async () => { if (!(!has_native_subs && subtitle)) return; const burn = subtitle.burn; const path = subtitle.path; const url = subtitle.url; const lang = subtitle.lang; const name = subtitle.name; const subtitle_headers = subtitle.subtitle_headers; if (burn && path) { // burn (local file only) const ext = path.extname(path).toLowerCase(); if ([".vtt", ".srt", ".ass", ".ssa"].includes(ext)) { // Always normalize separators first let normalized = path .normalize(path) .replace(/\\/g, "/") // back-slashes → forward .replace(":", "\\:"); // escape the drive colon command.addOption("-sub_charenc", "UTF-8"); command.addOption("-i", path); command.addOption("-vf", `subtitles='${normalized}'`); has_native_subs = true; return; } } if ( !url || !NS_misc.isValidUrlFormat(url) || typeof lang !== "string" || !(lang.trim().length > 0 && lang.trim().length < 4) ) { return; } if (subtitle_headers && Object.keys(subtitle_headers).length) { const headers_string = Object.entries(subtitle_headers) .map(([key, value]) => `${key}: ${value}`) .join("\r\n"); command.addOption("-headers", headers_string); } command.addOption("-metadata:s:s:0", `language=${lang.trim()}`); command.addOption( "-c:s", output_format === "ts" ? "dvb_subtitle" : "mov_text" ); command.addOption("-i", url); })(); // ------------------------------- // Video Audio Logic // ------------------------------- if ( probe_data.videoCodec === "h264" && probe_data.audioCodec === "aac" && !has_native_subs && (output_format === "mp4" || output_format === "ts") && typeof probe_data.audioIndex !== "number" ) { command.addOption( "-c:v", "copy", "-c:a", "copy", "-bsf:a", "aac_adtstoasc" ); } else { if (typeof probe_data.bestVideoIndex === "number") { command.addOption("-map", `0:${probe_data.bestVideoIndex}`); } else { if (typeof probe_data.audioIndex === "number") { command.addOption("-map", "0:v:0"); } } command.addOption("-c:v", "libx264"); } if (typeof probe_data.audioIndex === "number") { command.addOption("-map", `0:${probe_data.audioIndex}`); } command.addOption("-tune", "zerolatency"); // tells the encoder to minimize the delay between capturing a frame and outputting it command.addOption("-crf", "23"); command.addOption("-preset", live === "stream" ? "ultrafast" : "medium"); command.addOption("-pix_fmt", "yuv420p"); command.addOption("-c:a", "aac"); command.addOption("-b:a", "256k"); command.addOption("-max_interleave_delta", "0"); // ------------------------------- // Output format // ------------------------------- command.format(output_format === "ts" ? "mpegts" : output_format); // ------------------------------- // Flags // ------------------------------- command.addOption("-avoid_negative_ts", "make_zero"); command.addOption("-fflags", "+genpts+nobuffer"); // command.addOption("-fflags", "+genpts+igndts"); // Generate PTS and ignore invalid DTS if (live === "stream") { // command.addOption("-use_wallclock_as_timestamps", "1"); // Use wallclock for live streams } command.addOption("-fps_mode", "passthrough"); // Don't duplicate/drop frames for timestamp sync if (is_net_src) { command.addOption( "-movflags", "frag_keyframe+empty_moov+default_base_moof" ); } else { command.addOption("-movflags", "+faststart"); } return command; } async function F_build_ffmpeg_command_v3(arg0: { playback_headers?: Record; playback_url: URL; playback_type?: string; context?: Context; is_net_src?: boolean; has_tcp_progress?: boolean; subtitles?: Record[]; probe_data: T_Probe; output_format: "ts" | "mp4" | "mkv"; live: string; file_mode?: boolean; re_encode_audio?: boolean; }) { const { playback_url, context, is_net_src, has_tcp_progress, subtitles, probe_data, output_format, live, playback_type, file_mode, re_encode_audio } = arg0; let { playback_headers } = arg0; // ------------------------------- // Logging options // ------------------------------- const command: string[] = ["-hide_banner", "-loglevel", "info", "-stats"]; // ------------------------------- // ADD THREAD LIMIT (Crucial for 0.25 vCPU) // ------------------------------- // if (!CPU_0V1 || `${CPU_0V1}` !== "1") command.push("-threads", "1"); // ------------------------------- // Network reconnect options (network resilience) // ------------------------------- const timeoutVal = has_tcp_progress ? "0" : (80 * 1_000_000).toString(); if (is_net_src) { command.push("-reconnect", "1"); command.push("-reconnect_streamed", "1"); command.push("-reconnect_delay_max", "20"); command.push("-reconnect_on_network_error", "1"); command.push("-reconnect_on_http_error", "429,500,502,503,504"); // command.push("-rw_timeout", timeoutVal, "-timeout", timeoutVal); // command.push("-rw_timeout", timeoutVal); command.push("-rw_timeout", timeoutVal, "-timeout", timeoutVal); if (has_tcp_progress) { command.push("-multiple_requests", "1"); } } if (is_prod && FFMPEG_V === "latest") { command.push("-allowed_segment_extensions", "ALL"); command.push("-allowed_extensions", "ALL"); command.push("-extension_picky", "0"); command.push("-protocol_whitelist", "file,http,https,tcp,tls,crypto"); command.push("-strict", "experimental"); // command.push("-demuxer", "hls"); } // ------------------------------- // MAIN VIDEO INPUT (Input 0) // ------------------------------- function add_headers_if_needed() { const ua = NS_misc.F_get_obj_kv({ name: "User-Agent", obj: playback_headers }); if (!ua) { try { const userAgent = UserAgent.random({ deviceCategory: "desktop" }) || new UserAgent(); if (!playback_headers) { playback_headers = {}; } playback_headers["User-Agent"] = userAgent.toString(); } catch (err: any) {} } if (playback_headers && Object.keys(playback_headers).length > 0) { const ua = NS_misc.F_get_obj_kv({ name: "User-Agent", obj: playback_headers }); const ref = NS_misc.F_get_obj_kv({ name: "Referer", obj: playback_headers }); if (ua) command.push("-user_agent", ua); if (ref) command.push("-referer", ref); const other_headers = Object.entries(playback_headers) .filter(([k]) => !["user-agent", "referer"].includes(k.toLowerCase())) .map(([k, v]) => `${k}: ${v}`) .join("\r\n"); if (other_headers) { command.push("-headers", other_headers + "\r\n"); } } } add_headers_if_needed(); // command.push("-http_multiple", "0"); // command.push("-analyzeduration", "10M"); // command.push("-probesize", "10M"); // command.push("-seg_max_retry", "5"); // command.push("-max_reload", "100"); command.push("-fflags", "+discardcorrupt+genpts+nobuffer"); // command.push("-err_detect", "ignore_err"); if ( playback_type === "hls" || (typeof playback_type === "string" && playback_type.includes("hls")) || (typeof probe_data.format_name === "string" && probe_data.format_name.includes("hls")) ) { command.push("-http_persistent", "1"); } command.push("-i", playback_url.href); // ------------------------------- // SUBTITLE INPUTS (Input 1...N) // ------------------------------- const all_subs = Array.isArray(subtitles) ? subtitles : []; const valid_external_subs = (() => { const limit = 4; let soft_subs = all_subs.filter((s) => s.url && !s.burn); return get_filtered_subs({ limit, subs: soft_subs }); })(); const valid_bun_subs = (() => { const limit = 1; let burn_subs = all_subs.filter((s) => s.path && s.burn); return get_filtered_subs({ limit, subs: burn_subs }); })(); function get_filtered_subs(arg0: { limit: number; subs: any[] }) { const { limit, subs: originalSubs } = arg0; let subs = [...originalSubs]; if (!limit || !(subs.length > limit)) return subs; const defaultIdx = subs.findIndex((s) => { const hasValidUrl = typeof s?.url === "string" && NS_misc.isValidUrlFormat(s.url); return hasValidUrl && s.default; }); let defaultSub = null; if (defaultIdx > -1) { [defaultSub] = subs.splice(defaultIdx, 1); } const englishRegex = /\b(en|eng|english)\b/i; const englishSubs = []; const otherSubs = []; for (const s of subs) { const matchesEnglish = [s.lang, s.title, s.name].some( (field) => typeof field === "string" && englishRegex.test(field) ); const hasValidUrl = typeof s?.url === "string" && NS_misc.isValidUrlFormat(s.url); if (!hasValidUrl) continue; if (matchesEnglish) { englishSubs.push(s); } else { otherSubs.push(s); } } const result = []; if (defaultSub) result.push(defaultSub); let combined = [...result, ...englishSubs, ...otherSubs]; if (limit) { combined = combined.slice(0, limit); } return combined; } for (const sub of valid_external_subs) { const subtitle_headers = sub.subtitle_headers; const url = sub.url; if (subtitle_headers && Object.keys(subtitle_headers).length) { const h_str = Object.entries(subtitle_headers) .map(([k, v]) => `${k}: ${v}`) .join("\r\n"); command.push("-headers", h_str); } command.push("-i", url); } // --------------------------------------------------------- // MAPPING SECTION (The "Output" configuration starts here) // --------------------------------------------------------- const is_burning = valid_bun_subs && Array.isArray(valid_bun_subs) && valid_bun_subs.length > 0; const must_re_encode = valid_bun_subs?.length || probe_data.videoCodec !== "h264" || probe_data.audioCodec !== "aac" || typeof probe_data.subIndex === "number" || (output_format !== "mp4" && output_format !== "ts"); /* VIDEO MAPPING */ if (typeof probe_data.bestVideoIndex === "number") { command.push("-map", `0:${probe_data.bestVideoIndex}?`); } else { command.push("-map", "0:v:0?"); } if (must_re_encode) { command.push("-c:v", hw_encoder || "libx264"); if (is_burning && valid_bun_subs[0].path) { const safePath = valid_bun_subs[0].path .replace(/\\/g, "/") .replace(/:/g, "\\:"); command.push("-vf", `subtitles='${safePath}'`); } } else { command.push("-c:v", "copy"); } /* AUDIO MAPPING */ if (typeof probe_data.audioIndex === "number") { command.push("-map", `0:${probe_data.audioIndex}?`); } else { command.push("-map", "0:a:0"); } if (must_re_encode || re_encode_audio) { command.push("-c:a", "aac", "-b:a", "256k"); // command.push("-async", "1"); command.push("-af", "loudnorm=I=-14:TP=-1.5:LRA=11,aresample=async=1"); } else { command.push("-c:a", "copy", "-bsf:a", "aac_adtstoasc"); } /* SOFT SUBTITLE MAPPING */ let out_sub_idx = 0; let has_assigned_default = false; const sub_claims_default = valid_external_subs.some((s) => s.default) || typeof probe_data.subIndex === "number"; // Native Subs (from Input 0) if (typeof probe_data.subIndex === "number") { command.push("-map", `0:${probe_data.subIndex}`); command.push( `-c:s:${out_sub_idx}`, output_format === "ts" ? "dvb_subtitle" : "mov_text" ); command.push(`-disposition:s:${out_sub_idx}`, "default"); has_assigned_default = true; out_sub_idx++; } // External Subs (starting from Input 1) for (let i = 0; i < valid_external_subs.length; i++) { const sub = valid_external_subs[i]; const input_idx = i + 1; // Because video is 0 const title = sub.title || sub.name; const is_default = sub.default; let lang = (sub.lang || "und").trim(); lang = lang.slice(0, 3).toLowerCase(); const should_be_default = !has_assigned_default && (is_default || (!sub_claims_default && i === 0)); command.push("-map", `${input_idx}:0`); command.push( `-c:s:${out_sub_idx}`, output_format === "ts" ? "dvb_subtitle" : "mov_text" ); // Metadata command.push(`-metadata:s:s:${out_sub_idx}`, `language=${lang}`); if (title) { command.push(`-metadata:s:s:${out_sub_idx}`, `title=${title}`); } // Default Toggle if (should_be_default) { command.push(`-disposition:s:${out_sub_idx}`, "default"); has_assigned_default = true; } else { command.push(`-disposition:s:${out_sub_idx}`, "0"); } out_sub_idx++; } const use_ultrafast = live === "stream" || valid_bun_subs.length; /* ENCODING & TUNING */ if (live === "stream") { command.push("-tune", "zerolatency"); } // if (must_re_encode) { if(hw_encoder.includes("h264_qsv")) { command.push("-preset", use_ultrafast ? "7" : "7"); // QSV uses numbers: 7 is fastest speed (ultrafast), 4 is balanced (medium) } else { command.push("-crf", "23"); command.push("-preset", use_ultrafast ? "ultrafast" : "ultrafast"); // ultrafast veryfast faster medium } // } if (must_re_encode) command.push("-pix_fmt", "yuv420p"); command.push("-max_interleave_delta", "0"); /* OUTPUT FORMAT & FLAGS */ if (live === "stream") { command.push("-f", "ismv"); } else { command.push("-f", output_format === "ts" ? "mpegts" : output_format); } command.push("-avoid_negative_ts", "make_zero"); if (live === "stream") { // command.push("-use_wallclock_as_timestamps", "1"); } if (file_mode) { // command.push("-fps_mode", "cfr"); // passthrough cfr command.push("-max_muxing_queue_size", "9999"); } else { command.push("-fps_mode", "passthrough"); } if (is_net_src && !file_mode) { // command.push("-movflags", "frag_keyframe+empty_moov+default_base_moof"); command.push("-movflags", "frag_keyframe+dash+delay_moov"); } else { command.push("-movflags", "+faststart"); } return command; } function F_stderr_data_read(arg0: { output: string }) { const { output } = arg0; let duration = 0; let current_time = 0; let current_size_bytes = 0; let progress = 0; // Extract duration with flexible whitespace const durationMatch = output.match( /Duration:\s*(\d{2}):(\d{2}):(\d{2}\.\d{2})/ ); if (durationMatch) { const [, hours, minutes, seconds] = durationMatch; duration = parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseFloat(seconds); } // Extract current time with flexible whitespace const timeMatch = output.match(/time\s*=\s*(\d{2}):(\d{2}):(\d{2}\.\d{2})/); if (timeMatch) { const [, hours, minutes, seconds] = timeMatch; current_time = parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseFloat(seconds); } // Extract size with flexible whitespace (captures both 'size' and 'Lsize') const sizeMatch = output.match( /[Ll]?size\s*=\s*(\d+(?:\.\d+)?)\s*([kmgtKMGT]?[bB]?)/ ); if (sizeMatch) { const [, sizeValue, unit] = sizeMatch; let sizeInBytes = parseFloat(sizeValue); // Convert to bytes based on unit const unitLower = unit.toLowerCase(); switch (unitLower) { case "kb": sizeInBytes *= 1024; break; case "mb": sizeInBytes *= 1024 * 1024; break; case "gb": sizeInBytes *= 1024 * 1024 * 1024; break; case "tb": sizeInBytes *= 1024 * 1024 * 1024 * 1024; break; // 'b' or no unit means bytes already } current_size_bytes = Math.round(sizeInBytes); } // Calculate and log progress if we have the necessary data if (duration && current_time) { progress = Math.min( parseFloat(((current_time / duration) * 100).toFixed(2)), 100 ); let progress_info = `${progress}%`; if (current_size_bytes !== undefined) { // Format size for display const formatSize = (bytes: number): string => { if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`; }; // progress_info += `, size = ${formatSize(current_size_bytes)}`; } } return { video_duration: duration, current_out_time: current_time, current_size_bytes }; } function F_format_duration(seconds?: number): string { if (typeof seconds !== "number") { return ""; } if (isNaN(seconds) || seconds < 0) return "0:00"; const hrs = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); let data = ""; if (hrs > 0) { data = `${hrs}:${mins.toString().padStart(2, "0")}:${secs .toString() .padStart(2, "0")}`; } else { data = `${mins}:${secs.toString().padStart(2, "0")}`; } return data; } function F_format_size(bytes?: number) { if (typeof bytes !== "number") { return; } if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } export const NS_ffmpeg = { fluent_ffmpeg, ffmpeg_path, ffprobe_path, TIMEOUT_MS, PROBE_TIMEOUT_MS, F_probe_v1, F_probe_v2, F_build_ffmpeg_command_v1, F_build_ffmpeg_command_v2, F_build_ffmpeg_command_v3, F_format_duration, F_format_size, F_stderr_data_read, get_hardware_encoder };