GUI-STUDIO commited on
Commit
68467dc
·
1 Parent(s): 7644799

Proxima: forward method/body; refactor showtime

Browse files

Forward decoded proxy commands (method and upstream_payload) through proxima proxy handlers (backend Hono/Elysia and Cloudflare worker) so upstream requests can use POST bodies and custom methods. Add handling for base64-embedded payloads (b64.base and b64.data) in download.portal.helper and include b64 metadata in the ffmpeg response. Refactor showtime route into a reusable F_handle_process function, add a POST /:live endpoint, tighten parameter validation, and support ops (cmd/raw/decode). Add a test /baz7 route, bump several cloudflare_worker dev/deps versions, and comment out the Wrangler build command. These changes enable proxying of POST payloads and improve maintainability of the showtime processing logic.

Files changed (2) hide show
  1. src/sub-router/ffmpeg.ts +5 -1
  2. src/sub-router/showtime.ts +495 -436
src/sub-router/ffmpeg.ts CHANGED
@@ -198,7 +198,11 @@ const sub_router = new Elysia({ prefix: "/ffmpeg" })
198
  return {
199
  follow: url_o.href,
200
  ...data,
201
- metadata: R_.metadata
 
 
 
 
202
  };
203
  },
204
  {
 
198
  return {
199
  follow: url_o.href,
200
  ...data,
201
+ metadata: R_.metadata,
202
+ b64: {
203
+ base: new URL(`/showtime/${live}`, REQ_ORIGIN).toString(),
204
+ data: data_b64
205
+ }
206
  };
207
  },
