GUI-STUDIO commited on
Commit ·
bb60a2d
1
Parent(s): e32339f
Add sbe_id download flow & file_mode ffmpeg
Browse filesPropagate a generated sbe_id across download flow and enable file-mode ffmpeg processing for server-side downloads. Changes include: generate and pass sbe_id from download portal into ffmpeg routes; add in-memory transcode job stores and endpoints to track job state in both download and showtime routers; implement temporary S3/ffmpeg-downloads storage, file probing via F_probe_v2(file_mode), and serving of completed downloads with range/headers handling; update ffmpeg command builder to support file inputs, header handling fixes, HLS detection improvements and various robustness fixes (error handling, logging, formatting). Also add S3 entries to .gitignore and several small cleanup tweaks in related routers and libs.
- .gitignore +5 -0
- src/lib/ffmpeg.ts +53 -35
- src/sub-router/ffmpeg.ts +22 -2
- src/sub-router/showtime.ts +429 -41
.gitignore
CHANGED
|
@@ -42,3 +42,8 @@ yarn-error.log*
|
|
| 42 |
**/*.log
|
| 43 |
package-lock.json
|
| 44 |
**/*.bun
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
**/*.log
|
| 43 |
package-lock.json
|
| 44 |
**/*.bun
|
| 45 |
+
|
| 46 |
+
S3
|
| 47 |
+
/S3
|
| 48 |
+
s3
|
| 49 |
+
/s3
|
src/lib/ffmpeg.ts
CHANGED
|
@@ -20,8 +20,8 @@ const is_prod =
|
|
| 20 |
APP_ENV === "prod" ||
|
| 21 |
APP_ENV === "pre_prod";
|
| 22 |
|
| 23 |
-
const ffmpeg_path = is_prod ? "ffmpeg" :
|
| 24 |
-
const ffprobe_path = is_prod ? "ffprobe" :
|
| 25 |
|
| 26 |
// const ffmpeg_path = ffmpeg || "";
|
| 27 |
// const ffprobe_path = ffprobe.path;
|
|
@@ -223,24 +223,39 @@ async function F_probe_v2(arg0: {
|
|
| 223 |
playback_url: URL;
|
| 224 |
playback_type?: string;
|
| 225 |
context?: Context;
|
|
|
|
|
|
|
|
|
|
| 226 |
}) {
|
| 227 |
-
const { playback_url, context } = arg0;
|
| 228 |
let { playback_headers } = arg0;
|
| 229 |
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
headers: playback_headers
|
| 234 |
-
})
|
| 235 |
-
});
|
| 236 |
|
| 237 |
-
if (
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
});
|
| 241 |
|
| 242 |
-
if (
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
}
|
| 245 |
|
| 246 |
const controller = new AbortController();
|
|
@@ -349,10 +364,10 @@ async function F_probe_v2(arg0: {
|
|
| 349 |
}
|
| 350 |
}
|
| 351 |
|
| 352 |
-
add_headers_if_needed();
|
| 353 |
|
| 354 |
// add the input flag and URL
|
| 355 |
-
command.push("-i", playback_url.href);
|
| 356 |
|
| 357 |
console.log("[PROBE CMD]", command, "\n");
|
| 358 |
|
|
@@ -1000,6 +1015,7 @@ async function F_build_ffmpeg_command_v3(arg0: {
|
|
| 1000 |
probe_data: T_Probe;
|
| 1001 |
output_format: "ts" | "mp4" | "mkv";
|
| 1002 |
live: string;
|
|
|
|
| 1003 |
}) {
|
| 1004 |
const {
|
| 1005 |
playback_url,
|
|
@@ -1009,11 +1025,13 @@ async function F_build_ffmpeg_command_v3(arg0: {
|
|
| 1009 |
subtitles,
|
| 1010 |
probe_data,
|
| 1011 |
output_format,
|
| 1012 |
-
live,
|
|
|
|
|
|
|
| 1013 |
} = arg0;
|
| 1014 |
|
| 1015 |
let { playback_headers } = arg0;
|
| 1016 |
-
|
| 1017 |
// -------------------------------
|
| 1018 |
// Logging options
|
| 1019 |
// -------------------------------
|
|
@@ -1081,11 +1099,11 @@ async function F_build_ffmpeg_command_v3(arg0: {
|
|
| 1081 |
obj: playback_headers
|
| 1082 |
});
|
| 1083 |
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
|
| 1087 |
const other_headers = Object.entries(playback_headers)
|
| 1088 |
-
.filter(([k]) => !["user-
|
| 1089 |
.map(([k, v]) => `${k}: ${v}`)
|
| 1090 |
.join("\r\n");
|
| 1091 |
|
|
@@ -1097,7 +1115,6 @@ async function F_build_ffmpeg_command_v3(arg0: {
|
|
| 1097 |
|
| 1098 |
add_headers_if_needed();
|
| 1099 |
|
| 1100 |
-
// command.push("-http_persistent", "0");
|
| 1101 |
// command.push("-http_multiple", "0");
|
| 1102 |
// command.push("-analyzeduration", "10M");
|
| 1103 |
// command.push("-probesize", "10M");
|
|
@@ -1107,7 +1124,12 @@ async function F_build_ffmpeg_command_v3(arg0: {
|
|
| 1107 |
command.push("-fflags", "+discardcorrupt+genpts+nobuffer");
|
| 1108 |
command.push("-err_detect", "ignore_err");
|
| 1109 |
|
| 1110 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
command.push("-http_persistent", "1");
|
| 1112 |
}
|
| 1113 |
|
|
@@ -1347,16 +1369,13 @@ async function F_build_ffmpeg_command_v3(arg0: {
|
|
| 1347 |
|
| 1348 |
/* OUTPUT FORMAT & FLAGS */
|
| 1349 |
|
| 1350 |
-
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
|
| 1356 |
-
command.push(
|
| 1357 |
-
"-avoid_negative_ts",
|
| 1358 |
-
"make_zero"
|
| 1359 |
-
);
|
| 1360 |
|
| 1361 |
if (live === "stream") {
|
| 1362 |
// command.push("-use_wallclock_as_timestamps", "1");
|
|
@@ -1364,7 +1383,7 @@ async function F_build_ffmpeg_command_v3(arg0: {
|
|
| 1364 |
|
| 1365 |
command.push("-fps_mode", "passthrough");
|
| 1366 |
|
| 1367 |
-
if (is_net_src) {
|
| 1368 |
// command.push("-movflags", "frag_keyframe+empty_moov+default_base_moof");
|
| 1369 |
command.push("-movflags", "frag_keyframe+dash+delay_moov");
|
| 1370 |
} else {
|
|
@@ -1458,7 +1477,6 @@ function F_stderr_data_read(arg0: { output: string }) {
|
|
| 1458 |
}
|
| 1459 |
|
| 1460 |
function F_format_duration(seconds?: number): string {
|
| 1461 |
-
|
| 1462 |
if (typeof seconds !== "number") {
|
| 1463 |
return "";
|
| 1464 |
}
|
|
@@ -1469,7 +1487,7 @@ function F_format_duration(seconds?: number): string {
|
|
| 1469 |
const mins = Math.floor((seconds % 3600) / 60);
|
| 1470 |
const secs = Math.floor(seconds % 60);
|
| 1471 |
|
| 1472 |
-
let data =
|
| 1473 |
|
| 1474 |
if (hrs > 0) {
|
| 1475 |
data = `${hrs}:${mins.toString().padStart(2, "0")}:${secs
|
|
|
|
| 20 |
APP_ENV === "prod" ||
|
| 21 |
APP_ENV === "pre_prod";
|
| 22 |
|
| 23 |
+
const ffmpeg_path = is_prod ? "ffmpeg" : ffmpeg || "ffmpeg";
|
| 24 |
+
const ffprobe_path = is_prod ? "ffprobe" : ffprobe.path || "ffprobe";
|
| 25 |
|
| 26 |
// const ffmpeg_path = ffmpeg || "";
|
| 27 |
// const ffprobe_path = ffprobe.path;
|
|
|
|
| 223 |
playback_url: URL;
|
| 224 |
playback_type?: string;
|
| 225 |
context?: Context;
|
| 226 |
+
file_mode?: {
|
| 227 |
+
path: string;
|
| 228 |
+
};
|
| 229 |
}) {
|
| 230 |
+
const { playback_url, context, file_mode } = arg0;
|
| 231 |
let { playback_headers } = arg0;
|
| 232 |
|
| 233 |
+
if (!playback_headers) {
|
| 234 |
+
playback_headers = {};
|
| 235 |
+
}
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
+
if (!playback_url && !file_mode?.path) {
|
| 238 |
+
throw Error("Invalid playback endpoint");
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
if (playback_url && !file_mode?.path) {
|
| 242 |
+
const [E_, R_] = await NS_promise.F_catch_promise({
|
| 243 |
+
promise: NS_misc.F_test_connection({
|
| 244 |
+
url: playback_url,
|
| 245 |
+
headers: playback_headers
|
| 246 |
+
})
|
| 247 |
});
|
| 248 |
|
| 249 |
+
if (E_) {
|
| 250 |
+
const { status, message } = NS_error.F_collect_error_status_n_message({
|
| 251 |
+
ERR: E_
|
| 252 |
+
});
|
| 253 |
+
|
| 254 |
+
if (
|
| 255 |
+
["522", "510", "502", "503", "404", "410", "429"].includes(`${status}`)
|
| 256 |
+
)
|
| 257 |
+
throw E_;
|
| 258 |
+
}
|
| 259 |
}
|
| 260 |
|
| 261 |
const controller = new AbortController();
|
|
|
|
| 364 |
}
|
| 365 |
}
|
| 366 |
|
| 367 |
+
if (!file_mode) add_headers_if_needed();
|
| 368 |
|
| 369 |
// add the input flag and URL
|
| 370 |
+
command.push("-i", file_mode?.path || playback_url.href);
|
| 371 |
|
| 372 |
console.log("[PROBE CMD]", command, "\n");
|
| 373 |
|
|
|
|
| 1015 |
probe_data: T_Probe;
|
| 1016 |
output_format: "ts" | "mp4" | "mkv";
|
| 1017 |
live: string;
|
| 1018 |
+
file_mode?: boolean;
|
| 1019 |
}) {
|
| 1020 |
const {
|
| 1021 |
playback_url,
|
|
|
|
| 1025 |
subtitles,
|
| 1026 |
probe_data,
|
| 1027 |
output_format,
|
| 1028 |
+
live,
|
| 1029 |
+
playback_type,
|
| 1030 |
+
file_mode
|
| 1031 |
} = arg0;
|
| 1032 |
|
| 1033 |
let { playback_headers } = arg0;
|
| 1034 |
+
|
| 1035 |
// -------------------------------
|
| 1036 |
// Logging options
|
| 1037 |
// -------------------------------
|
|
|
|
| 1099 |
obj: playback_headers
|
| 1100 |
});
|
| 1101 |
|
| 1102 |
+
if (ua) command.push("-user_agent", ua);
|
| 1103 |
+
if (ref) command.push("-referer", ref);
|
| 1104 |
|
| 1105 |
const other_headers = Object.entries(playback_headers)
|
| 1106 |
+
.filter(([k]) => !["user-agent", "referer"].includes(k.toLowerCase()))
|
| 1107 |
.map(([k, v]) => `${k}: ${v}`)
|
| 1108 |
.join("\r\n");
|
| 1109 |
|
|
|
|
| 1115 |
|
| 1116 |
add_headers_if_needed();
|
| 1117 |
|
|
|
|
| 1118 |
// command.push("-http_multiple", "0");
|
| 1119 |
// command.push("-analyzeduration", "10M");
|
| 1120 |
// command.push("-probesize", "10M");
|
|
|
|
| 1124 |
command.push("-fflags", "+discardcorrupt+genpts+nobuffer");
|
| 1125 |
command.push("-err_detect", "ignore_err");
|
| 1126 |
|
| 1127 |
+
if (
|
| 1128 |
+
playback_type === "hls" ||
|
| 1129 |
+
(typeof playback_type === "string" && playback_type.includes("hls")) ||
|
| 1130 |
+
(typeof probe_data.format_name === "string" &&
|
| 1131 |
+
probe_data.format_name.includes("hls"))
|
| 1132 |
+
) {
|
| 1133 |
command.push("-http_persistent", "1");
|
| 1134 |
}
|
| 1135 |
|
|
|
|
| 1369 |
|
| 1370 |
/* OUTPUT FORMAT & FLAGS */
|
| 1371 |
|
| 1372 |
+
if (live === "stream") {
|
| 1373 |
+
command.push("-f", "ismv");
|
| 1374 |
+
} else {
|
| 1375 |
+
command.push("-f", output_format === "ts" ? "mpegts" : output_format);
|
| 1376 |
+
}
|
| 1377 |
|
| 1378 |
+
command.push("-avoid_negative_ts", "make_zero");
|
|
|
|
|
|
|
|
|
|
| 1379 |
|
| 1380 |
if (live === "stream") {
|
| 1381 |
// command.push("-use_wallclock_as_timestamps", "1");
|
|
|
|
| 1383 |
|
| 1384 |
command.push("-fps_mode", "passthrough");
|
| 1385 |
|
| 1386 |
+
if (is_net_src && !file_mode) {
|
| 1387 |
// command.push("-movflags", "frag_keyframe+empty_moov+default_base_moof");
|
| 1388 |
command.push("-movflags", "frag_keyframe+dash+delay_moov");
|
| 1389 |
} else {
|
|
|
|
| 1477 |
}
|
| 1478 |
|
| 1479 |
function F_format_duration(seconds?: number): string {
|
|
|
|
| 1480 |
if (typeof seconds !== "number") {
|
| 1481 |
return "";
|
| 1482 |
}
|
|
|
|
| 1487 |
const mins = Math.floor((seconds % 3600) / 60);
|
| 1488 |
const secs = Math.floor(seconds % 60);
|
| 1489 |
|
| 1490 |
+
let data = "";
|
| 1491 |
|
| 1492 |
if (hrs > 0) {
|
| 1493 |
data = `${hrs}:${mins.toString().padStart(2, "0")}:${secs
|
src/sub-router/ffmpeg.ts
CHANGED
|
@@ -22,6 +22,7 @@ const sub_router = new Elysia({ prefix: "/ffmpeg" })
|
|
| 22 |
let playback_headers = body?.playback_headers;
|
| 23 |
let gen = body?.gen;
|
| 24 |
let playback_type = body?.playback_type;
|
|
|
|
| 25 |
|
| 26 |
const REQ0 = request.url;
|
| 27 |
const REQ_ORIGIN = (ctx as any).publicOrigin || new URL(REQ0).origin;
|
|
@@ -67,7 +68,8 @@ const sub_router = new Elysia({ prefix: "/ffmpeg" })
|
|
| 67 |
promise: NS_ffmpeg.F_probe_v2({
|
| 68 |
playback_url: new URL(playback),
|
| 69 |
context: ctx,
|
| 70 |
-
playback_headers,
|
|
|
|
| 71 |
})
|
| 72 |
});
|
| 73 |
|
|
@@ -173,6 +175,14 @@ const sub_router = new Elysia({ prefix: "/ffmpeg" })
|
|
| 173 |
output_destination = `${output_destination}.${output_format}`;
|
| 174 |
}
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
const data = {
|
| 177 |
playback,
|
| 178 |
playback_headers,
|
|
@@ -182,7 +192,9 @@ const sub_router = new Elysia({ prefix: "/ffmpeg" })
|
|
| 182 |
subtitles: subtitles_ary,
|
| 183 |
out_mime,
|
| 184 |
probe_data,
|
| 185 |
-
progress_update_endpoint,
|
|
|
|
|
|
|
| 186 |
};
|
| 187 |
|
| 188 |
const data_str = devalue.stringify(data);
|
|
@@ -241,6 +253,14 @@ const sub_router = new Elysia({ prefix: "/ffmpeg" })
|
|
| 241 |
}
|
| 242 |
})
|
| 243 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
gen: t.Optional(
|
| 245 |
t.Object(
|
| 246 |
{
|
|
|
|
| 22 |
let playback_headers = body?.playback_headers;
|
| 23 |
let gen = body?.gen;
|
| 24 |
let playback_type = body?.playback_type;
|
| 25 |
+
const sbe_id = body?.sbe_id;
|
| 26 |
|
| 27 |
const REQ0 = request.url;
|
| 28 |
const REQ_ORIGIN = (ctx as any).publicOrigin || new URL(REQ0).origin;
|
|
|
|
| 68 |
promise: NS_ffmpeg.F_probe_v2({
|
| 69 |
playback_url: new URL(playback),
|
| 70 |
context: ctx,
|
| 71 |
+
playback_headers,
|
| 72 |
+
playback_type
|
| 73 |
})
|
| 74 |
});
|
| 75 |
|
|
|
|
| 175 |
output_destination = `${output_destination}.${output_format}`;
|
| 176 |
}
|
| 177 |
|
| 178 |
+
if (
|
| 179 |
+
!playback_type &&
|
| 180 |
+
probe_data &&
|
| 181 |
+
typeof probe_data.format_name === "string"
|
| 182 |
+
) {
|
| 183 |
+
playback_type = probe_data.format_name;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
const data = {
|
| 187 |
playback,
|
| 188 |
playback_headers,
|
|
|
|
| 192 |
subtitles: subtitles_ary,
|
| 193 |
out_mime,
|
| 194 |
probe_data,
|
| 195 |
+
progress_update_endpoint,
|
| 196 |
+
playback_type,
|
| 197 |
+
sbe_id
|
| 198 |
};
|
| 199 |
|
| 200 |
const data_str = devalue.stringify(data);
|
|
|
|
| 253 |
}
|
| 254 |
})
|
| 255 |
),
|
| 256 |
+
sbe_id: t.Optional(
|
| 257 |
+
t.String({
|
| 258 |
+
minLength: 1,
|
| 259 |
+
error() {
|
| 260 |
+
return "Invalid sbe id";
|
| 261 |
+
}
|
| 262 |
+
})
|
| 263 |
+
),
|
| 264 |
gen: t.Optional(
|
| 265 |
t.Object(
|
| 266 |
{
|
src/sub-router/showtime.ts
CHANGED
|
@@ -2,15 +2,30 @@ import { Context, Elysia, t } from "elysia";
|
|
| 2 |
import createError from "http-errors";
|
| 3 |
import { Subprocess } from "bun";
|
| 4 |
import { PassThrough } from "node:stream";
|
|
|
|
| 5 |
import { NS_misc } from "../lib/misc";
|
| 6 |
import * as devalue from "devalue";
|
| 7 |
import { NS_promise } from "../lib/promise";
|
| 8 |
import { NS_ffmpeg } from "../lib/ffmpeg";
|
| 9 |
import { NS_error } from "../lib/error";
|
| 10 |
import { redis } from "../lib/upstash";
|
|
|
|
|
|
|
| 11 |
|
| 12 |
const APP_ENV = process.env.APP_ENV;
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
const sub_router = new Elysia({ prefix: "/showtime" })
|
| 15 |
|
| 16 |
.get(
|
|
@@ -171,7 +186,9 @@ async function F_handle_process(arg0: {
|
|
| 171 |
subtitles,
|
| 172 |
out_mime,
|
| 173 |
probe_data,
|
| 174 |
-
progress_update_endpoint,
|
|
|
|
|
|
|
| 175 |
} = start_data;
|
| 176 |
|
| 177 |
const REQ0 = request.url;
|
|
@@ -207,38 +224,19 @@ async function F_handle_process(arg0: {
|
|
| 207 |
output_format = "mp4";
|
| 208 |
}
|
| 209 |
|
| 210 |
-
const is_network_source =
|
| 211 |
|
| 212 |
if (!out_mime) {
|
| 213 |
out_mime = (() => {
|
| 214 |
switch (output_format) {
|
| 215 |
case "mp4":
|
| 216 |
-
|
| 217 |
-
return "video/mp4";
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
break;
|
| 221 |
-
|
| 222 |
case "mkv":
|
| 223 |
-
|
| 224 |
-
return "video/x-matroska";
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
break;
|
| 228 |
-
|
| 229 |
case "ts":
|
| 230 |
-
|
| 231 |
-
return "video/mp2t";
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
break;
|
| 235 |
-
|
| 236 |
default:
|
| 237 |
-
|
| 238 |
-
return "video/mp4";
|
| 239 |
-
}
|
| 240 |
-
|
| 241 |
-
break;
|
| 242 |
}
|
| 243 |
})();
|
| 244 |
}
|
|
@@ -330,6 +328,99 @@ async function F_handle_process(arg0: {
|
|
| 330 |
return start_data;
|
| 331 |
}
|
| 332 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
// -------------------------------
|
| 334 |
// Setup abort handling
|
| 335 |
// -------------------------------
|
|
@@ -339,9 +430,219 @@ async function F_handle_process(arg0: {
|
|
| 339 |
|
| 340 |
function kill_controller() {
|
| 341 |
controller.abort();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
console.log("[CONTROLLER] Request aborted");
|
| 343 |
}
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
if (abort_signal) {
|
| 346 |
if (abort_signal.aborted) {
|
| 347 |
kill_controller();
|
|
@@ -372,16 +673,22 @@ async function F_handle_process(arg0: {
|
|
| 372 |
subtitles: subtitles_ary,
|
| 373 |
has_tcp_progress: tcp_progress,
|
| 374 |
is_net_src: is_network_source,
|
| 375 |
-
playback_headers: playback_headers,
|
|
|
|
|
|
|
| 376 |
});
|
| 377 |
|
| 378 |
if (tcp_progress) {
|
| 379 |
commands.push("-progress", progress_feedback_url!.trim());
|
| 380 |
} else {
|
| 381 |
-
commands.push("-progress", "pipe:2");
|
| 382 |
}
|
| 383 |
|
| 384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
|
| 386 |
if (ops === "cmd") {
|
| 387 |
return {
|
|
@@ -394,11 +701,14 @@ async function F_handle_process(arg0: {
|
|
| 394 |
// -------------------------------
|
| 395 |
if (is_network_source) {
|
| 396 |
/* ---------- set response headers ---------- */
|
| 397 |
-
ctx.set.headers["content-type"] = out_mime;
|
| 398 |
ctx.set.headers["cache-control"] = "no-cache";
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
| 402 |
}
|
| 403 |
if (probe_data.duration) {
|
| 404 |
ctx.set.headers["X-Content-Duration'"] = probe_data.duration;
|
|
@@ -424,6 +734,17 @@ async function F_handle_process(arg0: {
|
|
| 424 |
const bun_proc = (() => {
|
| 425 |
let proc: Subprocess<"ignore" | "pipe", "pipe", "pipe">;
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
try {
|
| 428 |
// Try with extra pipe (fd:3)
|
| 429 |
proc = Bun.spawn([NS_ffmpeg.ffmpeg_path, ...commands], {
|
|
@@ -472,6 +793,8 @@ async function F_handle_process(arg0: {
|
|
| 472 |
const original_filesize =
|
| 473 |
probe_data && NS_ffmpeg.F_format_size(probe_data?.size);
|
| 474 |
|
|
|
|
|
|
|
| 475 |
// capture stderr for errors/warnings/misc
|
| 476 |
(async () => {
|
| 477 |
for await (const chunk of bun_proc.stderr) {
|
|
@@ -480,7 +803,7 @@ async function F_handle_process(arg0: {
|
|
| 480 |
if (!log) continue;
|
| 481 |
|
| 482 |
// console.log(log, "\n");
|
| 483 |
-
|
| 484 |
if (log.toLowerCase().includes("error") || log.startsWith("[error]")) {
|
| 485 |
console.error("[ffmpeg log ERROR]", log, "\n");
|
| 486 |
logged_proc_errors.push(log);
|
|
@@ -526,19 +849,26 @@ async function F_handle_process(arg0: {
|
|
| 526 |
|
| 527 |
if (progress_update && Object.keys(progress_update).length > 0) {
|
| 528 |
(() => {
|
| 529 |
-
if
|
|
|
|
|
|
|
|
|
|
| 530 |
let _: any = progress_update.video_duration;
|
| 531 |
-
if
|
| 532 |
-
|
|
|
|
|
|
|
|
|
|
| 533 |
}
|
| 534 |
|
| 535 |
-
if(typeof progress_update.video_duration ===
|
| 536 |
-
_ = parseFloat(progress_update.video_duration)
|
| 537 |
}
|
| 538 |
|
| 539 |
-
progress_update.f_video_duration =
|
|
|
|
| 540 |
}
|
| 541 |
-
})()
|
| 542 |
|
| 543 |
if (APP_ENV === "dev") {
|
| 544 |
console.log("[ffmpeg PROGRESS]:", { progress_update }, "\n");
|
|
@@ -554,6 +884,19 @@ async function F_handle_process(arg0: {
|
|
| 554 |
...(play_title ? { play_title } : {})
|
| 555 |
};
|
| 556 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
// forward progress update to websocket clients
|
| 558 |
if (
|
| 559 |
progress_update_endpoint &&
|
|
@@ -592,6 +935,42 @@ async function F_handle_process(arg0: {
|
|
| 592 |
const passthrough = new PassThrough();
|
| 593 |
|
| 594 |
(async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
try {
|
| 596 |
for await (const chunk of bun_proc.stdout) {
|
| 597 |
kill_controller_timeout(); // first byte → cancel
|
|
@@ -601,9 +980,15 @@ async function F_handle_process(arg0: {
|
|
| 601 |
passthrough.end();
|
| 602 |
} catch (e) {
|
| 603 |
passthrough.destroy(e as Error);
|
|
|
|
| 604 |
}
|
| 605 |
})();
|
| 606 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
bun_proc.exited.then((code: number) => {
|
| 608 |
kill_controller_timeout();
|
| 609 |
|
|
@@ -614,7 +999,10 @@ async function F_handle_process(arg0: {
|
|
| 614 |
}
|
| 615 |
|
| 616 |
if (code === 0) {
|
| 617 |
-
console.log(
|
|
|
|
|
|
|
|
|
|
| 618 |
} else {
|
| 619 |
console.warn(
|
| 620 |
`FFmpeg exited with code: ${code}`,
|
|
|
|
| 2 |
import createError from "http-errors";
|
| 3 |
import { Subprocess } from "bun";
|
| 4 |
import { PassThrough } from "node:stream";
|
| 5 |
+
import node_fs from "node:fs";
|
| 6 |
import { NS_misc } from "../lib/misc";
|
| 7 |
import * as devalue from "devalue";
|
| 8 |
import { NS_promise } from "../lib/promise";
|
| 9 |
import { NS_ffmpeg } from "../lib/ffmpeg";
|
| 10 |
import { NS_error } from "../lib/error";
|
| 11 |
import { redis } from "../lib/upstash";
|
| 12 |
+
import { NS_routes_c } from "../../../../../packages/lib/helpers/client/route";
|
| 13 |
+
import path from "node:path";
|
| 14 |
|
| 15 |
const APP_ENV = process.env.APP_ENV;
|
| 16 |
|
| 17 |
+
// Define the job type
|
| 18 |
+
type T_TranscodeJob = {
|
| 19 |
+
status: "in-progress" | "done" | "failed";
|
| 20 |
+
tmp_path?: string | null;
|
| 21 |
+
created_at: number;
|
| 22 |
+
error?: string;
|
| 23 |
+
percent?: number;
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
// Create the store (module level)
|
| 27 |
+
const transcode_jobs = new Map<string, T_TranscodeJob>();
|
| 28 |
+
|
| 29 |
const sub_router = new Elysia({ prefix: "/showtime" })
|
| 30 |
|
| 31 |
.get(
|
|
|
|
| 186 |
subtitles,
|
| 187 |
out_mime,
|
| 188 |
probe_data,
|
| 189 |
+
progress_update_endpoint,
|
| 190 |
+
playback_type,
|
| 191 |
+
sbe_id
|
| 192 |
} = start_data;
|
| 193 |
|
| 194 |
const REQ0 = request.url;
|
|
|
|
| 224 |
output_format = "mp4";
|
| 225 |
}
|
| 226 |
|
| 227 |
+
const is_network_source = is_valid_playback_url;
|
| 228 |
|
| 229 |
if (!out_mime) {
|
| 230 |
out_mime = (() => {
|
| 231 |
switch (output_format) {
|
| 232 |
case "mp4":
|
| 233 |
+
return "video/mp4";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
case "mkv":
|
| 235 |
+
return "video/x-matroska";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
case "ts":
|
| 237 |
+
return "video/mp2t";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
default:
|
| 239 |
+
return "video/mp4";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
}
|
| 241 |
})();
|
| 242 |
}
|
|
|
|
| 328 |
return start_data;
|
| 329 |
}
|
| 330 |
|
| 331 |
+
// -------------------------------
|
| 332 |
+
// Determine if download mode
|
| 333 |
+
// -------------------------------
|
| 334 |
+
const is_download = live === "download" && sbe_id;
|
| 335 |
+
|
| 336 |
+
const job_id = is_download ? `${sbe_id}-${output_destination}` : undefined;
|
| 337 |
+
|
| 338 |
+
const in_factory = (() => {
|
| 339 |
+
if (!job_id) return;
|
| 340 |
+
const current_job = transcode_jobs.get(job_id);
|
| 341 |
+
if (!current_job) return;
|
| 342 |
+
|
| 343 |
+
const job_status = current_job.status;
|
| 344 |
+
const failed = job_status === "failed";
|
| 345 |
+
const done = job_status === "done";
|
| 346 |
+
const in_progress = job_status === "in-progress";
|
| 347 |
+
|
| 348 |
+
if (failed) {
|
| 349 |
+
transcode_jobs.delete(job_id);
|
| 350 |
+
return;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
return true;
|
| 354 |
+
})();
|
| 355 |
+
|
| 356 |
+
if (in_factory && job_id) {
|
| 357 |
+
const current_job = transcode_jobs.get(job_id);
|
| 358 |
+
const job_status = current_job?.status;
|
| 359 |
+
|
| 360 |
+
switch (job_status) {
|
| 361 |
+
case "in-progress":
|
| 362 |
+
{
|
| 363 |
+
const percent = current_job?.percent;
|
| 364 |
+
const percentage = typeof percent === "number" ? `${percent} %` : "";
|
| 365 |
+
|
| 366 |
+
return percentage
|
| 367 |
+
? `ENGINE PROGRESS: ${percentage}`
|
| 368 |
+
: "ENGINE PROCESSING ...";
|
| 369 |
+
}
|
| 370 |
+
break;
|
| 371 |
+
|
| 372 |
+
case "done":
|
| 373 |
+
{
|
| 374 |
+
const file_path = current_job?.tmp_path;
|
| 375 |
+
const _ = await serve_download({ file_path, job_id, ctx, request });
|
| 376 |
+
if (_) return _;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
break;
|
| 380 |
+
}
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
const tmpDir = path.join(
|
| 384 |
+
process.cwd(),
|
| 385 |
+
"S3",
|
| 386 |
+
"ffmpeg-downloads",
|
| 387 |
+
`${sbe_id}`
|
| 388 |
+
);
|
| 389 |
+
|
| 390 |
+
let tmp_download_path: string | null | undefined = await (async () => {
|
| 391 |
+
if (!is_download) return;
|
| 392 |
+
try {
|
| 393 |
+
// await Bun.mkdir(tmpDir, { recursive: true }).catch(() => {});
|
| 394 |
+
|
| 395 |
+
node_fs.mkdirSync(tmpDir, { recursive: true });
|
| 396 |
+
|
| 397 |
+
const tmpPath = path.join(tmpDir, `${Date.now()}-${output_destination}`);
|
| 398 |
+
|
| 399 |
+
console.log("\n", tmpPath, "\n");
|
| 400 |
+
|
| 401 |
+
return tmpPath;
|
| 402 |
+
} catch (e) {}
|
| 403 |
+
})();
|
| 404 |
+
|
| 405 |
+
const file_mode = is_download && tmp_download_path;
|
| 406 |
+
|
| 407 |
+
if (file_mode) {
|
| 408 |
+
const found_file = await find_latest_valid_file({
|
| 409 |
+
sbe_id,
|
| 410 |
+
output_destination
|
| 411 |
+
});
|
| 412 |
+
|
| 413 |
+
if (typeof found_file === "string" && found_file) {
|
| 414 |
+
const _ = await serve_download({
|
| 415 |
+
file_path: found_file,
|
| 416 |
+
job_id,
|
| 417 |
+
ctx,
|
| 418 |
+
request
|
| 419 |
+
});
|
| 420 |
+
if (_) return _;
|
| 421 |
+
}
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
// -------------------------------
|
| 425 |
// Setup abort handling
|
| 426 |
// -------------------------------
|
|
|
|
| 430 |
|
| 431 |
function kill_controller() {
|
| 432 |
controller.abort();
|
| 433 |
+
// Cleanup temp file if user aborts mid-transcode
|
| 434 |
+
if (tmp_download_path) {
|
| 435 |
+
const _ = `${tmp_download_path}`;
|
| 436 |
+
let try_again = 0;
|
| 437 |
+
|
| 438 |
+
delete_self();
|
| 439 |
+
|
| 440 |
+
function delete_self() {
|
| 441 |
+
if (try_again > 3) return;
|
| 442 |
+
|
| 443 |
+
node_fs.promises.unlink(_).catch((e) => {
|
| 444 |
+
setTimeout(() => {
|
| 445 |
+
delete_self();
|
| 446 |
+
try_again++;
|
| 447 |
+
}, 1_000);
|
| 448 |
+
});
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
task_failed();
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
console.log("[CONTROLLER] Request aborted");
|
| 455 |
}
|
| 456 |
|
| 457 |
+
function task_failed() {
|
| 458 |
+
(() => {
|
| 459 |
+
if (!job_id) return;
|
| 460 |
+
|
| 461 |
+
const current_job = transcode_jobs.get(job_id);
|
| 462 |
+
|
| 463 |
+
if (!current_job) {
|
| 464 |
+
transcode_jobs.set(job_id, {
|
| 465 |
+
status: "failed",
|
| 466 |
+
tmp_path: tmp_download_path,
|
| 467 |
+
created_at: Date.now()
|
| 468 |
+
});
|
| 469 |
+
} else {
|
| 470 |
+
transcode_jobs.set(job_id, {
|
| 471 |
+
...current_job,
|
| 472 |
+
status: "failed"
|
| 473 |
+
});
|
| 474 |
+
}
|
| 475 |
+
})();
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
function task_finished() {
|
| 479 |
+
(() => {
|
| 480 |
+
if (!job_id) return;
|
| 481 |
+
|
| 482 |
+
const current_job = transcode_jobs.get(job_id);
|
| 483 |
+
|
| 484 |
+
if (!current_job) {
|
| 485 |
+
transcode_jobs.set(job_id, {
|
| 486 |
+
status: "done",
|
| 487 |
+
tmp_path: tmp_download_path,
|
| 488 |
+
created_at: Date.now()
|
| 489 |
+
});
|
| 490 |
+
} else {
|
| 491 |
+
transcode_jobs.set(job_id, {
|
| 492 |
+
...current_job,
|
| 493 |
+
status: "done"
|
| 494 |
+
});
|
| 495 |
+
}
|
| 496 |
+
})();
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
function task_processing(arg0: { percent: number }) {
|
| 500 |
+
const { percent } = arg0;
|
| 501 |
+
|
| 502 |
+
(() => {
|
| 503 |
+
if (!job_id) return;
|
| 504 |
+
|
| 505 |
+
const current_job = transcode_jobs.get(job_id);
|
| 506 |
+
|
| 507 |
+
if (!current_job) {
|
| 508 |
+
transcode_jobs.set(job_id, {
|
| 509 |
+
status: "in-progress",
|
| 510 |
+
tmp_path: tmp_download_path,
|
| 511 |
+
percent,
|
| 512 |
+
created_at: Date.now()
|
| 513 |
+
});
|
| 514 |
+
} else {
|
| 515 |
+
transcode_jobs.set(job_id, {
|
| 516 |
+
...current_job,
|
| 517 |
+
status: "in-progress",
|
| 518 |
+
percent
|
| 519 |
+
});
|
| 520 |
+
}
|
| 521 |
+
})();
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
async function serve_download(arg0: {
|
| 525 |
+
file_path: string | null | undefined;
|
| 526 |
+
job_id?: string;
|
| 527 |
+
ctx: any;
|
| 528 |
+
request: Request;
|
| 529 |
+
}) {
|
| 530 |
+
const { file_path, job_id, ctx, request } = arg0;
|
| 531 |
+
|
| 532 |
+
if (!file_path) {
|
| 533 |
+
return;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
const file = Bun.file(file_path);
|
| 537 |
+
const exists = await file.exists();
|
| 538 |
+
|
| 539 |
+
if (!exists) {
|
| 540 |
+
if (job_id) transcode_jobs.delete(job_id);
|
| 541 |
+
return;
|
| 542 |
+
}
|
| 543 |
+
const file_size = file.size;
|
| 544 |
+
|
| 545 |
+
if (!file_size || file_size <= 0) return;
|
| 546 |
+
|
| 547 |
+
ctx.set.headers["content-disposition"] =
|
| 548 |
+
`attachment; filename="${output_destination}"`;
|
| 549 |
+
ctx.set.headers["cache-control"] = "no-cache";
|
| 550 |
+
ctx.set.headers["accept-ranges"] = "bytes";
|
| 551 |
+
|
| 552 |
+
const range_header = request.headers.get("range");
|
| 553 |
+
|
| 554 |
+
// if (range_header) {
|
| 555 |
+
// const match = range_header.match(/^bytes=(\d*)-(\d*)$/i);
|
| 556 |
+
// if (match) {
|
| 557 |
+
// const start = match[1] ? parseInt(match[1], 10) : 0;
|
| 558 |
+
// const end = match[2]
|
| 559 |
+
// ? Math.min(parseInt(match[2], 10), file_size - 1)
|
| 560 |
+
// : file_size - 1;
|
| 561 |
+
|
| 562 |
+
// if (start > end || start >= file_size) {
|
| 563 |
+
// ctx.set.status = 416;
|
| 564 |
+
// ctx.set.headers["content-range"] = `bytes */${file_size}`;
|
| 565 |
+
// return "Range Not Satisfiable";
|
| 566 |
+
// }
|
| 567 |
+
|
| 568 |
+
// ctx.set.status = 206;
|
| 569 |
+
// ctx.set.headers["content-range"] = `bytes ${start}-${end}/${file_size}`;
|
| 570 |
+
// ctx.set.headers["content-length"] = String(end - start + 1);
|
| 571 |
+
// return file.slice(start, end + 1);
|
| 572 |
+
// }
|
| 573 |
+
// }
|
| 574 |
+
|
| 575 |
+
return file;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
// ── helper (module level) ───────
|
| 579 |
+
async function find_latest_valid_file(arg0: {
|
| 580 |
+
sbe_id: string;
|
| 581 |
+
output_destination: string;
|
| 582 |
+
}): Promise<string | null | undefined> {
|
| 583 |
+
const { sbe_id, output_destination } = arg0;
|
| 584 |
+
|
| 585 |
+
const dir = path.join(process.cwd(), "S3", "ffmpeg-downloads", `${sbe_id}`);
|
| 586 |
+
|
| 587 |
+
try {
|
| 588 |
+
const entries = await node_fs.promises.readdir(dir);
|
| 589 |
+
|
| 590 |
+
// keep only files that contain output_destination in the name
|
| 591 |
+
const matches = entries.filter((f) => f.includes(output_destination));
|
| 592 |
+
|
| 593 |
+
if (!matches.length) return null;
|
| 594 |
+
|
| 595 |
+
// sort descending by the timestamp prefix (e.g. "1714000000000-video.mp4")
|
| 596 |
+
matches.sort((a, b) => {
|
| 597 |
+
const ts_a = parseInt(a.split("-")[0]) || 0;
|
| 598 |
+
const ts_b = parseInt(b.split("-")[0]) || 0;
|
| 599 |
+
return ts_b - ts_a; // latest first
|
| 600 |
+
});
|
| 601 |
+
|
| 602 |
+
// return the first one that exists and is not corrupted
|
| 603 |
+
const virgin_file = await (async () => {
|
| 604 |
+
for (const filename of matches) {
|
| 605 |
+
const full_path = path.join(dir, filename);
|
| 606 |
+
|
| 607 |
+
try {
|
| 608 |
+
const stat = await node_fs.promises.stat(full_path);
|
| 609 |
+
if (!(stat.size > 0)) {
|
| 610 |
+
node_fs.promises.unlink(full_path).catch(() => {});
|
| 611 |
+
continue;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
const [E_, R_] = await NS_promise.F_catch_promise({
|
| 615 |
+
promise: NS_ffmpeg.F_probe_v2({
|
| 616 |
+
playback_url: new URL("http://localhost:0000/"),
|
| 617 |
+
context: ctx as any,
|
| 618 |
+
file_mode: {
|
| 619 |
+
path: full_path
|
| 620 |
+
}
|
| 621 |
+
})
|
| 622 |
+
});
|
| 623 |
+
|
| 624 |
+
if (R_ && R_.probe_data) {
|
| 625 |
+
return full_path;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
if (E_) {
|
| 629 |
+
node_fs.promises.unlink(full_path).catch(() => {});
|
| 630 |
+
}
|
| 631 |
+
} catch (e) {
|
| 632 |
+
continue;
|
| 633 |
+
}
|
| 634 |
+
}
|
| 635 |
+
})();
|
| 636 |
+
|
| 637 |
+
if (virgin_file) {
|
| 638 |
+
return virgin_file;
|
| 639 |
+
}
|
| 640 |
+
} catch (e) {
|
| 641 |
+
// dir doesn't exist yet or unreadable
|
| 642 |
+
return null;
|
| 643 |
+
}
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
if (abort_signal) {
|
| 647 |
if (abort_signal.aborted) {
|
| 648 |
kill_controller();
|
|
|
|
| 673 |
subtitles: subtitles_ary,
|
| 674 |
has_tcp_progress: tcp_progress,
|
| 675 |
is_net_src: is_network_source,
|
| 676 |
+
playback_headers: playback_headers,
|
| 677 |
+
playback_type,
|
| 678 |
+
file_mode: file_mode
|
| 679 |
});
|
| 680 |
|
| 681 |
if (tcp_progress) {
|
| 682 |
commands.push("-progress", progress_feedback_url!.trim());
|
| 683 |
} else {
|
| 684 |
+
commands.push("-progress", "pipe:2");
|
| 685 |
}
|
| 686 |
|
| 687 |
+
if (file_mode) {
|
| 688 |
+
commands.push("-y", tmp_download_path);
|
| 689 |
+
} else {
|
| 690 |
+
commands.push("-y", "pipe:1");
|
| 691 |
+
}
|
| 692 |
|
| 693 |
if (ops === "cmd") {
|
| 694 |
return {
|
|
|
|
| 701 |
// -------------------------------
|
| 702 |
if (is_network_source) {
|
| 703 |
/* ---------- set response headers ---------- */
|
| 704 |
+
if (!tmp_download_path) ctx.set.headers["content-type"] = out_mime;
|
| 705 |
ctx.set.headers["cache-control"] = "no-cache";
|
| 706 |
+
|
| 707 |
+
if (is_download) {
|
| 708 |
+
if (!tmp_download_path) {
|
| 709 |
+
ctx.set.headers["content-disposition"] =
|
| 710 |
+
`attachment; filename="${output_destination}"`;
|
| 711 |
+
}
|
| 712 |
}
|
| 713 |
if (probe_data.duration) {
|
| 714 |
ctx.set.headers["X-Content-Duration'"] = probe_data.duration;
|
|
|
|
| 734 |
const bun_proc = (() => {
|
| 735 |
let proc: Subprocess<"ignore" | "pipe", "pipe", "pipe">;
|
| 736 |
|
| 737 |
+
if (file_mode) {
|
| 738 |
+
proc = Bun.spawn([NS_ffmpeg.ffmpeg_path, ...commands], {
|
| 739 |
+
stdout: "pipe",
|
| 740 |
+
stderr: "pipe",
|
| 741 |
+
stdin: "pipe",
|
| 742 |
+
signal: controller.signal
|
| 743 |
+
});
|
| 744 |
+
|
| 745 |
+
return proc;
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
try {
|
| 749 |
// Try with extra pipe (fd:3)
|
| 750 |
proc = Bun.spawn([NS_ffmpeg.ffmpeg_path, ...commands], {
|
|
|
|
| 793 |
const original_filesize =
|
| 794 |
probe_data && NS_ffmpeg.F_format_size(probe_data?.size);
|
| 795 |
|
| 796 |
+
let schemed_data: any = null;
|
| 797 |
+
|
| 798 |
// capture stderr for errors/warnings/misc
|
| 799 |
(async () => {
|
| 800 |
for await (const chunk of bun_proc.stderr) {
|
|
|
|
| 803 |
if (!log) continue;
|
| 804 |
|
| 805 |
// console.log(log, "\n");
|
| 806 |
+
|
| 807 |
if (log.toLowerCase().includes("error") || log.startsWith("[error]")) {
|
| 808 |
console.error("[ffmpeg log ERROR]", log, "\n");
|
| 809 |
logged_proc_errors.push(log);
|
|
|
|
| 849 |
|
| 850 |
if (progress_update && Object.keys(progress_update).length > 0) {
|
| 851 |
(() => {
|
| 852 |
+
if (
|
| 853 |
+
progress_update.video_duration &&
|
| 854 |
+
!(progress_update as any).f_video_duration
|
| 855 |
+
) {
|
| 856 |
let _: any = progress_update.video_duration;
|
| 857 |
+
if (
|
| 858 |
+
typeof progress_update.video_duration !== "string" &&
|
| 859 |
+
typeof progress_update.video_duration !== "number"
|
| 860 |
+
) {
|
| 861 |
+
return;
|
| 862 |
}
|
| 863 |
|
| 864 |
+
if (typeof progress_update.video_duration === "string") {
|
| 865 |
+
_ = parseFloat(progress_update.video_duration);
|
| 866 |
}
|
| 867 |
|
| 868 |
+
(progress_update as any).f_video_duration =
|
| 869 |
+
NS_ffmpeg.F_format_duration(_);
|
| 870 |
}
|
| 871 |
+
})();
|
| 872 |
|
| 873 |
if (APP_ENV === "dev") {
|
| 874 |
console.log("[ffmpeg PROGRESS]:", { progress_update }, "\n");
|
|
|
|
| 884 |
...(play_title ? { play_title } : {})
|
| 885 |
};
|
| 886 |
|
| 887 |
+
schemed_data = progress_update;
|
| 888 |
+
|
| 889 |
+
(() => {
|
| 890 |
+
const percent = (progress_update as any)?.percentage;
|
| 891 |
+
if (percent && parseFloat(`${percent}`)) {
|
| 892 |
+
task_processing({ percent: parseFloat(`${percent}`) });
|
| 893 |
+
|
| 894 |
+
if (parseFloat(`${percent}`) >= 100) {
|
| 895 |
+
task_finished();
|
| 896 |
+
}
|
| 897 |
+
}
|
| 898 |
+
})();
|
| 899 |
+
|
| 900 |
// forward progress update to websocket clients
|
| 901 |
if (
|
| 902 |
progress_update_endpoint &&
|
|
|
|
| 935 |
const passthrough = new PassThrough();
|
| 936 |
|
| 937 |
(async () => {
|
| 938 |
+
if (file_mode) {
|
| 939 |
+
try {
|
| 940 |
+
const reader = bun_proc.stdout.getReader();
|
| 941 |
+
const decoder = new TextDecoder();
|
| 942 |
+
|
| 943 |
+
while (true) {
|
| 944 |
+
kill_controller_timeout();
|
| 945 |
+
(() => {
|
| 946 |
+
if (!job_id) return;
|
| 947 |
+
transcode_jobs.set(job_id, {
|
| 948 |
+
status: "in-progress",
|
| 949 |
+
tmp_path: tmp_download_path,
|
| 950 |
+
created_at: Date.now()
|
| 951 |
+
});
|
| 952 |
+
})();
|
| 953 |
+
|
| 954 |
+
const { done, value } = await reader.read();
|
| 955 |
+
if (done) {
|
| 956 |
+
console.log("\n");
|
| 957 |
+
console.log(
|
| 958 |
+
`FFMPEG finished -> ${schemed_data?.current_filesize} ${tmp_download_path}`,
|
| 959 |
+
"\n"
|
| 960 |
+
);
|
| 961 |
+
break;
|
| 962 |
+
}
|
| 963 |
+
|
| 964 |
+
// if (APP_ENV === "dev") {
|
| 965 |
+
// console.log(`FFMPEG STDOUT: ${decoder.decode(value)}`);
|
| 966 |
+
// }
|
| 967 |
+
}
|
| 968 |
+
} catch (e) {
|
| 969 |
+
console.error("Error reading stdout:", e);
|
| 970 |
+
}
|
| 971 |
+
return;
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
try {
|
| 975 |
for await (const chunk of bun_proc.stdout) {
|
| 976 |
kill_controller_timeout(); // first byte → cancel
|
|
|
|
| 980 |
passthrough.end();
|
| 981 |
} catch (e) {
|
| 982 |
passthrough.destroy(e as Error);
|
| 983 |
+
task_failed();
|
| 984 |
}
|
| 985 |
})();
|
| 986 |
|
| 987 |
+
if (file_mode) {
|
| 988 |
+
// return "Compile started, check your output folder: " + tmp_download_path;
|
| 989 |
+
return `COMPILER ENGINE started, this may take a while. ${progress_update_endpoint ? `check process -> ${progress_update_endpoint}` : "check back later"}`;
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
bun_proc.exited.then((code: number) => {
|
| 993 |
kill_controller_timeout();
|
| 994 |
|
|
|
|
| 999 |
}
|
| 1000 |
|
| 1001 |
if (code === 0) {
|
| 1002 |
+
console.log(
|
| 1003 |
+
`FFMPEG finished -> ${schemed_data?.current_filesize} ${output_destination}`,
|
| 1004 |
+
"\n"
|
| 1005 |
+
);
|
| 1006 |
} else {
|
| 1007 |
console.warn(
|
| 1008 |
`FFmpeg exited with code: ${code}`,
|