Spaces:
Sleeping
Sleeping
| 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"; | |
| import { NS_runtime } from "../../../../../packages/lib/env.runtime"; | |
| const APP_ENV = () => NS_runtime.ENV_VAR_DB?.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<string, any> | 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<FfprobeData>((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<string, any> | 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<never>((_, 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<string, any>; | |
| playback_url: URL; | |
| context?: Context; | |
| is_net_src?: boolean; | |
| has_tcp_progress?: boolean; | |
| subtitles?: Record<string, any>[]; | |
| 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<string, any>; | |
| playback_url: URL; | |
| context?: Context; | |
| is_net_src?: boolean; | |
| has_tcp_progress?: boolean; | |
| subtitle?: Record<string, any>; | |
| 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<string, any>; | |
| playback_url: URL; | |
| playback_type?: string; | |
| context?: Context; | |
| is_net_src?: boolean; | |
| has_tcp_progress?: boolean; | |
| subtitles?: Record<string, any>[]; | |
| 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 | |
| }; | |