208
  {
src/sub-router/showtime.ts CHANGED
@@ -10,536 +10,595 @@ import { NS_error } from "../lib/error";
10
 
11
  const APP_ENV = process.env.APP_ENV;
12
 
13
- const sub_router = new Elysia({ prefix: "/showtime" }).get(
14
- "/:live/:data",
15
- async (ctx) => {
16
- const { params, query, request, status } = ctx;
17
- let { live, data } = params;
18
- let { ops } = query;
19
-
20
- const start_data = (() => {
21
- try {
22
- let raw: any = data;
23
- const is_encoded = NS_misc.isUrlEncoded(data);
24
-
25
- if (is_encoded) {
26
- raw = decodeURIComponent(data);
27
- }
28
-
29
- // // Add back padding if needed
30
- // let data_b64 = raw;
31
- //
32
- // while (data_b64.length % 4) {
33
- // data_b64 += "=";
34
- // }
35
- // data_b64 = data_b64.replace(/-/g, "+").replace(/_/g, "/");
36
- // raw = Buffer.from(data_b64, "base64").toString("utf8");
37
-
38
- raw = Buffer.from(raw, "base64").toString("utf8");
39
-
40
- raw = devalue.parse(raw);
41
-
42
- return raw;
43
- } catch (error: unknown) {
44
- console.error(error);
45
- return;
46
- }
47
- })();
48
-
49
- if (
50
- !start_data ||
51
- typeof start_data !== "object" ||
52
- Array.isArray(start_data)
53
- ) {
54
- throw createError(400, "Please provide valid start data");
55
  }
56
-
57
- if (ops === "decode") {
58
- return start_data;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- let {
62
- playback: playback_url,
63
- playback_headers,
64
- output_format,
65
- output_destination,
66
- progress_feedback_url,
67
- subtitles,
68
- out_mime,
69
- probe_data,
70
- progress_update_endpoint
71
- } = start_data;
72
 
73
- const REQ0 = request.url;
74
- const REQ_ORIGIN = (ctx as any).publicOrigin || new URL(REQ0).origin;
75
 
76
- const is_valid_playback_url = (() => {
77
- const is_encoded = NS_misc.isUrlEncoded(playback_url);
78
 
79
- if (!is_encoded) playback_url = encodeURI(playback_url);
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- return NS_misc.isValidUrlFormat(playback_url);
82
- })();
 
83
 
84
- const is_valid_progress_feedback_url = (() => {
85
- if (!progress_feedback_url) return;
 
 
 
 
 
 
 
 
 
86
 
87
- const is_encoded = NS_misc.isUrlEncoded(progress_feedback_url);
 
88
 
89
- if (!is_encoded) progress_feedback_url = encodeURI(progress_feedback_url);
 
90
 
91
- return NS_misc.isValidUrlFormat(progress_feedback_url);
92
- })();
93
 
94
- if (!is_valid_playback_url) {
95
- throw createError(400, "Please provide a valid playback URL");
96
- }
97
 
98
- if (!is_valid_playback_url) {
99
- throw createError(400, "Please provide a valid playback URL");
100
- }
101
 
102
- if (!output_format) {
103
- output_format = "mp4";
104
- }
105
 
106
- const is_network_source = true;
107
 
108
- if (!out_mime) {
109
- out_mime = (() => {
110
- switch (output_format) {
111
- case "mp4":
112
- {
113
- return "video/mp4";
114
- }
115
 
116
- break;
117
-
118
- case "mkv":
119
- {
120
- return "video/x-matroska";
121
- }
122
-
123
- break;
124
-
125
- case "ts":
126
- {
127
- return "video/mp2t";
128
- }
129
-
130
- break;
131
 
132
- default:
133
- {
134
- return "video/mp4";
135
- }
136
 
137
- break;
138
- }
139
- })();
140
- }
141
 
142
- if (!output_destination) {
143
- output_destination = `video-${Date.now()}.${output_format}`.trim();
144
- }
145
 
146
- if (!output_destination.endsWith(`.${output_format}`)) {
147
- output_destination = `${output_destination}.${output_format}`;
148
- }
 
 
 
 
149
 
150
- const tcp_progress = is_valid_progress_feedback_url ? true : false;
151
 
152
- const subtitles_ary = (() => {
153
- if (!subtitles) return;
 
 
154
 
155
- const _ = (() => {
156
- if (typeof subtitles === "string") {
157
- try {
158
- const data = JSON.parse(subtitles);
159
 
160
- if (typeof data === "object") return data;
161
- } catch (error: unknown) {
162
- return;
163
  }
164
- }
165
 
166
- return subtitles;
167
- })();
168
 
169
- if (typeof _ === "object") {
170
- if (!Array.isArray(_)) {
171
- return [_];
172
- }
173
 
174
- return _;
175
  }
176
  })();
 
177
 
178
- start_data.subtitles = subtitles_ary;
 
 
179
 
180
- playback_headers = (() => {
181
- if (!playback_headers) {
182
- return {};
183
- }
184
 
185
- if (
186
- typeof playback_headers === "object" &&
187
- !Array.isArray(playback_headers)
188
- ) {
189
- return playback_headers;
190
- }
191
 
192
- if (typeof playback_headers === "string") {
 
193
  try {
194
- const data = JSON.parse(playback_headers);
195
 
196
- if (typeof data === "object" && !Array.isArray(data)) {
197
- return data;
198
- }
199
- } catch (error: unknown) {}
200
  }
201
 
202
- return {};
203
  })();
204
 
205
- if (typeof probe_data !== "object" || Array.isArray(probe_data)) {
206
- const [E_, R_] = await NS_promise.F_catch_promise({
207
- promise: NS_ffmpeg.F_probe_v2({
208
- playback_url: new URL(playback_url),
209
- context: ctx as any,
210
- playback_headers
211
- })
212
- });
213
-
214
- if (E_ || !R_) {
215
- const { status, message } = NS_error.F_collect_error_status_n_message({
216
- ERR: E_
217
- });
218
-
219
- throw createError(status, message);
220
  }
221
 
222
- probe_data = R_.probe_data;
223
  }
 
 
 
224
 
225
- if (ops === "raw") {
226
- return start_data;
 
227
  }
228
 
229
- // -------------------------------
230
- // Setup abort handling
231
- // -------------------------------
232
- const abort_signal = ctx?.request.signal;
 
 
233
 
234
- const controller = new AbortController();
 
 
235
 
236
- function kill_controller() {
237
- controller.abort();
238
- console.log("[CONTROLLER] Request aborted");
 
239
  }
240
 
241
- if (abort_signal) {
242
- if (abort_signal.aborted) {
243
- kill_controller();
244
- } else {
245
- abort_signal.addEventListener(
246
- "abort",
247
- () => {
248
- kill_controller();
249
- },
250
- {
251
- once: true
252
- }
253
- );
254
- }
255
- }
256
 
257
- console.log("\n", "PINGING FFMPEG NOW", "\n");
258
-
259
- // -------------------------------
260
- // Build commands
261
- // -------------------------------
262
- const commands = await NS_ffmpeg.F_build_ffmpeg_command_v3({
263
- playback_url: new URL(playback_url),
264
- context: ctx as any,
265
- output_format,
266
- live,
267
- probe_data,
268
- subtitles: subtitles_ary,
269
- has_tcp_progress: tcp_progress,
270
- is_net_src: is_network_source,
271
- playback_headers: playback_headers
272
  });
273
 
274
- if (tcp_progress) {
275
- commands.push("-progress", progress_feedback_url!.trim());
276
- } else {
277
- commands.push("-progress", "pipe:2"); // progress goes to stderr, for fd3 use "pipe:3"
 
 
278
  }
279
 
280
- commands.push("-y", "pipe:1");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
- if (ops === "cmd") {
283
- return {
284
- commands
285
- };
 
 
 
 
 
 
 
 
 
286
  }
 
287
 
288
- // -------------------------------
289
- // network response headers
290
- // -------------------------------
291
- if (is_network_source) {
292
- /* ---------- set response headers ---------- */
293
- ctx.set.headers["content-type"] = out_mime;
294
- ctx.set.headers["cache-control"] = "no-cache";
295
- if (live === "download") {
296
- ctx.set.headers["content-disposition"] =
297
- `attachment; filename="${output_destination}"`;
298
- }
299
- if(probe_data.duration) {
300
- ctx.set.headers["X-Content-Duration'"] = probe_data.duration;
301
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  }
 
 
 
 
303
 
304
- let t: Timer | undefined;
305
 
306
- t = setTimeout(kill_controller, NS_ffmpeg.TIMEOUT_MS);
307
 
308
- function kill_controller_timeout() {
309
- if (t) {
310
- clearTimeout(t);
311
- t = undefined;
312
- }
313
  }
 
314
 
315
- // -------------------------------
316
- // spawn
317
- // -------------------------------
318
- console.log("[FFMPEG CMD]", commands, "\n");
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
- const bun_proc = (() => {
321
- let proc: Subprocess<"ignore" | "pipe", "pipe", "pipe">;
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
- try {
324
- // Try with extra pipe (fd:3)
325
- proc = Bun.spawn([NS_ffmpeg.ffmpeg_path, ...commands], {
326
- stdout: "pipe", // video info, stream mapping
327
- stderr: "pipe", // normal logs, warnings, errors
328
- stdin: "pipe",
329
- stdio: ["ignore", "pipe", "pipe", "pipe"] /* 👈 add fd:3 */,
330
- signal: controller.signal
331
- });
332
-
333
- (proc as any).extraPipe = true; // custom flag so we know later
334
- } catch (err) {
335
- console.warn(
336
- "[Spawn FFmpeg] Failed to create extra pipe, falling back to stderr:",
337
- err
338
- );
339
 
340
- // Fallback: no pipe:3
341
- proc = Bun.spawn([NS_ffmpeg.ffmpeg_path, ...commands], {
342
- stdout: "pipe", // video info, stream mapping
343
- stderr: "pipe", // normal logs, warnings, errors
344
- stdin: "pipe",
345
- signal: controller.signal
346
- });
347
 
348
- (proc as any).extraPipe = false;
349
- }
350
 
351
- return proc;
352
- })();
353
 
354
- const logged_proc_errors: string[] = [];
 
 
355
 
356
- const has_pipe_3 = (bun_proc as any).extraPipe && bun_proc.stdio[3];
 
 
357
 
358
- // log progress
359
- const decoder = new TextDecoder();
360
- let stderrBuffer = ""; // Buffer for incomplete lines
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
 
362
- // capture progress from fd:3
363
- if (has_pipe_3) {
364
- }
365
 
366
- let video_duration =
367
- probe_data && NS_ffmpeg.F_format_duration(probe_data?.duration);
368
- const original_filesize =
369
- probe_data && NS_ffmpeg.F_format_size(probe_data?.size);
370
-
371
- // capture stderr for errors/warnings/misc
372
- (async () => {
373
- for await (const chunk of bun_proc.stderr) {
374
- const log = decoder.decode(chunk).trim();
375
-
376
- if (!log) continue;
377
-
378
- // console.log(log, "\n");
379
- //
380
- if (log.toLowerCase().includes("error") || log.startsWith("[error]")) {
381
- console.error("[ffmpeg log ERROR]", log, "\n");
382
- logged_proc_errors.push(log);
383
- continue;
384
- } else if (log.toLowerCase().includes("failed")) {
385
- console.error("[ffmpeg log FAIL]", log, "\n");
386
- logged_proc_errors.push(log);
387
- continue;
388
  }
 
 
 
 
 
 
 
 
 
 
 
389
 
390
- let progress_update = NS_ffmpeg.F_stderr_data_read({ output: log });
 
 
 
 
 
 
391
 
392
- if (!parseInt(`${progress_update?.current_out_time}`)) {
393
- if (progress_update.video_duration && !probe_data.duration) {
394
- probe_data.duration = progress_update.video_duration;
395
- video_duration = NS_ffmpeg.F_format_duration(probe_data.duration);
396
- }
397
- (progress_update as any) = undefined;
398
- } else {
399
- const total_duration = probe_data?.duration || 0;
400
- const current_time = progress_update.current_out_time;
401
- const current_size_bytes = progress_update.current_size_bytes;
402
-
403
- progress_update.video_duration = total_duration;
404
- (progress_update as any).percentage = Math.min(
405
- parseFloat(((current_time / total_duration) * 100).toFixed(2)),
406
- 100
407
- );
408
-
409
- (progress_update as any).f_video_duration = video_duration;
410
- (progress_update as any).f_current_out_time =
411
- NS_ffmpeg.F_format_duration(current_time);
412
- (progress_update as any).original_filesize = original_filesize;
413
- (progress_update as any).current_filesize =
414
- NS_ffmpeg.F_format_size(current_size_bytes);
415
  }
 
416
 
 
417
  if (APP_ENV === "dev") {
418
- if (!progress_update || !(Object.keys(progress_update).length > 0)) {
419
- // console.log(`[ffmpeg log] ${log}`, "\n");
420
- }
421
  }
422
 
423
- if (progress_update && Object.keys(progress_update).length > 0) {
424
- if (APP_ENV === "dev") {
425
- console.log("[ffmpeg PROGRESS]:", { progress_update }, "\n");
426
- }
427
 
428
- const ws_client_id = ctx.request.headers.get("x-ws-client-id");
429
-
430
- const play_title = ctx.request.headers.get("x-play-title");
431
-
432
- const forward_progress = {
433
- filename: output_destination,
434
- ...progress_update,
435
- ...(play_title ? { play_title } : {})
436
- };
437
-
438
- // forward progress update to websocket clients
439
- if (
440
- progress_update_endpoint &&
441
- NS_misc.isValidUrlFormat(progress_update_endpoint)
442
- ) {
443
- try {
444
- fetch(progress_update_endpoint, {
445
- method: "POST",
446
- headers: {
447
- "Content-Type": "application/json"
448
- },
449
- body: JSON.stringify(forward_progress)
450
- });
451
- } catch (e) {}
452
- }
453
 
454
- if (ctx && ctx.request?.headers) {
455
- // const ws_clients = NS_websockets.ws_clients;
456
- // if (ws_client_id && ws_clients) {
457
- // const target = ws_clients.get(ws_client_id);
458
- // target?.send(
459
- // JSON.stringify({
460
- // type: `${
461
- // live === "download" ? "DOWNLOAD" : "STREAM"
462
- // } PROGRESS`,
463
- // data: forward_progress
464
- // })
465
- // );
466
- // }
467
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  }
469
  }
470
- })();
 
471
 
472
- // stream mode
473
- const passthrough = new PassThrough();
474
 
475
- (async () => {
476
- try {
477
- for await (const chunk of bun_proc.stdout) {
478
- kill_controller_timeout(); // first byte → cancel
479
 
480
- passthrough.write(chunk);
481
- }
482
- passthrough.end();
483
- } catch (e) {
484
- passthrough.destroy(e as Error);
485
  }
486
- })();
 
 
 
 
487
 
488
- bun_proc.exited.then((code: number) => {
489
- kill_controller_timeout();
490
 
491
- if (code !== 0 && !passthrough.destroyed) {
492
- passthrough.destroy(new Error(`FFmpeg exited with code ${code}`));
493
- } else if (!passthrough.destroyed) {
494
- passthrough.end();
495
- }
496
 
497
- if (code === 0) {
498
- console.log(`FFmpeg finished -> ${output_destination}`, "\n");
499
- } else {
500
- console.warn(
501
- `FFmpeg exited with code: ${code}`,
502
- "\n",
503
- "[ffmpeg log ERRORS]",
504
- logged_proc_errors,
505
- "\n"
506
- );
507
- }
508
- });
509
 
510
- return passthrough;
511
- },
512
- {
513
- params: t.Object({
514
- live: t.Union([t.Literal("download"), t.Literal("stream")], {
515
- error() {
516
- return "Invalid live operation, must be one of: download, stream";
517
- }
518
- }),
519
- data: t.String({
520
- minLength: 1,
521
- error() {
522
- return "Invalid process data, Expected string";
523
- }
524
- })
525
- }),
526
- query: t.Object({
527
- raw: t.Optional(
528
- t.Number({
529
- minimum: 0,
530
- maximum: 1,
531
- error: () => "Invalid raw, Expected binary boolean"
532
- })
533
- ),
534
- ops: t.Optional(
535
- t.Union([t.Literal("cmd"), t.Literal("raw"), t.Literal("decode")], {
536
- error() {
537
- return "Invalid operation, must be one of: cmd, raw, decode";
538
- }
539
- })
540
- )
541
- })
542
- }
543
- );
544
 
545
  export const ShowtimeRouter = sub_router;
 
10
 
11
  const APP_ENV = process.env.APP_ENV;
12
 
13
+ const sub_router = new Elysia({ prefix: "/showtime" })
14
+
15
+ .get(
16
+ "/:live/:data",
17
+ async (ctx) => {
18
+ const { params, query, request, status } = ctx;
19
+ let { live, data } = params;
20
+ let { ops } = query;
21
+
22
+ return await F_handle_process({ data, request, ops, ctx, live });
23
+ },
24
+ {
25
+ params: t.Object({
26
+ live: t.Union([t.Literal("download"), t.Literal("stream")], {
27
+ error() {
28
+ return "Invalid live operation, must be one of: download, stream";
29
+ }
30
+ }),
31
+ data: t.String({
32
+ minLength: 1,
33
+ error() {
34
+ return "Invalid process data, Expected string";
35
+ }
36
+ })
37
+ }),
38
+ query: t.Object({
39
+ raw: t.Optional(
40
+ t.Number({
41
+ minimum: 0,
42
+ maximum: 1,
43
+ error: () => "Invalid raw, Expected binary boolean"
44
+ })
45
+ ),
46
+ ops: t.Optional(
47
+ t.Union([t.Literal("cmd"), t.Literal("raw"), t.Literal("decode")], {
48
+ error() {
49
+ return "Invalid operation, must be one of: cmd, raw, decode";
50
+ }
51
+ })
52
+ )
53
+ })
 
54
  }
55
+ )
56
+
57
+ .post(
58
+ "/:live",
59
+ async (ctx) => {
60
+ const { params, query, body, request, status } = ctx;
61
+ let { live } = params;
62
+ let { ops } = query;
63
+ let { data } = body;
64
+
65
+ return await F_handle_process({ data, request, ops, ctx, live });
66
+ },
67
+ {
68
+ params: t.Object({
69
+ live: t.Union([t.Literal("download"), t.Literal("stream")], {
70
+ error() {
71
+ return "Invalid live operation, must be one of: download, stream";
72
+ }
73
+ })
74
+ }),
75
+ query: t.Object({
76
+ raw: t.Optional(
77
+ t.Number({
78
+ minimum: 0,
79
+ maximum: 1,
80
+ error: () => "Invalid raw, Expected binary boolean"
81
+ })
82
+ ),
83
+ ops: t.Optional(
84
+ t.Union([t.Literal("cmd"), t.Literal("raw"), t.Literal("decode")], {
85
+ error() {
86
+ return "Invalid operation, must be one of: cmd, raw, decode";
87
+ }
88
+ })
89
+ )
90
+ }),
91
+ body: t.Object({
92
+ data: t.String({
93
+ minLength: 1,
94
+ error() {
95
+ return "Invalid process data, Expected string";
96
+ }
97
+ })
98
+ })
99
  }
100
+ );
101
+
102
+ async function F_handle_process(arg0: {
103
+ data: string;
104
+ live: string;
105
+ ops?: string;
106
+ request: Request;
107
+ ctx: any;
108
+ }) {
109
+ const { data, ops, request, ctx, live } = arg0;
110
+
111
+ const start_data = (() => {
112
+ try {
113
+ let raw: any = data;
114
+ const is_encoded = NS_misc.isUrlEncoded(data);
115
+
116
+ if (is_encoded) {
117
+ raw = decodeURIComponent(data);
118
+ }
119
 
120
+ // // Add back padding if needed
121
+ // let data_b64 = raw;
122
+ //
123
+ // while (data_b64.length % 4) {
124
+ // data_b64 += "=";
125
+ // }
126
+ // data_b64 = data_b64.replace(/-/g, "+").replace(/_/g, "/");
127
+ // raw = Buffer.from(data_b64, "base64").toString("utf8");
 
 
 
128
 
129
+ raw = Buffer.from(raw, "base64").toString("utf8");
 
130
 
131
+ raw = devalue.parse(raw);
 
132
 
133
+ return raw;
134
+ } catch (error: unknown) {
135
+ console.error(error);
136
+ return;
137
+ }
138
+ })();
139
+
140
+ if (
141
+ !start_data ||
142
+ typeof start_data !== "object" ||
143
+ Array.isArray(start_data)
144
+ ) {
145
+ throw createError(400, "Please provide valid start data");
146
+ }
147
 
148
+ if (ops === "decode") {
149
+ return start_data;
150
+ }
151
 
152
+ let {
153
+ playback: playback_url,
154
+ playback_headers,
155
+ output_format,
156
+ output_destination,
157
+ progress_feedback_url,
158
+ subtitles,
159
+ out_mime,
160
+ probe_data,
161
+ progress_update_endpoint
162
+ } = start_data;
163
 
164
+ const REQ0 = request.url;
165
+ const REQ_ORIGIN = (ctx as any).publicOrigin || new URL(REQ0).origin;
166
 
167
+ const is_valid_playback_url = (() => {
168
+ const is_encoded = NS_misc.isUrlEncoded(playback_url);
169
 
170
+ if (!is_encoded) playback_url = encodeURI(playback_url);
 
171
 
172
+ return NS_misc.isValidUrlFormat(playback_url);
173
+ })();
 
174
 
175
+ const is_valid_progress_feedback_url = (() => {
176
+ if (!progress_feedback_url) return;
 
177
 
178
+ const is_encoded = NS_misc.isUrlEncoded(progress_feedback_url);
 
 
179
 
180
+ if (!is_encoded) progress_feedback_url = encodeURI(progress_feedback_url);
181
 
182
+ return NS_misc.isValidUrlFormat(progress_feedback_url);
183
+ })();
 
 
 
 
 
184
 
185
+ if (!is_valid_playback_url) {
186
+ throw createError(400, "Please provide a valid playback URL");
187
+ }
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ if (!is_valid_playback_url) {
190
+ throw createError(400, "Please provide a valid playback URL");
191
+ }
 
192
 
193
+ if (!output_format) {
194
+ output_format = "mp4";
195
+ }
 
196
 
197
+ const is_network_source = true;
 
 
198
 
199
+ if (!out_mime) {
200
+ out_mime = (() => {
201
+ switch (output_format) {
202
+ case "mp4":
203
+ {
204
+ return "video/mp4";
205
+ }
206
 
207
+ break;
208
 
209
+ case "mkv":
210
+ {
211
+ return "video/x-matroska";
212
+ }
213
 
214
+ break;
 
 
 
215
 
216
+ case "ts":
217
+ {
218
+ return "video/mp2t";
219
  }
 
220
 
221
+ break;
 
222
 
223
+ default:
224
+ {
225
+ return "video/mp4";
226
+ }
227
 
228
+ break;
229
  }
230
  })();
231
+ }
232
 
233
+ if (!output_destination) {
234
+ output_destination = `video-${Date.now()}.${output_format}`.trim();
235
+ }
236
 
237
+ if (!output_destination.endsWith(`.${output_format}`)) {
238
+ output_destination = `${output_destination}.${output_format}`;
239
+ }
 
240
 
241
+ const tcp_progress = is_valid_progress_feedback_url ? true : false;
242
+
243
+ const subtitles_ary = (() => {
244
+ if (!subtitles) return;
 
 
245
 
246
+ const _ = (() => {
247
+ if (typeof subtitles === "string") {
248
  try {
249
+ const data = JSON.parse(subtitles);
250
 
251
+ if (typeof data === "object") return data;
252
+ } catch (error: unknown) {
253
+ return;
254
+ }
255
  }
256
 
257
+ return subtitles;
258
  })();
259
 
260
+ if (typeof _ === "object") {
261
+ if (!Array.isArray(_)) {
262
+ return [_];
 
 
 
 
 
 
 
 
 
 
 
 
263
  }
264
 
265
+ return _;
266
  }
267
+ })();
268
+
269
+ start_data.subtitles = subtitles_ary;
270
 
271
+ playback_headers = (() => {
272
+ if (!playback_headers) {
273
+ return {};
274
  }
275
 
276
+ if (
277
+ typeof playback_headers === "object" &&
278
+ !Array.isArray(playback_headers)
279
+ ) {
280
+ return playback_headers;
281
+ }
282
 
283
+ if (typeof playback_headers === "string") {
284
+ try {
285
+ const data = JSON.parse(playback_headers);
286
 
287
+ if (typeof data === "object" && !Array.isArray(data)) {
288
+ return data;
289
+ }
290
+ } catch (error: unknown) {}
291
  }
292
 
293
+ return {};
294
+ })();
 
 
 
 
 
 
 
 
 
 
 
 
 
295
 
296
+ if (typeof probe_data !== "object" || Array.isArray(probe_data)) {
297
+ const [E_, R_] = await NS_promise.F_catch_promise({
298
+ promise: NS_ffmpeg.F_probe_v2({
299
+ playback_url: new URL(playback_url),
300
+ context: ctx as any,
301
+ playback_headers
302
+ })
 
 
 
 
 
 
 
 
303
  });
304
 
305
+ if (E_ || !R_) {
306
+ const { status, message } = NS_error.F_collect_error_status_n_message({
307
+ ERR: E_
308
+ });
309
+
310
+ throw createError(status, message);
311
  }
312
 
313
+ probe_data = R_.probe_data;
314
+ }
315
+
316
+ if (ops === "raw") {
317
+ return start_data;
318
+ }
319
+
320
+ // -------------------------------
321
+ // Setup abort handling
322
+ // -------------------------------
323
+ const abort_signal = ctx?.request.signal;
324
+
325
+ const controller = new AbortController();
326
+
327
+ function kill_controller() {
328
+ controller.abort();
329
+ console.log("[CONTROLLER] Request aborted");
330
+ }
331
 
332
+ if (abort_signal) {
333
+ if (abort_signal.aborted) {
334
+ kill_controller();
335
+ } else {
336
+ abort_signal.addEventListener(
337
+ "abort",
338
+ () => {
339
+ kill_controller();
340
+ },
341
+ {
342
+ once: true
343
+ }
344
+ );
345
  }
346
+ }
347
 
348
+ console.log("\n", "PINGING FFMPEG NOW", "\n");
349
+
350
+ // -------------------------------
351
+ // Build commands
352
+ // -------------------------------
353
+ const commands = await NS_ffmpeg.F_build_ffmpeg_command_v3({
354
+ playback_url: new URL(playback_url),
355
+ context: ctx as any,
356
+ output_format,
357
+ live,
358
+ probe_data,
359
+ subtitles: subtitles_ary,
360
+ has_tcp_progress: tcp_progress,
361
+ is_net_src: is_network_source,
362
+ playback_headers: playback_headers
363
+ });
364
+
365
+ if (tcp_progress) {
366
+ commands.push("-progress", progress_feedback_url!.trim());
367
+ } else {
368
+ commands.push("-progress", "pipe:2"); // progress goes to stderr, for fd3 use "pipe:3"
369
+ }
370
+
371
+ commands.push("-y", "pipe:1");
372
+
373
+ if (ops === "cmd") {
374
+ return {
375
+ commands
376
+ };
377
+ }
378
+
379
+ // -------------------------------
380
+ // network response headers
381
+ // -------------------------------
382
+ if (is_network_source) {
383
+ /* ---------- set response headers ---------- */
384
+ ctx.set.headers["content-type"] = out_mime;
385
+ ctx.set.headers["cache-control"] = "no-cache";
386
+ if (live === "download") {
387
+ ctx.set.headers["content-disposition"] =
388
+ `attachment; filename="${output_destination}"`;
389
  }
390
+ if (probe_data.duration) {
391
+ ctx.set.headers["X-Content-Duration'"] = probe_data.duration;
392
+ }
393
+ }
394
 
395
+ let t: Timer | undefined;
396
 
397
+ t = setTimeout(kill_controller, NS_ffmpeg.TIMEOUT_MS);
398
 
399
+ function kill_controller_timeout() {
400
+ if (t) {
401
+ clearTimeout(t);
402
+ t = undefined;
 
403
  }
404
+ }
405
 
406
+ // -------------------------------
407
+ // spawn
408
+ // -------------------------------
409
+ console.log("[FFMPEG CMD]", commands, "\n");
410
+
411
+ const bun_proc = (() => {
412
+ let proc: Subprocess<"ignore" | "pipe", "pipe", "pipe">;
413
+
414
+ try {
415
+ // Try with extra pipe (fd:3)
416
+ proc = Bun.spawn([NS_ffmpeg.ffmpeg_path, ...commands], {
417
+ stdout: "pipe", // video info, stream mapping
418
+ stderr: "pipe", // normal logs, warnings, errors
419
+ stdin: "pipe",
420
+ stdio: ["ignore", "pipe", "pipe", "pipe"] /* 👈 add fd:3 */,
421
+ signal: controller.signal
422
+ });
423
 
424
+ (proc as any).extraPipe = true; // custom flag so we know later
425
+ } catch (err) {
426
+ console.warn(
427
+ "[Spawn FFmpeg] Failed to create extra pipe, falling back to stderr:",
428
+ err
429
+ );
430
+
431
+ // Fallback: no pipe:3
432
+ proc = Bun.spawn([NS_ffmpeg.ffmpeg_path, ...commands], {
433
+ stdout: "pipe", // video info, stream mapping
434
+ stderr: "pipe", // normal logs, warnings, errors
435
+ stdin: "pipe",
436
+ signal: controller.signal
437
+ });
438
 
439
+ (proc as any).extraPipe = false;
440
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
+ return proc;
443
+ })();
 
 
 
 
 
444
 
445
+ const logged_proc_errors: string[] = [];
 
446
 
447
+ const has_pipe_3 = (bun_proc as any).extraPipe && bun_proc.stdio[3];
 
448
 
449
+ // log progress
450
+ const decoder = new TextDecoder();
451
+ let stderrBuffer = ""; // Buffer for incomplete lines
452
 
453
+ // capture progress from fd:3
454
+ if (has_pipe_3) {
455
+ }
456
 
457
+ let video_duration =
458
+ probe_data && NS_ffmpeg.F_format_duration(probe_data?.duration);
459
+ const original_filesize =
460
+ probe_data && NS_ffmpeg.F_format_size(probe_data?.size);
461
+
462
+ // capture stderr for errors/warnings/misc
463
+ (async () => {
464
+ for await (const chunk of bun_proc.stderr) {
465
+ const log = decoder.decode(chunk).trim();
466
+
467
+ if (!log) continue;
468
+
469
+ // console.log(log, "\n");
470
+ //
471
+ if (log.toLowerCase().includes("error") || log.startsWith("[error]")) {
472
+ console.error("[ffmpeg log ERROR]", log, "\n");
473
+ logged_proc_errors.push(log);
474
+ continue;
475
+ } else if (log.toLowerCase().includes("failed")) {
476
+ console.error("[ffmpeg log FAIL]", log, "\n");
477
+ logged_proc_errors.push(log);
478
+ continue;
479
+ }
480
 
481
+ let progress_update = NS_ffmpeg.F_stderr_data_read({ output: log });
 
 
482
 
483
+ if (!parseInt(`${progress_update?.current_out_time}`)) {
484
+ if (progress_update.video_duration && !probe_data.duration) {
485
+ probe_data.duration = progress_update.video_duration;
486
+ video_duration = NS_ffmpeg.F_format_duration(probe_data.duration);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  }
488
+ (progress_update as any) = undefined;
489
+ } else {
490
+ const total_duration = probe_data?.duration || 0;
491
+ const current_time = progress_update.current_out_time;
492
+ const current_size_bytes = progress_update.current_size_bytes;
493
+
494
+ progress_update.video_duration = total_duration;
495
+ (progress_update as any).percentage = Math.min(
496
+ parseFloat(((current_time / total_duration) * 100).toFixed(2)),
497
+ 100
498
+ );
499
 
500
+ (progress_update as any).f_video_duration = video_duration;
501
+ (progress_update as any).f_current_out_time =
502
+ NS_ffmpeg.F_format_duration(current_time);
503
+ (progress_update as any).original_filesize = original_filesize;
504
+ (progress_update as any).current_filesize =
505
+ NS_ffmpeg.F_format_size(current_size_bytes);
506
+ }
507
 
508
+ if (APP_ENV === "dev") {
509
+ if (!progress_update || !(Object.keys(progress_update).length > 0)) {
510
+ // console.log(`[ffmpeg log] ${log}`, "\n");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  }
512
+ }
513
 
514
+ if (progress_update && Object.keys(progress_update).length > 0) {
515
  if (APP_ENV === "dev") {
516
+ console.log("[ffmpeg PROGRESS]:", { progress_update }, "\n");
 
 
517
  }
518
 
519
+ const ws_client_id = ctx.request.headers.get("x-ws-client-id");
 
 
 
520
 
521
+ const play_title = ctx.request.headers.get("x-play-title");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
 
523
+ const forward_progress = {
524
+ filename: output_destination,
525
+ ...progress_update,
526
+ ...(play_title ? { play_title } : {})
527
+ };
528
+
529
+ // forward progress update to websocket clients
530
+ if (
531
+ progress_update_endpoint &&
532
+ NS_misc.isValidUrlFormat(progress_update_endpoint)
533
+ ) {
534
+ try {
535
+ fetch(progress_update_endpoint, {
536
+ method: "POST",
537
+ headers: {
538
+ "Content-Type": "application/json"
539
+ },
540
+ body: JSON.stringify(forward_progress)
541
+ });
542
+ } catch (e) {}
543
+ }
544
+
545
+ if (ctx && ctx.request?.headers) {
546
+ // const ws_clients = NS_websockets.ws_clients;
547
+ // if (ws_client_id && ws_clients) {
548
+ // const target = ws_clients.get(ws_client_id);
549
+ // target?.send(
550
+ // JSON.stringify({
551
+ // type: `${
552
+ // live === "download" ? "DOWNLOAD" : "STREAM"
553
+ // } PROGRESS`,
554
+ // data: forward_progress
555
+ // })
556
+ // );
557
+ // }
558
  }
559
  }
560
+ }
561
+ })();
562
 
563
+ // stream mode
564
+ const passthrough = new PassThrough();
565
 
566
+ (async () => {
567
+ try {
568
+ for await (const chunk of bun_proc.stdout) {
569
+ kill_controller_timeout(); // first byte → cancel
570
 
571
+ passthrough.write(chunk);
 
 
 
 
572
  }
573
+ passthrough.end();
574
+ } catch (e) {
575
+ passthrough.destroy(e as Error);
576
+ }
577
+ })();
578
 
579
+ bun_proc.exited.then((code: number) => {
580
+ kill_controller_timeout();
581
 
582
+ if (code !== 0 && !passthrough.destroyed) {
583
+ passthrough.destroy(new Error(`FFmpeg exited with code ${code}`));
584
+ } else if (!passthrough.destroyed) {
585
+ passthrough.end();
586
+ }
587
 
588
+ if (code === 0) {
589
+ console.log(`FFmpeg finished -> ${output_destination}`, "\n");
590
+ } else {
591
+ console.warn(
592
+ `FFmpeg exited with code: ${code}`,
593
+ "\n",
594
+ "[ffmpeg log ERRORS]",
595
+ logged_proc_errors,
596
+ "\n"
597
+ );
598
+ }
599
+ });
600
 
601
+ return passthrough;
602
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
 
604
  export const ShowtimeRouter = sub_router;