GUI-STUDIO commited on
Commit
bb60a2d
·
1 Parent(s): e32339f

Add sbe_id download flow & file_mode ffmpeg

Browse files

Propagate 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 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" : (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,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
- const [E_, R_] = await NS_promise.F_catch_promise({
231
- promise: NS_misc.F_test_connection({
232
- url: playback_url,
233
- headers: playback_headers
234
- })
235
- });
236
 
237
- if (E_) {
238
- const { status, message } = NS_error.F_collect_error_status_n_message({
239
- ERR: E_
 
 
 
 
 
 
 
240
  });
241
 
242
- if (["522", "510", "502", "503", "404", "410"].includes(`${status}`))
243
- throw E_;
 
 
 
 
 
 
 
 
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, playback_type
 
 
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
- // if (ua) command.push("-user_agent", ua);
1085
- // if (ref) command.push("-referer", ref);
1086
 
1087
  const other_headers = Object.entries(playback_headers)
1088
- .filter(([k]) => !["user-agentx", "refererx"].includes(k.toLowerCase()))
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(playback_type === 'hls' || (typeof probe_data.format_name === 'string' && probe_data.format_name.includes("hls"))) {
 
 
 
 
 
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
- // if (live === "stream") {
1351
- // command.push("-f", "ismv");
1352
- // } else {
1353
- command.push("-f", output_format === "ts" ? "mpegts" : output_format);
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, playback_type
 
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, playback_type
 
 
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, playback_type
 
 
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 = true;
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, playback_type
 
 
376
  });
377
 
378
  if (tcp_progress) {
379
  commands.push("-progress", progress_feedback_url!.trim());
380
  } else {
381
- commands.push("-progress", "pipe:2"); // progress goes to stderr, for fd3 use "pipe:3"
382
  }
383
 
384
- commands.push("-y", "pipe:1");
 
 
 
 
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
- if (live === "download") {
400
- ctx.set.headers["content-disposition"] =
401
- `attachment; filename="${output_destination}"`;
 
 
 
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(progress_update.video_duration && !progress_update.f_video_duration) {
 
 
 
530
  let _: any = progress_update.video_duration;
531
- if(typeof progress_update.video_duration !== 'string' && typeof progress_update.video_duration !== 'number') {
532
- return
 
 
 
533
  }
534
 
535
- if(typeof progress_update.video_duration === 'string') {
536
- _ = parseFloat(progress_update.video_duration)
537
  }
538
 
539
- progress_update.f_video_duration = NS_ffmpeg.F_format_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(`FFmpeg finished -> ${output_destination}`, "\n");
 
 
 
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}`,