GUI-STUDIO commited on
Commit
a0c7e60
·
1 Parent(s): b59895c
src/sub-router/showtime-v2.ts CHANGED
@@ -45,6 +45,11 @@ const APP_ORIGIN = () =>
45
  const HF_BUCKET_NAME = () =>
46
  NS_runtime.ENV_VAR_DB?.HF_BUCKET_NAME || process.env.HF_BUCKET_NAME;
47
 
 
 
 
 
 
48
  const sub_router = new Elysia({ prefix: "/showtimeV2" }).get(
49
  "/delivery/:data_key",
50
  async (ctx) => {
@@ -55,11 +60,6 @@ const sub_router = new Elysia({ prefix: "/showtimeV2" }).get(
55
  const REQ0 = new URL(request.url);
56
  const REQ_ORIGIN = (ctx as any).publicOrigin || REQ0.origin;
57
 
58
- const is_dev = (() => {
59
- // return false;
60
- return APP_ENV() === "dev";
61
- })();
62
-
63
  const metadata = await (async () => {
64
  try {
65
  if (is_dev) {
@@ -85,1242 +85,1288 @@ const sub_router = new Elysia({ prefix: "/showtimeV2" }).get(
85
  return metadata;
86
  }
87
 
88
- const {
89
- playback,
90
- probe_metadata,
91
- output_destination,
92
- sbe_id,
93
- delivery_mode,
94
- delivery_format,
95
- out_mime,
96
- progress_update_endpoint,
97
- subtitles
98
- } = metadata as T_delivery_payload;
99
 
100
- const is_download_mode = delivery_mode === "download";
101
- const is_stream_mode = delivery_mode === "stream";
 
 
102
 
103
- if (!is_download_mode && !is_stream_mode) {
104
- return ctx.status(400, "Unsupported delivery mode");
105
  }
106
 
107
- const clean_metadata = NS_ffmpeg_v2.F_scan_raw_probe_metadata({
108
- metadata: probe_metadata
109
- });
110
-
111
- const picture_quality = (() => {
112
- const _ = playback.picture_quality;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
- if (typeof _ !== "string") {
115
- return "";
116
- }
 
 
 
 
 
 
 
 
 
 
117
 
118
- return _.trim().split(" ").join("");
119
- })();
 
 
 
 
 
 
 
 
 
120
 
121
- const sbe_s_e = (() => {
122
- const _ = metadata.sbe_s_e;
123
 
124
- if (typeof _ !== "string" || _.toLowerCase().includes("nan")) {
125
- return;
126
- }
127
 
128
- return _.trim().split(" ").join("");
129
- })();
130
 
131
- const qLevel = (() => {
132
- if (
133
- typeof metadata.qLevel === "string" &&
134
- metadata.qLevel.trim().length > 0
135
- ) {
136
- return metadata.qLevel.trim();
137
- }
138
 
139
- const _ = NS_ffmpeg_v2.F_get_qLevel({
140
- metadata: probe_metadata
141
- });
142
 
143
- return _;
144
- })();
145
 
146
- const ws = (() => {
147
- const is_https = new URL(REQ_ORIGIN).protocol === "https:";
 
148
 
149
- const ws_protocol = is_https ? "wss" : "ws";
 
150
 
151
- const _ = `${ws_protocol}://${REQ0.host}/ws`;
 
152
 
153
- const data = {
154
- connect: _,
155
- channel: data_key
156
- };
157
 
158
- return data;
159
- })();
160
 
161
- const output_destination_clean = NS_misc.cleanNameV2(output_destination);
 
 
 
 
 
 
162
 
163
- const clean_file_id =
164
- `${picture_quality}-${output_destination_clean}`.toLowerCase();
 
165
 
166
- const job_id = `${sbe_id}-${clean_file_id}`;
 
167
 
168
- const file_id = `${Date.now()}-${clean_file_id}`;
 
169
 
170
- const cache_folder = `cache/${sbe_id}`;
171
 
172
- const S3Dir = path.join(STORAGE_ROOT(), `${cache_folder}`);
173
 
174
- // same key the disk flow uploads to, so the seed lookup finds either one
175
- const bucket_file_path = `${cache_folder}/${file_id}`;
 
 
176
 
177
- // const dl_to_s3 = is_dev || IS_RENDER_COM();
178
- const dl_to_s3 = IS_RENDER_COM();
179
 
180
- // S3 PIPE: only download mode pipes to S3, stream mode always goes to the client
181
- const use_s3_pipe = is_download_mode && dl_to_s3;
182
 
183
- // S3 PIPE: fail fast instead of finding out after ffmpeg has started
184
- if (use_s3_pipe && (!S3() || !HF_BUCKET_NAME())) {
185
- return ctx.status(500, "S3 bucket is not configured");
186
- }
187
 
188
- const file_with_path = (() => {
189
- if (!is_download_mode || use_s3_pipe) return;
190
 
191
- try {
192
- node_fs.mkdirSync(S3Dir, { recursive: true });
193
 
194
- return path.join(S3Dir, file_id);
195
- } catch (e) {}
196
- })();
197
 
198
- if (is_download_mode && !use_s3_pipe && !file_with_path) {
199
- return ctx.status(500, "Could not prepare the output directory");
200
- }
201
 
202
- const MAX_ATTEMPTS = 2;
 
203
 
204
- const MAX_LOG_LINES = 200;
 
205
 
206
- const err_message = (err: unknown) =>
207
- NS_error.F_collect_error_message({ ERR: err }).message;
208
 
209
- const live = {
210
- percent: 0,
211
- size: "",
212
- failed: false,
213
- done: false,
214
- launched: false,
215
- user_aborted: false,
216
- last_push: 0
217
- };
218
 
219
- let flags: string[] = [];
220
- let progress_data: Record<string, any> | undefined = undefined;
221
- let seek_size_bytes = 0;
222
- let logged_proc_errors: string[] = [];
223
- let logged_proc_info: string[] = [];
224
- let has_subs = false;
225
- let local_cleaned = false;
226
- let attempt = 0;
227
- let watchdog: ReturnType<typeof setTimeout> | undefined = undefined;
228
 
229
- let error_429 = false;
230
- let error_end_of_file = false;
231
- let error_403 = false;
232
 
233
- let claimed = false;
 
234
 
235
- // S3 PIPE: state of the current attempt's upload
236
- type S3Outcome = { success: true } | { success: false; error: unknown };
237
 
238
- let s3_attempt:
239
- | { upload: Upload; done: Promise<S3Outcome>; counter: Transform }
240
- | undefined = undefined;
241
 
242
- let s3_bytes = 0;
 
243
 
244
- let controller = new AbortController();
245
- const abort_signal = ctx?.request.signal;
246
 
247
- const on_request_abort = () => {
248
- live.user_aborted = true;
249
 
250
- kill_controller("client aborted");
251
- };
252
 
253
- const detach_request_abort = () => {
254
- abort_signal?.removeEventListener("abort", on_request_abort);
255
- };
256
 
257
- if (abort_signal?.aborted) {
258
- return;
259
- }
260
 
261
- abort_signal?.addEventListener("abort", on_request_abort, { once: true });
 
 
 
 
 
 
 
 
262
 
263
- const passthrough = is_stream_mode ? new PassThrough() : undefined;
 
 
 
 
 
 
 
 
264
 
265
- try {
266
- const current_job = transcode_jobs.get(job_id);
267
-
268
- if (current_job?.status === "running") {
269
- const percent = current_job?.percent;
270
- const uploading = current_job?.uploading;
271
- const percentage = typeof percent === "number" ? `${percent} %` : "";
272
- const transcoded_size = current_job?.transcoded_size;
273
- const progress_endpoint =
274
- current_job?.progress_endpoint || progress_update_endpoint;
275
-
276
- const message = percentage
277
- ? `ENGINE PROGRESS: ${percentage}`
278
- : "ENGINE PROCESSING ...";
279
-
280
- // if(!uploading)
281
- return {
282
- processing: true,
283
- message,
284
- percent,
285
- transcoded_size,
286
- track_process: progress_endpoint,
287
- file: output_destination,
288
- job_id,
289
- output_destination,
290
- qLevel,
291
- picture_quality,
292
- ws,
293
- flags: current_job.flags,
294
- logs: current_job.logs,
295
- ...(ops === "just_check" ? { passed: true } : {})
296
- };
297
- }
298
 
299
- if (current_job?.status === "failed") {
300
- const logs = current_job?.logs;
301
 
302
- if (logs) id_log_errors({ logs });
 
303
 
304
- transcode_jobs.delete(job_id);
 
 
305
 
306
- if (ops === "just_check") {
307
- return {
308
- passed: false,
309
- delivery: output_destination,
310
- track_process: progress_update_endpoint,
311
- ws,
312
- flags: current_job.flags,
313
- logs: current_job?.logs
314
- };
315
- }
316
 
317
- // run ffprobe to see if playback still valid
318
- const _ = await ping_probe({ playback });
319
 
320
- if (!_?.success) {
321
- throw new Error("Something went wrong: with probe");
322
- }
323
- }
324
 
325
- // check/serve seeded file
326
- const seed_file = await (async () => {
327
- if (ops === "cmd") {
328
- return;
329
- }
330
 
331
- const _ = current_job?.db_metadata?.stream_link;
 
 
332
 
333
- if (_) {
334
- return current_job.db_metadata;
335
- }
336
 
337
- const __ = await wide_search_runner({ document: true });
338
-
339
- return __;
340
- })();
341
-
342
- if (seed_file) {
343
- const file_size_bytes = parseFloat(`${seed_file?.size}`);
344
-
345
- const percent = 100;
346
- const transcoded_size = file_size_bytes
347
- ? NS_ffmpeg_v2.F_format_size(file_size_bytes)
348
- : current_job?.transcoded_size;
349
- const progress_endpoint =
350
- current_job?.progress_endpoint || progress_update_endpoint;
351
-
352
- if (!current_job?.db_metadata?.stream_link) {
353
- record_task_info({
354
- job_id,
355
- file_with_path: file_with_path || "",
356
- status: "done",
357
- info: {
358
- title: output_destination,
359
- percent,
360
- transcoded_size,
361
- ...("stream_link" in seed_file && seed_file.stream_link
362
- ? { db_metadata: seed_file }
363
- : {})
364
- }
365
- });
366
- }
367
 
368
- if (ops === "just_check") {
369
- return {
370
- passed: true,
371
- ready: true,
372
- percent,
373
- transcoded_size,
374
- output_destination,
375
- flags: current_job?.flags,
376
- logs: current_job?.logs,
377
- ...(seed_file && "stream_link" in seed_file && seed_file.stream_link
378
- ? { stream_link: seed_file.stream_link }
379
- : {}),
380
- track_process: progress_endpoint,
381
- ws
382
- };
383
- }
384
 
385
- // handle direct file or url redirect
386
- if (seed_file && "stream_link" in seed_file && seed_file.stream_link) {
387
- const stream_link = new URL(seed_file.stream_link);
388
- stream_link.searchParams.set("download", "true");
389
-
390
- const [E_, R_] = await NS_promise.F_catch_promise({
391
- promise: (async () => {
392
- const cloudflare =
393
- await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
394
-
395
- if (!cloudflare.available) return;
396
-
397
- const _ = await NS_pack_proxima.F_handle_pack_encode({
398
- base: new URL(cloudflare.available),
399
- payload: {
400
- endpoint: stream_link.toString(),
401
- commands: {
402
- response_headers: {
403
- set: {
404
- "content-disposition": `attachment; filename="${output_destination}"`
405
- }
406
- },
407
- use_h_version: true
408
- }
409
- }
410
- });
411
 
412
- return { proxy_url: _ };
413
- })()
414
- });
 
 
 
 
415
 
416
- const new_stream_link =
417
- (R_ as any)?.proxy_url || stream_link.toString();
 
418
 
419
- // 302, not 301: the link is proxied/temporary and browsers cache 301s
420
- return ctx.redirect(new_stream_link, 302);
421
- } else {
422
- // if seed is file set headers
423
- construct_response_headers({
424
- is_download: true
425
- });
 
 
 
 
 
 
 
 
 
 
 
426
 
427
- return seed_file;
428
- }
429
- }
430
 
431
- // -------------------------------
432
- // Build flags
433
- // -------------------------------
434
- const CMD = await F_cmd({ subs: !error_end_of_file });
435
 
436
- flags = CMD.flags;
437
 
438
  if (ops === "just_check") {
439
- // run ffprobe to see if playback still valid
440
- const _ = await ping_probe({ playback });
441
-
442
- if (_.success) {
443
- return {
444
- passed: true,
445
- sbe_id,
446
- delivery: output_destination,
447
- qLevel,
448
- picture_quality,
449
- track_process: progress_update_endpoint,
450
- ws,
451
- flags
452
- };
453
- }
454
-
455
  return {
456
  passed: false,
457
  delivery: output_destination,
458
  track_process: progress_update_endpoint,
459
- flags
 
 
460
  };
461
  }
462
 
463
- if (ops === "cmd") {
464
- return {
465
- cmd: flags
466
- };
 
467
  }
 
468
 
469
- // the client gave up while we were preparing
470
- if (controller.signal.aborted) {
 
471
  return;
472
  }
473
 
474
- // -------------------------------
475
- // network response headers
476
- // -------------------------------
477
- construct_response_headers({
478
- is_streaming: is_stream_mode
479
- });
480
 
481
- NS_Misc.console.log({
482
- ENV: "dev",
483
- _browser: false,
484
- _log: ["[FFMPEG FLAGS]", flags, "\n"]
485
- });
486
 
487
- claimed = true;
 
 
 
 
488
 
489
- // -------------------------------
490
- // spawn
491
- // -------------------------------
492
- if (!is_stream_mode) {
 
 
 
 
493
  record_task_info({
494
  job_id,
495
  file_with_path: file_with_path || "",
496
- status: "running",
497
  info: {
498
  title: output_destination,
499
- ws,
500
- progress_endpoint: progress_update_endpoint,
501
- flags
 
 
502
  }
503
  });
504
  }
505
 
506
- run({ ii: 0 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
 
508
- live.launched = true;
 
 
 
 
 
 
509
 
510
- if (is_stream_mode) {
511
- return passthrough;
512
  }
 
513
 
514
- // download mode: the job lives on its own from here on
515
- detach_request_abort();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
 
517
- const message = `COMPILER ENGINE started, this may take a while. ${progress_update_endpoint ? `check process -> ${progress_update_endpoint}` : "check back later"}`;
 
 
 
 
 
 
518
 
 
519
  return {
520
- message,
521
- title: output_destination,
522
- ws,
523
- track_process: progress_update_endpoint
524
  };
525
- } finally {
526
- // any early return or throw above means ffmpeg was never started
527
- if (!live.launched) {
528
- detach_request_abort();
529
 
530
- // drop our claim, but never touch another request's job or a done/failed record
531
- if (claimed && transcode_jobs.get(job_id)?.status === "running") {
532
- transcode_jobs.delete(job_id);
533
- }
534
- }
535
  }
536
 
537
- // ---------- ffmpeg lifecycle (hoisted) ----------
 
 
 
 
 
538
 
539
- /**
540
- * Starts (or restarts) ffmpeg. Never throws, failures go through fail().
541
- */
542
- async function run(arg0: {
543
- ii: number;
544
- keep_subs?: boolean;
545
- logs?: string[];
546
- }) {
547
- const { ii, keep_subs, logs } = arg0;
548
 
549
- if (ii >= MAX_ATTEMPTS) return;
550
 
551
- try {
552
- if (logs) id_log_errors({ logs });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
 
554
- if (ii > 0) {
555
- const _CMD = await F_cmd({ subs: keep_subs });
556
 
557
- flags = _CMD.flags;
558
 
559
- if (controller.signal.aborted) controller = new AbortController();
 
 
560
 
561
- progress_data = undefined;
562
- seek_size_bytes = 0;
563
- live.percent = 0;
564
- live.size = "";
565
- local_cleaned = false;
566
- }
567
 
568
- start_ffmpeg({ ii });
569
- } catch (err) {
570
- console.warn("spawn run error:", err, "\n");
571
 
572
- fail([err_message(err)]);
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  }
574
  }
 
575
 
576
- function start_ffmpeg(arg0: { ii: number }) {
577
- const { ii } = arg0;
578
 
579
- const my_attempt = ++attempt;
 
 
 
 
 
 
 
 
580
 
581
- let settled = false;
582
 
583
- logged_proc_errors = [];
584
- logged_proc_info = [];
585
 
586
- const push_log = (arr: string[], line: string) => {
587
- arr.push(line);
588
 
589
- if (arr.length > MAX_LOG_LINES) {
590
- arr.splice(0, arr.length - MAX_LOG_LINES);
591
- }
592
- };
593
 
594
- let counter: Transform | undefined;
595
 
596
- if (use_s3_pipe) {
597
- s3_bytes = 0;
 
 
 
 
598
 
599
- const c = new Transform({
600
- transform(chunk, _enc, cb) {
601
- s3_bytes += chunk.length;
602
 
603
- cb(null, chunk);
604
- }
605
- });
606
 
607
- c.on("error", () => {}); // real errors surface through `done`
 
608
 
609
- const upload = NS_bucket.makeS3Upload({
610
- key: bucket_file_path,
611
- body: c,
612
- ContentType: out_mime
613
- });
614
 
615
- const done: Promise<S3Outcome> = upload.done().then(
616
- () => ({ success: true as const }),
617
- (error) => {
618
- console.warn("S3:ERROR", bucket_file_path, error, "\n");
619
 
620
- // the upload died, ffmpeg's output has nowhere to go: stop it
621
- if (my_attempt === attempt) kill_controller("bucket upload failed");
622
 
623
- return { success: false as const, error };
624
- }
625
- );
626
 
627
- s3_attempt = { upload, done, counter: c };
628
- counter = c;
629
  }
 
630
 
631
- // watchdog: kill ffmpeg if it never produces any output
632
- set_t_timeout();
633
 
634
- const proc = spawn(NS_ffmpeg_v2.ffmpeg_path, flags, {
635
- signal: controller.signal
636
- });
637
 
638
- if (passthrough) proc.stdout.pipe(passthrough);
 
 
639
 
640
- if (counter) {
641
- proc.stdout.on("error", () => {}); // EPIPE if the upload is torn down
642
- proc.stdout.pipe(counter);
643
- }
644
 
645
- proc.stderr.on("data", (data) => {
646
- if (my_attempt !== attempt) return;
647
 
648
- kill_controller_timeout();
 
 
 
 
649
 
650
- const log = data.toString().trim();
 
 
 
651
 
652
- if (is_dev) console.log("💠", log, "\n");
 
653
 
654
- if (!log) return;
 
 
655
 
656
- push_log(logged_proc_info, log);
 
 
657
 
658
- if (
659
- NS_ffmpeg_v2.proc_error_pattern().some((pattern) => pattern.test(log))
660
- ) {
661
- push_log(logged_proc_errors, log);
662
 
663
- return;
664
- }
 
665
 
666
- const parsed = F_stderr_parse_progress({
667
- stderr_line: log,
668
- full_duration: parseFloat(`${probe_metadata.format.duration}`),
669
- seek_size_bytes
670
- });
671
 
672
- if (!parsed || !Object.keys(parsed).length) return;
 
 
 
673
 
674
- progress_data = parsed;
 
675
 
676
- seek_size_bytes = parseFloat(`${parsed.size_bytes}`) || 0;
677
 
678
- live.size = parsed.size || live.size;
679
 
680
- // S3 PIPE: ffmpeg's own size can be N/A on a pipe, the counter is exact
681
- if (use_s3_pipe && s3_bytes > 0) {
682
- live.size = NS_ffmpeg_v2.F_format_size(s3_bytes) || live.size;
683
- }
684
 
685
- if (typeof parsed.percent === "number") {
686
- live.percent = Math.min(99, parsed.percent);
687
- }
688
 
689
- push_progress({});
690
- });
691
 
692
- proc.on("close", (code) => settle({ code }));
 
 
 
693
 
694
- proc.on("error", (err) => {
695
- console.error("⚠️❌ FFmpeg spawn error:", err, "\n");
696
 
697
- if (proc.pid === undefined) settle({ code: null, err });
 
 
 
698
  });
699
 
700
- function settle(arg0: { code: number | null; err?: Error }) {
701
- if (settled) return;
702
 
703
- settled = true;
704
 
705
- kill_controller_timeout();
706
 
707
- on_ffmpeg_end({ ii, ...arg0 }).catch((e) => {
708
- console.warn("on_ffmpeg_end error:", e, "\n");
709
 
710
- fail([err_message(e)]);
711
- });
 
712
  }
713
- }
714
 
715
- async function on_ffmpeg_end(arg0: {
716
- ii: number;
717
- code: number | null;
718
- err?: Error;
719
- }) {
720
- const { ii, code, err } = arg0;
721
 
722
- // S3 PIPE: the upload finishes just after ffmpeg's stdout closes. Wait
723
- // for it, success needs BOTH ffmpeg and the upload to have succeeded.
724
- const s3_result = s3_attempt ? await s3_attempt.done : undefined;
725
 
726
- const percent = progress_data?.percent;
727
 
728
- const failure_reason = (() => {
729
- if (err) return `FFmpeg could not start: ${err.message}`;
730
 
731
- if (s3_result && !s3_result.success) {
732
- return `Bucket upload failed: ${err_message(s3_result.error)}`;
733
- }
734
 
735
- if (code !== 0) {
736
- return controller.signal.aborted
737
- ? "FFmpeg was stopped (timeout or abort)"
738
- : `FFmpeg exited with code ${code}`;
739
- }
740
 
741
- if (typeof percent === "number" && percent > 0 && percent < 90) {
742
- return "Incomplete media data";
743
- }
744
- })();
745
 
746
- if (failure_reason) {
747
- console.error(`⚠️❌ ${failure_reason}`, "\n");
748
- console.error("Error details:", logged_proc_errors, "\n");
749
 
750
- // S3 PIPE: a truncated media is still "playable", so the half-written object must go. Wait for the delete: a retry reuses the same key.
751
- await abort_s3_upload();
752
 
753
- cleanup_local();
 
 
 
754
 
755
- const will_retry =
756
- is_download_mode && !live.user_aborted && ii + 1 < MAX_ATTEMPTS;
 
 
 
 
757
 
758
- if (will_retry) {
759
- let i = ii + 1;
 
760
 
761
- if (is_dev) console.log(`WILL RETRY: ${i}`, "\n");
762
 
763
- run({
764
- ii: i,
765
- keep_subs: F_keep_subs(),
766
- logs: logged_proc_info
767
- });
768
 
769
- return;
770
- }
 
771
 
772
- fail([failure_reason, ...logged_proc_errors]);
 
 
 
 
773
 
774
- return;
 
775
  }
 
776
 
777
- NS_Misc.console.log({
778
- ENV: "all",
779
- _browser: false,
780
- _log: [
781
- `🏁 FFMPEG finished: (${percent}) -> ${progress_data?.size} ${file_with_path || output_destination}`,
782
- "\n"
783
- ]
784
- });
785
 
786
- if (is_stream_mode) {
787
- // finish_done();
788
 
789
- return;
790
- }
791
 
792
- await deliver_download();
793
- }
794
 
795
- /**
796
- * ffmpeg finished successfully.
797
- S3 pipe: the object is already in the bucket.
798
- Disk: upload the local file, then delete it whether the upload worked or not.
799
- */
800
- async function deliver_download() {
801
- if (use_s3_pipe) {
802
- s3_attempt = undefined;
803
 
804
- live.size = NS_ffmpeg_v2.F_format_size(s3_bytes) || live.size;
805
-
806
- live.percent = 99;
807
- push_progress({ force: true });
808
 
809
- await document_and_finish();
 
 
 
 
810
 
811
  return;
812
  }
813
 
814
- if (!file_with_path) {
815
- fail(["No local output file"]);
816
 
817
- return;
818
- }
819
 
820
- if (is_dev) {
821
- finish_done();
 
 
 
 
 
 
822
 
823
- return;
824
- }
825
 
826
- live.percent = 99;
827
- push_progress({ force: true, uploading: true });
828
 
829
- let uploaded = false;
 
830
 
831
- try {
832
- uploaded = await upload_with_retry({ file_with_path });
833
- } catch (e) {
834
- console.warn("bucket upload error:", e, "\n");
835
- } finally {
836
- cleanup_local();
837
- }
 
838
 
839
- if (!uploaded) {
840
- fail(["Bucket upload failed"]);
841
 
842
- return;
843
- }
844
 
845
  await document_and_finish();
 
 
846
  }
847
 
848
- async function document_and_finish() {
849
- // document file
850
- await wide_search_runner({
851
- document: true,
852
- skip: {
853
- local: true,
854
- db: true
855
- }
856
- }).catch(() => {});
857
 
 
858
  finish_done();
 
 
859
  }
860
 
861
- async function upload_with_retry(arg0: { file_with_path: string }) {
862
- const { file_with_path } = arg0;
863
 
864
- if (typeof file_with_path !== "string" || !file_with_path.trim().length) {
865
- return false;
866
- }
867
 
868
- const delays = [0, 2000, 5000];
 
 
 
 
 
 
869
 
870
- for (let i = 0; i < delays.length; i++) {
871
- if (delays[i]) await Bun.sleep(delays[i]!);
872
 
873
- const r = await NS_bucket.upload_to_bucket({
874
- job_id,
875
- bucket_path: bucket_file_path,
876
- file_with_path,
877
- REQ0,
878
- skip: { probe: i > 0 }
879
- });
880
 
881
- if (r?.success) return true;
 
882
 
883
- if (r && !r.success && r.reason === "probe") return false;
 
 
 
 
 
 
884
  }
 
 
 
 
885
 
 
 
 
 
886
  return false;
887
  }
888
 
889
- /*
890
- S3 PIPE: tears down the current attempt's upload and deletes whatever
891
- reached the bucket. Idempotent, and safe when there is no upload.
892
- */
893
- async function abort_s3_upload() {
894
- const a = s3_attempt;
895
 
896
- if (!a) return;
 
897
 
898
- s3_attempt = undefined;
 
 
 
 
 
 
899
 
900
- a.counter.destroy();
901
 
902
- await NS_bucket.delete_self_s3({
903
- key: bucket_file_path,
904
- upload_in: a.upload,
905
- done: a.done
906
- });
907
  }
908
 
909
- // ---------- reporting (hoisted) ----------
 
910
 
911
- function relay_endpoint(data: Record<string, any>) {
912
- if (!progress_update_endpoint) return;
 
 
 
 
913
 
914
- try {
915
- relay_progress_info({
916
- endpoint: new URL(progress_update_endpoint),
917
- progress_data: data
918
- });
919
- } catch {}
920
- }
921
 
922
- function push_progress(arg0: { force?: boolean, uploading?: boolean }) {
923
- if (live.done || live.failed || is_stream_mode) return;
924
 
925
- // each push hits the store, the websocket and an outbound HTTP relay,
926
- // so cap it. Completion always goes through finish_done.
927
- const now = Date.now();
928
 
929
- if (!arg0.force && now - live.last_push < 1500) return;
 
 
 
 
 
930
 
931
- live.last_push = now;
932
 
933
- record_task_info({
934
- job_id,
935
- file_with_path: file_with_path || "",
936
- status: "running",
937
- info: {
938
- uploading: arg0.uploading,
939
- title: output_destination,
940
- percent: live.percent,
941
- transcoded_size: live.size,
942
- progress_endpoint: progress_update_endpoint,
943
- ws,
944
- flags,
945
- log_errors: logged_proc_errors,
946
- logs: logged_proc_info
947
- }
948
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
949
 
950
- const data = {
951
- ...progress_data,
 
 
 
952
  uploading: arg0.uploading,
 
953
  percent: live.percent,
954
- size: live.size || progress_data?.size,
955
- filename: output_destination,
956
- location: `${REQ0.origin}/${REQ0.pathname}`,
 
957
  log_errors: logged_proc_errors,
958
  logs: logged_proc_info
959
- };
 
960
 
961
- relay_progress_info_ws({
962
- channel: data_key,
963
- payload: {
964
- channel: data_key,
965
- status: "running",
966
- data
967
- }
968
- });
 
 
969
 
970
- relay_endpoint(data);
971
- }
 
 
 
 
 
 
972
 
973
- function finish_done() {
974
- if (live.done || live.failed || is_stream_mode) return;
975
 
976
- live.done = true;
977
- live.percent = 100;
978
 
979
- record_task_info({
980
- job_id,
981
- file_with_path: file_with_path || "",
982
- status: "done",
983
- info: {
984
- title: output_destination,
985
- percent: 100,
986
- transcoded_size: live.size || progress_data?.size,
987
- progress_endpoint: progress_update_endpoint,
988
- flags,
989
- log_errors: logged_proc_errors,
990
- logs: logged_proc_info
991
- }
992
- });
993
 
994
- relay_progress_info_ws({
995
- channel: data_key,
996
- payload: {
997
- channel: data_key,
998
- status: "done",
999
- filename: output_destination,
1000
- logs: logged_proc_info
1001
- }
1002
- });
1003
-
1004
- relay_endpoint({
1005
  percent: 100,
1006
- filename: output_destination,
1007
- location: `${REQ0.origin}/${REQ0.pathname}`,
 
1008
  log_errors: logged_proc_errors,
1009
  logs: logged_proc_info
1010
- });
 
1011
 
1012
- if (is_download_mode) {
1013
- transcode_jobs.delete(job_id);
 
 
 
 
 
1014
  }
 
 
 
 
 
 
 
 
 
 
 
 
1015
  }
 
1016
 
1017
- function fail(logs: string[]) {
1018
- if (live.failed || live.done || is_stream_mode) return;
1019
 
1020
- live.failed = true;
1021
 
1022
- record_task_info({
1023
- job_id,
1024
- file_with_path: file_with_path || "",
1025
- status: "failed",
1026
- info: {
1027
- title: output_destination,
1028
- ws,
1029
- error: logs.join(" | "),
1030
- log_errors: logged_proc_errors,
1031
- logs: logged_proc_info
1032
- }
1033
- });
1034
 
1035
- relay_progress_info_ws({
 
 
1036
  channel: data_key,
1037
- payload: {
1038
- channel: data_key,
1039
- status: "failed",
1040
- filename: output_destination,
1041
- logs: logged_proc_info
1042
- }
1043
- });
1044
-
1045
- relay_endpoint({
1046
  status: "failed",
1047
  filename: output_destination,
1048
- location: `${REQ0.origin}/${REQ0.pathname}`,
1049
- log_errors: logged_proc_errors,
1050
  logs: logged_proc_info
1051
- });
 
1052
 
1053
- passthrough?.destroy(new Error(logs[0] || "failed"));
 
 
 
 
 
 
1054
 
1055
- cleanup_local();
1056
 
1057
- // S3 PIPE: covers failures that did not come through on_ffmpeg_end
1058
- // (spawn/run errors). A no-op when the upload was already torn down.
1059
- abort_s3_upload().catch((e) => console.warn("abort_s3_upload:", e));
1060
- }
1061
 
1062
- // ---------- helpers (hoisted) ----------
 
 
 
1063
 
1064
- /**
1065
- * Deletes the local output file. Safe to call from every exit path: it
1066
- * only runs once per attempt. Skipped in dev, where the local file is the
1067
- * store. No-op in S3 pipe mode, there is no local file.
1068
- */
1069
- function cleanup_local() {
1070
- if (!file_with_path || local_cleaned) return;
1071
 
1072
- if (is_dev && live.done) return;
 
 
 
 
 
 
1073
 
1074
- local_cleaned = true;
1075
 
1076
- NS_bucket.delete_self({ try_count: 0, file_with_path });
1077
- }
1078
 
1079
- function kill_controller(reason: string) {
1080
- controller.abort();
1081
 
1082
- console.log(`[CONTROLLER] aborted: ${reason}`);
1083
- }
1084
 
1085
- function set_t_timeout() {
1086
- kill_controller_timeout();
1087
 
1088
- watchdog = setTimeout(
1089
- () => kill_controller("no output from ffmpeg"),
1090
- NS_ffmpeg_v2.TIMEOUT_MS
1091
- );
1092
- }
1093
 
1094
- function kill_controller_timeout() {
1095
- if (watchdog) {
1096
- clearTimeout(watchdog);
1097
- watchdog = undefined;
1098
- }
 
 
 
 
 
1099
  }
 
1100
 
1101
- function id_log_errors(arg0: { logs: string[] }) {
1102
- const { logs } = arg0;
1103
 
1104
- if (!Array.isArray(logs)) {
1105
- return;
1106
- }
1107
 
1108
- const has = (needle: string) => {
1109
- return logs.some((xx) =>
1110
- xx.toString().toLowerCase().includes(needle.toLowerCase())
1111
- );
1112
- };
1113
 
1114
- if (has("http error 429 too many requests")) {
1115
- error_429 = true;
1116
- }
1117
 
1118
- if (has("failed too many times, skipping")) {
1119
- error_429 = true;
1120
- }
1121
 
1122
- if (has("error opening input: end of file")) {
1123
- error_end_of_file = true;
1124
- }
1125
 
1126
- if (has("error opening input files: server returned 403 forbidden")) {
1127
- error_403 = true;
1128
- }
1129
 
1130
- if (has("http error 404 not found")) {
1131
- error_403 = true;
1132
- }
1133
  }
 
1134
 
1135
- // keep subtitles unless one of THEIR urls caused the end-of-file error
1136
- function F_keep_subs() {
1137
- if (!logged_proc_info.length || !subtitles?.length) {
1138
- return true;
1139
- }
1140
 
1141
- const eof_lines = logged_proc_info
1142
- .map((xx) => xx.toString().toLowerCase())
1143
- .filter((xx) => xx.includes("error opening input: end of file"));
1144
 
1145
- if (!eof_lines.length) {
1146
- return true;
1147
- }
1148
 
1149
- const sub_failed = subtitles.some((sub) => {
1150
- const url = sub.url.toString().toLowerCase();
1151
 
1152
- return eof_lines.some((line) => line.includes(url));
1153
- });
1154
 
1155
- return !sub_failed;
1156
- }
1157
 
1158
- async function F_cmd(arg0: { subs?: boolean }) {
1159
- const { subs } = arg0;
1160
 
1161
- const new_playback = await (async () => {
1162
- if ((!error_429 && !error_403) || playback.url.includes("/proxima")) {
1163
- return playback;
1164
- }
1165
 
1166
- // get_hf_sia_proxy
1167
- const R_ = IS_HF()
1168
- ? await NS_scrape_post_processing_helper.get_cloudflare_proxy({})
1169
- : await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
1170
 
1171
- if (!R_) return playback;
1172
 
1173
- const proxy_base = R_.available || R_.fallback;
1174
 
1175
- if (NS_routes_c.F_is_url(proxy_base)) {
1176
- const _ = await NS_pack_proxima.F_handle_pack_encode({
1177
- base: proxy_base,
1178
- payload: {
1179
- endpoint: playback.url,
1180
- commands: {
1181
- use_h_version: true,
1182
- redirect_origin: true,
1183
- headers: playback.headers
1184
- }
1185
  }
1186
- });
 
1187
 
1188
- if (_) {
1189
- const __ = { ...playback };
1190
 
1191
- __.url = _.toString();
1192
 
1193
- delete __.headers;
1194
 
1195
- return __;
1196
- }
1197
  }
 
1198
 
1199
- return playback;
1200
- })();
1201
-
1202
- has_subs = subs === false ? false : true;
1203
-
1204
- const data = await NS_ffmpeg_v2.F_build_ffmpeg_flags_t1({
1205
- playback: new_playback,
1206
- subtitles: subs === false ? undefined : subtitles,
1207
- probe_metadata,
1208
- delivery_format,
1209
- delivery_mode,
1210
- output_destination: file_with_path || "",
1211
- flash_ping: false,
1212
- dl_to_s3: use_s3_pipe
1213
- });
1214
 
1215
- return data;
1216
- }
 
 
 
 
 
 
 
 
 
 
1217
 
1218
- function construct_response_headers(arg0: {
1219
- is_download?: boolean;
1220
- is_streaming?: boolean;
1221
- }) {
1222
- const { is_streaming, is_download } = arg0;
1223
 
1224
- /* ---------- set response headers ---------- */
1225
- if (is_download || is_streaming) {
1226
- ctx.set.headers["content-type"] = out_mime;
1227
- }
 
1228
 
1229
- ctx.set.headers["cache-control"] = "no-cache";
 
 
 
1230
 
1231
- if (is_download) {
1232
- ctx.set.headers["content-disposition"] =
1233
- `attachment; filename="${output_destination}"`;
1234
 
1235
- ctx.set.headers["accept-ranges"] = "bytes";
1236
- }
 
1237
 
1238
- if (clean_metadata?.format?.duration) {
1239
- ctx.set.headers["X-Content-Duration"] = clean_metadata.format?.duration;
1240
- }
1241
  }
1242
 
1243
- async function wide_search_runner(arg0: {
1244
- skip?: {
1245
- local?: boolean;
1246
- db?: boolean;
1247
- bucket?: boolean;
1248
- };
1249
- document: boolean;
1250
- }) {
1251
- const { skip, document } = arg0;
1252
-
1253
- if (!is_download_mode) return;
1254
 
1255
- const [E_, R_] = await NS_promise.F_catch_promise({
1256
- promise: NS_showtime_v2_helper.find_seed_file({
1257
- sbe_id,
1258
- output_destination: {
1259
- og: output_destination,
1260
- clean: output_destination_clean,
1261
- file_with_path
1262
- },
1263
- picture_quality,
1264
- qLevel,
1265
- sbe_s_e,
1266
- cache_folder,
1267
- file_id: clean_file_id,
1268
- job_id,
1269
- S3Dir,
1270
- cleanup_corruption: true,
1271
- document,
1272
- ideal_runtime: playback.ideal_runtime,
1273
- skip,
1274
- has_subs
1275
- })
1276
- });
 
 
 
 
 
 
 
 
 
 
 
 
1277
 
1278
- if (R_ && "stream_link" in R_ && R_.stream_link) {
1279
- if (!NS_misc.isValidUrlFormat(R_.stream_link)) {
1280
- return;
1281
- }
1282
  }
1283
-
1284
- return R_;
1285
  }
1286
- },
1287
- {
1288
- params: t.Object({
1289
- data_key: t.String({
1290
- minLength: 1,
1291
- error() {
1292
- return "Invalid process data, Expected string";
1293
- }
1294
- })
1295
- }),
1296
- query: t.Object({
1297
- raw: t.Optional(
1298
- t.Number({
1299
- minimum: 0,
1300
- maximum: 1,
1301
- error: () => "Invalid raw, Expected binary boolean"
1302
- })
1303
- ),
1304
- ops: t.Optional(
1305
- t.Union(
1306
- [
1307
- t.Literal("cmd"),
1308
- t.Literal("raw"),
1309
- t.Literal("decode"),
1310
- t.Literal("just_check")
1311
- ],
1312
- {
1313
- error() {
1314
- return "Invalid operation, must be one of: cmd, raw, decode";
1315
- }
1316
- }
1317
- )
1318
- )
1319
- })
1320
- }
1321
- );
1322
 
1323
- export const ShowtimeV2Router = sub_router;
 
 
1324
 
1325
  function F_stderr_parse_progress(arg0: {
1326
  stderr_line: string;
 
45
  const HF_BUCKET_NAME = () =>
46
  NS_runtime.ENV_VAR_DB?.HF_BUCKET_NAME || process.env.HF_BUCKET_NAME;
47
 
48
+ const is_dev = (() => {
49
+ // return false;
50
+ return APP_ENV() === "dev";
51
+ })();
52
+
53
  const sub_router = new Elysia({ prefix: "/showtimeV2" }).get(
54
  "/delivery/:data_key",
55
  async (ctx) => {
 
60
  const REQ0 = new URL(request.url);
61
  const REQ_ORIGIN = (ctx as any).publicOrigin || REQ0.origin;
62
 
 
 
 
 
 
63
  const metadata = await (async () => {
64
  try {
65
  if (is_dev) {
 
85
  return metadata;
86
  }
87
 
88
+ const [E_, R_] = await NS_promise.F_catch_promise({
89
+ promise: handle_data({
90
+ data_key,
91
+ metadata,
92
+ REQ0,
93
+ REQ_ORIGIN,
94
+ ctx: ctx as any,
95
+ ops
96
+ })
97
+ });
 
98
 
99
+ if (E_) {
100
+ const { message, status } = NS_error.F_collect_error_status_n_message({
101
+ ERR: E_
102
+ });
103
 
104
+ return ctx.status(status, message);
 
105
  }
106
 
107
+ return R_;
108
+ },
109
+ {
110
+ params: t.Object({
111
+ data_key: t.String({
112
+ minLength: 1,
113
+ error() {
114
+ return "Invalid process data, Expected string";
115
+ }
116
+ })
117
+ }),
118
+ query: t.Object({
119
+ raw: t.Optional(
120
+ t.Number({
121
+ minimum: 0,
122
+ maximum: 1,
123
+ error: () => "Invalid raw, Expected binary boolean"
124
+ })
125
+ ),
126
+ ops: t.Optional(
127
+ t.Union(
128
+ [
129
+ t.Literal("cmd"),
130
+ t.Literal("raw"),
131
+ t.Literal("decode"),
132
+ t.Literal("just_check")
133
+ ],
134
+ {
135
+ error() {
136
+ return "Invalid operation, must be one of: cmd, raw, decode";
137
+ }
138
+ }
139
+ )
140
+ )
141
+ })
142
+ }
143
+ );
144
 
145
+ export const ShowtimeV2Router = sub_router;
146
+ export const ShowtimeV2RouterHandlePayload = handle_data;
147
+
148
+ async function handle_data(arg0: {
149
+ data_key: string;
150
+ metadata: T_delivery_payload;
151
+ REQ0: URL;
152
+ REQ_ORIGIN: string;
153
+ ctx: Context;
154
+ ops?: string;
155
+ throw_error?: boolean;
156
+ }) {
157
+ const { data_key, metadata, REQ0, REQ_ORIGIN, ctx, ops, throw_error } = arg0;
158
 
159
+ const {
160
+ playback,
161
+ probe_metadata,
162
+ output_destination,
163
+ sbe_id,
164
+ delivery_mode,
165
+ delivery_format,
166
+ out_mime,
167
+ progress_update_endpoint,
168
+ subtitles
169
+ } = metadata as T_delivery_payload;
170
 
171
+ const is_download_mode = delivery_mode === "download";
172
+ const is_stream_mode = delivery_mode === "stream";
173
 
174
+ if (!is_download_mode && !is_stream_mode) {
175
+ const message = "Unsupported delivery mode";
 
176
 
177
+ if (throw_error) throw Error(JSON.stringify(message));
 
178
 
179
+ return ctx.status(400, message);
180
+ }
 
 
 
 
 
181
 
182
+ const clean_metadata = NS_ffmpeg_v2.F_scan_raw_probe_metadata({
183
+ metadata: probe_metadata
184
+ });
185
 
186
+ const picture_quality = (() => {
187
+ const _ = playback.picture_quality;
188
 
189
+ if (typeof _ !== "string") {
190
+ return "";
191
+ }
192
 
193
+ return _.trim().split(" ").join("");
194
+ })();
195
 
196
+ const sbe_s_e = (() => {
197
+ const _ = metadata.sbe_s_e;
198
 
199
+ if (typeof _ !== "string" || _.toLowerCase().includes("nan")) {
200
+ return;
201
+ }
 
202
 
203
+ return _.trim().split(" ").join("");
204
+ })();
205
 
206
+ const qLevel = (() => {
207
+ if (
208
+ typeof metadata.qLevel === "string" &&
209
+ metadata.qLevel.trim().length > 0
210
+ ) {
211
+ return metadata.qLevel.trim();
212
+ }
213
 
214
+ const _ = NS_ffmpeg_v2.F_get_qLevel({
215
+ metadata: probe_metadata
216
+ });
217
 
218
+ return _;
219
+ })();
220
 
221
+ const ws = (() => {
222
+ const is_https = new URL(REQ_ORIGIN).protocol === "https:";
223
 
224
+ const ws_protocol = is_https ? "wss" : "ws";
225
 
226
+ const _ = `${ws_protocol}://${REQ0.host}/ws`;
227
 
228
+ const data = {
229
+ connect: _,
230
+ channel: data_key
231
+ };
232
 
233
+ return data;
234
+ })();
235
 
236
+ const output_destination_clean = NS_misc.cleanNameV2(output_destination);
 
237
 
238
+ const clean_file_id =
239
+ `${picture_quality}-${output_destination_clean}`.toLowerCase();
 
 
240
 
241
+ const job_id = `${sbe_id}-${clean_file_id}`;
 
242
 
243
+ const file_id = `${Date.now()}-${clean_file_id}`;
 
244
 
245
+ const cache_folder = `cache/${sbe_id}`;
 
 
246
 
247
+ const S3Dir = path.join(STORAGE_ROOT(), `${cache_folder}`);
 
 
248
 
249
+ // same key the disk flow uploads to, so the seed lookup finds either one
250
+ const bucket_file_path = `${cache_folder}/${file_id}`;
251
 
252
+ // const dl_to_s3 = is_dev || IS_RENDER_COM();
253
+ const dl_to_s3 = IS_RENDER_COM();
254
 
255
+ // S3 PIPE: only download mode pipes to S3, stream mode always goes to the client
256
+ const use_s3_pipe = is_download_mode && dl_to_s3;
257
 
258
+ // S3 PIPE: fail fast instead of finding out after ffmpeg has started
259
+ if (use_s3_pipe && (!S3() || !HF_BUCKET_NAME())) {
260
+ const message = "S3 bucket is not configured";
 
 
 
 
 
 
261
 
262
+ if (throw_error) throw Error(JSON.stringify(message));
 
 
 
 
 
 
 
 
263
 
264
+ return ctx.status(500, message);
265
+ }
 
266
 
267
+ const file_with_path = (() => {
268
+ if (!is_download_mode || use_s3_pipe) return;
269
 
270
+ try {
271
+ node_fs.mkdirSync(S3Dir, { recursive: true });
272
 
273
+ return path.join(S3Dir, file_id);
274
+ } catch (e) {}
275
+ })();
276
 
277
+ if (is_download_mode && !use_s3_pipe && !file_with_path) {
278
+ const message = "Could not prepare the output directory";
279
 
280
+ if (throw_error) throw Error(JSON.stringify(message));
 
281
 
282
+ return ctx.status(500, "Could not prepare the output directory");
283
+ }
284
 
285
+ const MAX_ATTEMPTS = 2;
 
286
 
287
+ const MAX_LOG_LINES = 200;
 
 
288
 
289
+ const err_message = (err: unknown) =>
290
+ NS_error.F_collect_error_message({ ERR: err }).message;
 
291
 
292
+ const live = {
293
+ percent: 0,
294
+ size: "",
295
+ failed: false,
296
+ done: false,
297
+ launched: false,
298
+ user_aborted: false,
299
+ last_push: 0
300
+ };
301
 
302
+ let flags: string[] = [];
303
+ let progress_data: Record<string, any> | undefined = undefined;
304
+ let seek_size_bytes = 0;
305
+ let logged_proc_errors: string[] = [];
306
+ let logged_proc_info: string[] = [];
307
+ let has_subs = false;
308
+ let local_cleaned = false;
309
+ let attempt = 0;
310
+ let watchdog: ReturnType<typeof setTimeout> | undefined = undefined;
311
 
312
+ let error_429 = false;
313
+ let error_end_of_file = false;
314
+ let error_403 = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
+ let claimed = false;
 
317
 
318
+ // S3 PIPE: state of the current attempt's upload
319
+ type S3Outcome = { success: true } | { success: false; error: unknown };
320
 
321
+ let s3_attempt:
322
+ | { upload: Upload; done: Promise<S3Outcome>; counter: Transform }
323
+ | undefined = undefined;
324
 
325
+ let s3_bytes = 0;
 
 
 
 
 
 
 
 
 
326
 
327
+ let controller = new AbortController();
328
+ const abort_signal = ctx?.request.signal;
329
 
330
+ const on_request_abort = () => {
331
+ live.user_aborted = true;
 
 
332
 
333
+ kill_controller("client aborted");
334
+ };
 
 
 
335
 
336
+ const detach_request_abort = () => {
337
+ abort_signal?.removeEventListener("abort", on_request_abort);
338
+ };
339
 
340
+ if (abort_signal?.aborted) {
341
+ return;
342
+ }
343
 
344
+ abort_signal?.addEventListener("abort", on_request_abort, { once: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
+ const passthrough = is_stream_mode ? new PassThrough() : undefined;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
+ try {
349
+ const current_job = transcode_jobs.get(job_id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
 
351
+ if (current_job?.status === "running") {
352
+ const percent = current_job?.percent;
353
+ const uploading = current_job?.uploading;
354
+ const percentage = typeof percent === "number" ? `${percent} %` : "";
355
+ const transcoded_size = current_job?.transcoded_size;
356
+ const progress_endpoint =
357
+ current_job?.progress_endpoint || progress_update_endpoint;
358
 
359
+ const message = percentage
360
+ ? `ENGINE PROGRESS: ${percentage}`
361
+ : "ENGINE PROCESSING ...";
362
 
363
+ // if(!uploading)
364
+ return {
365
+ processing: true,
366
+ message,
367
+ percent,
368
+ transcoded_size,
369
+ track_process: progress_endpoint,
370
+ file: output_destination,
371
+ job_id,
372
+ output_destination,
373
+ qLevel,
374
+ picture_quality,
375
+ ws,
376
+ flags: current_job.flags,
377
+ logs: current_job.logs,
378
+ ...(ops === "just_check" ? { passed: true } : {})
379
+ };
380
+ }
381
 
382
+ if (current_job?.status === "failed") {
383
+ const logs = current_job?.logs;
 
384
 
385
+ if (logs) id_log_errors({ logs });
 
 
 
386
 
387
+ transcode_jobs.delete(job_id);
388
 
389
  if (ops === "just_check") {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  return {
391
  passed: false,
392
  delivery: output_destination,
393
  track_process: progress_update_endpoint,
394
+ ws,
395
+ flags: current_job.flags,
396
+ logs: current_job?.logs
397
  };
398
  }
399
 
400
+ // run ffprobe to see if playback still valid
401
+ const _ = await ping_probe({ playback });
402
+
403
+ if (!_?.success) {
404
+ throw new Error("Something went wrong: with probe");
405
  }
406
+ }
407
 
408
+ // check/serve seeded file
409
+ const seed_file = await (async () => {
410
+ if (ops === "cmd") {
411
  return;
412
  }
413
 
414
+ const _ = current_job?.db_metadata?.stream_link;
 
 
 
 
 
415
 
416
+ if (_) {
417
+ return current_job.db_metadata;
418
+ }
419
+
420
+ const __ = await wide_search_runner({ document: true });
421
 
422
+ return __;
423
+ })();
424
+
425
+ if (seed_file) {
426
+ const file_size_bytes = parseFloat(`${seed_file?.size}`);
427
 
428
+ const percent = 100;
429
+ const transcoded_size = file_size_bytes
430
+ ? NS_ffmpeg_v2.F_format_size(file_size_bytes)
431
+ : current_job?.transcoded_size;
432
+ const progress_endpoint =
433
+ current_job?.progress_endpoint || progress_update_endpoint;
434
+
435
+ if (!current_job?.db_metadata?.stream_link) {
436
  record_task_info({
437
  job_id,
438
  file_with_path: file_with_path || "",
439
+ status: "done",
440
  info: {
441
  title: output_destination,
442
+ percent,
443
+ transcoded_size,
444
+ ...("stream_link" in seed_file && seed_file.stream_link
445
+ ? { db_metadata: seed_file }
446
+ : {})
447
  }
448
  });
449
  }
450
 
451
+ if (ops === "just_check") {
452
+ return {
453
+ passed: true,
454
+ ready: true,
455
+ percent,
456
+ transcoded_size,
457
+ output_destination,
458
+ flags: current_job?.flags,
459
+ logs: current_job?.logs,
460
+ ...(seed_file && "stream_link" in seed_file && seed_file.stream_link
461
+ ? { stream_link: seed_file.stream_link }
462
+ : {}),
463
+ track_process: progress_endpoint,
464
+ ws
465
+ };
466
+ }
467
+
468
+ // handle direct file or url redirect
469
+ if (seed_file && "stream_link" in seed_file && seed_file.stream_link) {
470
+ const stream_link = new URL(seed_file.stream_link);
471
+ stream_link.searchParams.set("download", "true");
472
+
473
+ const [E_, R_] = await NS_promise.F_catch_promise({
474
+ promise: (async () => {
475
+ const cloudflare =
476
+ await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
477
+
478
+ if (!cloudflare.available) return;
479
+
480
+ const _ = await NS_pack_proxima.F_handle_pack_encode({
481
+ base: new URL(cloudflare.available),
482
+ payload: {
483
+ endpoint: stream_link.toString(),
484
+ commands: {
485
+ response_headers: {
486
+ set: {
487
+ "content-disposition": `attachment; filename="${output_destination}"`
488
+ }
489
+ },
490
+ use_h_version: true
491
+ }
492
+ }
493
+ });
494
+
495
+ return { proxy_url: _ };
496
+ })()
497
+ });
498
+
499
+ const new_stream_link =
500
+ (R_ as any)?.proxy_url || stream_link.toString();
501
 
502
+ // 302, not 301: the link is proxied/temporary and browsers cache 301s
503
+ return ctx.redirect(new_stream_link, 302);
504
+ } else {
505
+ // if seed is file set headers
506
+ construct_response_headers({
507
+ is_download: true
508
+ });
509
 
510
+ return seed_file;
 
511
  }
512
+ }
513
 
514
+ // -------------------------------
515
+ // Build flags
516
+ // -------------------------------
517
+ const CMD = await F_cmd({ subs: !error_end_of_file });
518
+
519
+ flags = CMD.flags;
520
+
521
+ if (ops === "just_check") {
522
+ // run ffprobe to see if playback still valid
523
+ const _ = await ping_probe({ playback });
524
+
525
+ if (_.success) {
526
+ return {
527
+ passed: true,
528
+ sbe_id,
529
+ delivery: output_destination,
530
+ qLevel,
531
+ picture_quality,
532
+ track_process: progress_update_endpoint,
533
+ ws,
534
+ flags
535
+ };
536
+ }
537
 
538
+ return {
539
+ passed: false,
540
+ delivery: output_destination,
541
+ track_process: progress_update_endpoint,
542
+ flags
543
+ };
544
+ }
545
 
546
+ if (ops === "cmd") {
547
  return {
548
+ cmd: flags
 
 
 
549
  };
550
+ }
 
 
 
551
 
552
+ // the client gave up while we were preparing
553
+ if (controller.signal.aborted) {
554
+ return;
 
 
555
  }
556
 
557
+ // -------------------------------
558
+ // network response headers
559
+ // -------------------------------
560
+ construct_response_headers({
561
+ is_streaming: is_stream_mode
562
+ });
563
 
564
+ NS_Misc.console.log({
565
+ ENV: "dev",
566
+ _browser: false,
567
+ _log: ["[FFMPEG FLAGS]", flags, "\n"]
568
+ });
 
 
 
 
569
 
570
+ claimed = true;
571
 
572
+ // -------------------------------
573
+ // spawn
574
+ // -------------------------------
575
+ if (!is_stream_mode) {
576
+ record_task_info({
577
+ job_id,
578
+ file_with_path: file_with_path || "",
579
+ status: "running",
580
+ info: {
581
+ title: output_destination,
582
+ ws,
583
+ progress_endpoint: progress_update_endpoint,
584
+ flags
585
+ }
586
+ });
587
+ }
588
 
589
+ run({ ii: 0 });
 
590
 
591
+ live.launched = true;
592
 
593
+ if (is_stream_mode) {
594
+ return passthrough;
595
+ }
596
 
597
+ // download mode: the job lives on its own from here on
598
+ detach_request_abort();
 
 
 
 
599
 
600
+ const message = `COMPILER ENGINE started, this may take a while. ${progress_update_endpoint ? `check process -> ${progress_update_endpoint}` : "check back later"}`;
 
 
601
 
602
+ return {
603
+ message,
604
+ title: output_destination,
605
+ ws,
606
+ track_process: progress_update_endpoint
607
+ };
608
+ } finally {
609
+ // any early return or throw above means ffmpeg was never started
610
+ if (!live.launched) {
611
+ detach_request_abort();
612
+
613
+ // drop our claim, but never touch another request's job or a done/failed record
614
+ if (claimed && transcode_jobs.get(job_id)?.status === "running") {
615
+ transcode_jobs.delete(job_id);
616
  }
617
  }
618
+ }
619
 
620
+ // ---------- ffmpeg lifecycle (hoisted) ----------
 
621
 
622
+ /**
623
+ * Starts (or restarts) ffmpeg. Never throws, failures go through fail().
624
+ */
625
+ async function run(arg0: {
626
+ ii: number;
627
+ keep_subs?: boolean;
628
+ logs?: string[];
629
+ }) {
630
+ const { ii, keep_subs, logs } = arg0;
631
 
632
+ if (ii >= MAX_ATTEMPTS) return;
633
 
634
+ try {
635
+ if (logs) id_log_errors({ logs });
636
 
637
+ if (ii > 0) {
638
+ const _CMD = await F_cmd({ subs: keep_subs });
639
 
640
+ flags = _CMD.flags;
 
 
 
641
 
642
+ if (controller.signal.aborted) controller = new AbortController();
643
 
644
+ progress_data = undefined;
645
+ seek_size_bytes = 0;
646
+ live.percent = 0;
647
+ live.size = "";
648
+ local_cleaned = false;
649
+ }
650
 
651
+ start_ffmpeg({ ii });
652
+ } catch (err) {
653
+ console.warn("spawn run error:", err, "\n");
654
 
655
+ fail([err_message(err)]);
656
+ }
657
+ }
658
 
659
+ function start_ffmpeg(arg0: { ii: number }) {
660
+ const { ii } = arg0;
661
 
662
+ const my_attempt = ++attempt;
 
 
 
 
663
 
664
+ let settled = false;
 
 
 
665
 
666
+ logged_proc_errors = [];
667
+ logged_proc_info = [];
668
 
669
+ const push_log = (arr: string[], line: string) => {
670
+ arr.push(line);
 
671
 
672
+ if (arr.length > MAX_LOG_LINES) {
673
+ arr.splice(0, arr.length - MAX_LOG_LINES);
674
  }
675
+ };
676
 
677
+ let counter: Transform | undefined;
 
678
 
679
+ if (use_s3_pipe) {
680
+ s3_bytes = 0;
 
681
 
682
+ const c = new Transform({
683
+ transform(chunk, _enc, cb) {
684
+ s3_bytes += chunk.length;
685
 
686
+ cb(null, chunk);
687
+ }
688
+ });
 
689
 
690
+ c.on("error", () => {}); // real errors surface through `done`
 
691
 
692
+ const upload = NS_bucket.makeS3Upload({
693
+ key: bucket_file_path,
694
+ body: c,
695
+ ContentType: out_mime
696
+ });
697
 
698
+ const done: Promise<S3Outcome> = upload.done().then(
699
+ () => ({ success: true as const }),
700
+ (error) => {
701
+ console.warn("S3:ERROR", bucket_file_path, error, "\n");
702
 
703
+ // the upload died, ffmpeg's output has nowhere to go: stop it
704
+ if (my_attempt === attempt) kill_controller("bucket upload failed");
705
 
706
+ return { success: false as const, error };
707
+ }
708
+ );
709
 
710
+ s3_attempt = { upload, done, counter: c };
711
+ counter = c;
712
+ }
713
 
714
+ // watchdog: kill ffmpeg if it never produces any output
715
+ set_t_timeout();
 
 
716
 
717
+ const proc = spawn(NS_ffmpeg_v2.ffmpeg_path, flags, {
718
+ signal: controller.signal
719
+ });
720
 
721
+ if (passthrough) proc.stdout.pipe(passthrough);
 
 
 
 
722
 
723
+ if (counter) {
724
+ proc.stdout.on("error", () => {}); // EPIPE if the upload is torn down
725
+ proc.stdout.pipe(counter);
726
+ }
727
 
728
+ proc.stderr.on("data", (data) => {
729
+ if (my_attempt !== attempt) return;
730
 
731
+ kill_controller_timeout();
732
 
733
+ const log = data.toString().trim();
734
 
735
+ if (is_dev) console.log("💠", log, "\n");
 
 
 
736
 
737
+ if (!log) return;
 
 
738
 
739
+ push_log(logged_proc_info, log);
 
740
 
741
+ if (
742
+ NS_ffmpeg_v2.proc_error_pattern().some((pattern) => pattern.test(log))
743
+ ) {
744
+ push_log(logged_proc_errors, log);
745
 
746
+ return;
747
+ }
748
 
749
+ const parsed = F_stderr_parse_progress({
750
+ stderr_line: log,
751
+ full_duration: parseFloat(`${probe_metadata.format.duration}`),
752
+ seek_size_bytes
753
  });
754
 
755
+ if (!parsed || !Object.keys(parsed).length) return;
 
756
 
757
+ progress_data = parsed;
758
 
759
+ seek_size_bytes = parseFloat(`${parsed.size_bytes}`) || 0;
760
 
761
+ live.size = parsed.size || live.size;
 
762
 
763
+ // S3 PIPE: ffmpeg's own size can be N/A on a pipe, the counter is exact
764
+ if (use_s3_pipe && s3_bytes > 0) {
765
+ live.size = NS_ffmpeg_v2.F_format_size(s3_bytes) || live.size;
766
  }
 
767
 
768
+ if (typeof parsed.percent === "number") {
769
+ live.percent = Math.min(99, parsed.percent);
770
+ }
 
 
 
771
 
772
+ push_progress({});
773
+ });
 
774
 
775
+ proc.on("close", (code) => settle({ code }));
776
 
777
+ proc.on("error", (err) => {
778
+ console.error("⚠️❌ FFmpeg spawn error:", err, "\n");
779
 
780
+ if (proc.pid === undefined) settle({ code: null, err });
781
+ });
 
782
 
783
+ function settle(arg0: { code: number | null; err?: Error }) {
784
+ if (settled) return;
 
 
 
785
 
786
+ settled = true;
 
 
 
787
 
788
+ kill_controller_timeout();
 
 
789
 
790
+ on_ffmpeg_end({ ii, ...arg0 }).catch((e) => {
791
+ console.warn("on_ffmpeg_end error:", e, "\n");
792
 
793
+ fail([err_message(e)]);
794
+ });
795
+ }
796
+ }
797
 
798
+ async function on_ffmpeg_end(arg0: {
799
+ ii: number;
800
+ code: number | null;
801
+ err?: Error;
802
+ }) {
803
+ const { ii, code, err } = arg0;
804
 
805
+ // S3 PIPE: the upload finishes just after ffmpeg's stdout closes. Wait
806
+ // for it, success needs BOTH ffmpeg and the upload to have succeeded.
807
+ const s3_result = s3_attempt ? await s3_attempt.done : undefined;
808
 
809
+ const percent = progress_data?.percent;
810
 
811
+ const failure_reason = (() => {
812
+ if (err) return `FFmpeg could not start: ${err.message}`;
 
 
 
813
 
814
+ if (s3_result && !s3_result.success) {
815
+ return `Bucket upload failed: ${err_message(s3_result.error)}`;
816
+ }
817
 
818
+ if (code !== 0) {
819
+ return controller.signal.aborted
820
+ ? "FFmpeg was stopped (timeout or abort)"
821
+ : `FFmpeg exited with code ${code}`;
822
+ }
823
 
824
+ if (typeof percent === "number" && percent > 0 && percent < 90) {
825
+ return "Incomplete media data";
826
  }
827
+ })();
828
 
829
+ if (failure_reason) {
830
+ console.error(`⚠️❌ ${failure_reason}`, "\n");
831
+ console.error("Error details:", logged_proc_errors, "\n");
 
 
 
 
 
832
 
833
+ // S3 PIPE: a truncated media is still "playable", so the half-written object must go. Wait for the delete: a retry reuses the same key.
834
+ await abort_s3_upload();
835
 
836
+ cleanup_local();
 
837
 
838
+ const will_retry =
839
+ is_download_mode && !live.user_aborted && ii + 1 < MAX_ATTEMPTS;
840
 
841
+ if (will_retry) {
842
+ let i = ii + 1;
 
 
 
 
 
 
843
 
844
+ if (is_dev) console.log(`WILL RETRY: ${i}`, "\n");
 
 
 
845
 
846
+ run({
847
+ ii: i,
848
+ keep_subs: F_keep_subs(),
849
+ logs: logged_proc_info
850
+ });
851
 
852
  return;
853
  }
854
 
855
+ fail([failure_reason, ...logged_proc_errors]);
 
856
 
857
+ return;
858
+ }
859
 
860
+ NS_Misc.console.log({
861
+ ENV: "all",
862
+ _browser: false,
863
+ _log: [
864
+ `🏁 FFMPEG finished: (${percent}) -> ${progress_data?.size} ${file_with_path || output_destination}`,
865
+ "\n"
866
+ ]
867
+ });
868
 
869
+ if (is_stream_mode) {
870
+ // finish_done();
871
 
872
+ return;
873
+ }
874
 
875
+ await deliver_download();
876
+ }
877
 
878
+ /**
879
+ * ffmpeg finished successfully.
880
+ S3 pipe: the object is already in the bucket.
881
+ Disk: upload the local file, then delete it whether the upload worked or not.
882
+ */
883
+ async function deliver_download() {
884
+ if (use_s3_pipe) {
885
+ s3_attempt = undefined;
886
 
887
+ live.size = NS_ffmpeg_v2.F_format_size(s3_bytes) || live.size;
 
888
 
889
+ live.percent = 99;
890
+ push_progress({ force: true });
891
 
892
  await document_and_finish();
893
+
894
+ return;
895
  }
896
 
897
+ if (!file_with_path) {
898
+ fail(["No local output file"]);
899
+
900
+ return;
901
+ }
 
 
 
 
902
 
903
+ if (is_dev) {
904
  finish_done();
905
+
906
+ return;
907
  }
908
 
909
+ live.percent = 99;
910
+ push_progress({ force: true, uploading: true });
911
 
912
+ let uploaded = false;
 
 
913
 
914
+ try {
915
+ uploaded = await upload_with_retry({ file_with_path });
916
+ } catch (e) {
917
+ console.warn("bucket upload error:", e, "\n");
918
+ } finally {
919
+ cleanup_local();
920
+ }
921
 
922
+ if (!uploaded) {
923
+ fail(["Bucket upload failed"]);
924
 
925
+ return;
926
+ }
 
 
 
 
 
927
 
928
+ await document_and_finish();
929
+ }
930
 
931
+ async function document_and_finish() {
932
+ // document file
933
+ await wide_search_runner({
934
+ document: true,
935
+ skip: {
936
+ local: true,
937
+ db: true
938
  }
939
+ }).catch(() => {});
940
+
941
+ finish_done();
942
+ }
943
 
944
+ async function upload_with_retry(arg0: { file_with_path: string }) {
945
+ const { file_with_path } = arg0;
946
+
947
+ if (typeof file_with_path !== "string" || !file_with_path.trim().length) {
948
  return false;
949
  }
950
 
951
+ const delays = [0, 2000, 5000];
 
 
 
 
 
952
 
953
+ for (let i = 0; i < delays.length; i++) {
954
+ if (delays[i]) await Bun.sleep(delays[i]!);
955
 
956
+ const r = await NS_bucket.upload_to_bucket({
957
+ job_id,
958
+ bucket_path: bucket_file_path,
959
+ file_with_path,
960
+ REQ0,
961
+ skip: { probe: i > 0 }
962
+ });
963
 
964
+ if (r?.success) return true;
965
 
966
+ if (r && !r.success && r.reason === "probe") return false;
 
 
 
 
967
  }
968
 
969
+ return false;
970
+ }
971
 
972
+ /*
973
+ S3 PIPE: tears down the current attempt's upload and deletes whatever
974
+ reached the bucket. Idempotent, and safe when there is no upload.
975
+ */
976
+ async function abort_s3_upload() {
977
+ const a = s3_attempt;
978
 
979
+ if (!a) return;
 
 
 
 
 
 
980
 
981
+ s3_attempt = undefined;
 
982
 
983
+ a.counter.destroy();
 
 
984
 
985
+ await NS_bucket.delete_self_s3({
986
+ key: bucket_file_path,
987
+ upload_in: a.upload,
988
+ done: a.done
989
+ });
990
+ }
991
 
992
+ // ---------- reporting (hoisted) ----------
993
 
994
+ function relay_endpoint(data: Record<string, any>) {
995
+ if (!progress_update_endpoint) return;
996
+
997
+ try {
998
+ relay_progress_info({
999
+ endpoint: new URL(progress_update_endpoint),
1000
+ progress_data: data
 
 
 
 
 
 
 
 
1001
  });
1002
+ } catch {}
1003
+ }
1004
+
1005
+ function push_progress(arg0: { force?: boolean; uploading?: boolean }) {
1006
+ if (live.done || live.failed || is_stream_mode) return;
1007
+
1008
+ // each push hits the store, the websocket and an outbound HTTP relay,
1009
+ // so cap it. Completion always goes through finish_done.
1010
+ const now = Date.now();
1011
+
1012
+ if (!arg0.force && now - live.last_push < 1500) return;
1013
+
1014
+ live.last_push = now;
1015
 
1016
+ record_task_info({
1017
+ job_id,
1018
+ file_with_path: file_with_path || "",
1019
+ status: "running",
1020
+ info: {
1021
  uploading: arg0.uploading,
1022
+ title: output_destination,
1023
  percent: live.percent,
1024
+ transcoded_size: live.size,
1025
+ progress_endpoint: progress_update_endpoint,
1026
+ ws,
1027
+ flags,
1028
  log_errors: logged_proc_errors,
1029
  logs: logged_proc_info
1030
+ }
1031
+ });
1032
 
1033
+ const data = {
1034
+ ...progress_data,
1035
+ uploading: arg0.uploading,
1036
+ percent: live.percent,
1037
+ size: live.size || progress_data?.size,
1038
+ filename: output_destination,
1039
+ location: `${REQ0.origin}/${REQ0.pathname}`,
1040
+ log_errors: logged_proc_errors,
1041
+ logs: logged_proc_info
1042
+ };
1043
 
1044
+ relay_progress_info_ws({
1045
+ channel: data_key,
1046
+ payload: {
1047
+ channel: data_key,
1048
+ status: "running",
1049
+ data
1050
+ }
1051
+ });
1052
 
1053
+ relay_endpoint(data);
1054
+ }
1055
 
1056
+ function finish_done() {
1057
+ if (live.done || live.failed || is_stream_mode) return;
1058
 
1059
+ live.done = true;
1060
+ live.percent = 100;
 
 
 
 
 
 
 
 
 
 
 
 
1061
 
1062
+ record_task_info({
1063
+ job_id,
1064
+ file_with_path: file_with_path || "",
1065
+ status: "done",
1066
+ info: {
1067
+ title: output_destination,
 
 
 
 
 
1068
  percent: 100,
1069
+ transcoded_size: live.size || progress_data?.size,
1070
+ progress_endpoint: progress_update_endpoint,
1071
+ flags,
1072
  log_errors: logged_proc_errors,
1073
  logs: logged_proc_info
1074
+ }
1075
+ });
1076
 
1077
+ relay_progress_info_ws({
1078
+ channel: data_key,
1079
+ payload: {
1080
+ channel: data_key,
1081
+ status: "done",
1082
+ filename: output_destination,
1083
+ logs: logged_proc_info
1084
  }
1085
+ });
1086
+
1087
+ relay_endpoint({
1088
+ percent: 100,
1089
+ filename: output_destination,
1090
+ location: `${REQ0.origin}/${REQ0.pathname}`,
1091
+ log_errors: logged_proc_errors,
1092
+ logs: logged_proc_info
1093
+ });
1094
+
1095
+ if (is_download_mode) {
1096
+ transcode_jobs.delete(job_id);
1097
  }
1098
+ }
1099
 
1100
+ function fail(logs: string[]) {
1101
+ if (live.failed || live.done || is_stream_mode) return;
1102
 
1103
+ live.failed = true;
1104
 
1105
+ record_task_info({
1106
+ job_id,
1107
+ file_with_path: file_with_path || "",
1108
+ status: "failed",
1109
+ info: {
1110
+ title: output_destination,
1111
+ ws,
1112
+ error: logs.join(" | "),
1113
+ log_errors: logged_proc_errors,
1114
+ logs: logged_proc_info
1115
+ }
1116
+ });
1117
 
1118
+ relay_progress_info_ws({
1119
+ channel: data_key,
1120
+ payload: {
1121
  channel: data_key,
 
 
 
 
 
 
 
 
 
1122
  status: "failed",
1123
  filename: output_destination,
 
 
1124
  logs: logged_proc_info
1125
+ }
1126
+ });
1127
 
1128
+ relay_endpoint({
1129
+ status: "failed",
1130
+ filename: output_destination,
1131
+ location: `${REQ0.origin}/${REQ0.pathname}`,
1132
+ log_errors: logged_proc_errors,
1133
+ logs: logged_proc_info
1134
+ });
1135
 
1136
+ passthrough?.destroy(new Error(logs[0] || "failed"));
1137
 
1138
+ cleanup_local();
 
 
 
1139
 
1140
+ // S3 PIPE: covers failures that did not come through on_ffmpeg_end
1141
+ // (spawn/run errors). A no-op when the upload was already torn down.
1142
+ abort_s3_upload().catch((e) => console.warn("abort_s3_upload:", e));
1143
+ }
1144
 
1145
+ // ---------- helpers (hoisted) ----------
 
 
 
 
 
 
1146
 
1147
+ /**
1148
+ * Deletes the local output file. Safe to call from every exit path: it
1149
+ * only runs once per attempt. Skipped in dev, where the local file is the
1150
+ * store. No-op in S3 pipe mode, there is no local file.
1151
+ */
1152
+ function cleanup_local() {
1153
+ if (!file_with_path || local_cleaned) return;
1154
 
1155
+ if (is_dev && live.done) return;
1156
 
1157
+ local_cleaned = true;
 
1158
 
1159
+ NS_bucket.delete_self({ try_count: 0, file_with_path });
1160
+ }
1161
 
1162
+ function kill_controller(reason: string) {
1163
+ controller.abort();
1164
 
1165
+ console.log(`[CONTROLLER] aborted: ${reason}`);
1166
+ }
1167
 
1168
+ function set_t_timeout() {
1169
+ kill_controller_timeout();
 
 
 
1170
 
1171
+ watchdog = setTimeout(
1172
+ () => kill_controller("no output from ffmpeg"),
1173
+ NS_ffmpeg_v2.TIMEOUT_MS
1174
+ );
1175
+ }
1176
+
1177
+ function kill_controller_timeout() {
1178
+ if (watchdog) {
1179
+ clearTimeout(watchdog);
1180
+ watchdog = undefined;
1181
  }
1182
+ }
1183
 
1184
+ function id_log_errors(arg0: { logs: string[] }) {
1185
+ const { logs } = arg0;
1186
 
1187
+ if (!Array.isArray(logs)) {
1188
+ return;
1189
+ }
1190
 
1191
+ const has = (needle: string) => {
1192
+ return logs.some((xx) =>
1193
+ xx.toString().toLowerCase().includes(needle.toLowerCase())
1194
+ );
1195
+ };
1196
 
1197
+ if (has("http error 429 too many requests")) {
1198
+ error_429 = true;
1199
+ }
1200
 
1201
+ if (has("failed too many times, skipping")) {
1202
+ error_429 = true;
1203
+ }
1204
 
1205
+ if (has("error opening input: end of file")) {
1206
+ error_end_of_file = true;
1207
+ }
1208
 
1209
+ if (has("error opening input files: server returned 403 forbidden")) {
1210
+ error_403 = true;
1211
+ }
1212
 
1213
+ if (has("http error 404 not found")) {
1214
+ error_403 = true;
 
1215
  }
1216
+ }
1217
 
1218
+ // keep subtitles unless one of THEIR urls caused the end-of-file error
1219
+ function F_keep_subs() {
1220
+ if (!logged_proc_info.length || !subtitles?.length) {
1221
+ return true;
1222
+ }
1223
 
1224
+ const eof_lines = logged_proc_info
1225
+ .map((xx) => xx.toString().toLowerCase())
1226
+ .filter((xx) => xx.includes("error opening input: end of file"));
1227
 
1228
+ if (!eof_lines.length) {
1229
+ return true;
1230
+ }
1231
 
1232
+ const sub_failed = subtitles.some((sub) => {
1233
+ const url = sub.url.toString().toLowerCase();
1234
 
1235
+ return eof_lines.some((line) => line.includes(url));
1236
+ });
1237
 
1238
+ return !sub_failed;
1239
+ }
1240
 
1241
+ async function F_cmd(arg0: { subs?: boolean }) {
1242
+ const { subs } = arg0;
1243
 
1244
+ const new_playback = await (async () => {
1245
+ if ((!error_429 && !error_403) || playback.url.includes("/proxima")) {
1246
+ return playback;
1247
+ }
1248
 
1249
+ // get_hf_sia_proxy
1250
+ const R_ = IS_HF()
1251
+ ? await NS_scrape_post_processing_helper.get_cloudflare_proxy({})
1252
+ : await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
1253
 
1254
+ if (!R_) return playback;
1255
 
1256
+ const proxy_base = R_.available || R_.fallback;
1257
 
1258
+ if (NS_routes_c.F_is_url(proxy_base)) {
1259
+ const _ = await NS_pack_proxima.F_handle_pack_encode({
1260
+ base: proxy_base,
1261
+ payload: {
1262
+ endpoint: playback.url,
1263
+ commands: {
1264
+ use_h_version: true,
1265
+ redirect_origin: true,
1266
+ headers: playback.headers
 
1267
  }
1268
+ }
1269
+ });
1270
 
1271
+ if (_) {
1272
+ const __ = { ...playback };
1273
 
1274
+ __.url = _.toString();
1275
 
1276
+ delete __.headers;
1277
 
1278
+ return __;
 
1279
  }
1280
+ }
1281
 
1282
+ return playback;
1283
+ })();
 
 
 
 
 
 
 
 
 
 
 
 
 
1284
 
1285
+ has_subs = subs === false ? false : true;
1286
+
1287
+ const data = await NS_ffmpeg_v2.F_build_ffmpeg_flags_t1({
1288
+ playback: new_playback,
1289
+ subtitles: subs === false ? undefined : subtitles,
1290
+ probe_metadata,
1291
+ delivery_format,
1292
+ delivery_mode,
1293
+ output_destination: file_with_path || "",
1294
+ flash_ping: false,
1295
+ dl_to_s3: use_s3_pipe
1296
+ });
1297
 
1298
+ return data;
1299
+ }
 
 
 
1300
 
1301
+ function construct_response_headers(arg0: {
1302
+ is_download?: boolean;
1303
+ is_streaming?: boolean;
1304
+ }) {
1305
+ const { is_streaming, is_download } = arg0;
1306
 
1307
+ /* ---------- set response headers ---------- */
1308
+ if (is_download || is_streaming) {
1309
+ ctx.set.headers["content-type"] = out_mime;
1310
+ }
1311
 
1312
+ ctx.set.headers["cache-control"] = "no-cache";
 
 
1313
 
1314
+ if (is_download) {
1315
+ ctx.set.headers["content-disposition"] =
1316
+ `attachment; filename="${output_destination}"`;
1317
 
1318
+ ctx.set.headers["accept-ranges"] = "bytes";
 
 
1319
  }
1320
 
1321
+ if (clean_metadata?.format?.duration) {
1322
+ ctx.set.headers["X-Content-Duration"] = clean_metadata.format?.duration;
1323
+ }
1324
+ }
 
 
 
 
 
 
 
1325
 
1326
+ async function wide_search_runner(arg0: {
1327
+ skip?: {
1328
+ local?: boolean;
1329
+ db?: boolean;
1330
+ bucket?: boolean;
1331
+ };
1332
+ document: boolean;
1333
+ }) {
1334
+ const { skip, document } = arg0;
1335
+
1336
+ if (!is_download_mode) return;
1337
+
1338
+ const [E_, R_] = await NS_promise.F_catch_promise({
1339
+ promise: NS_showtime_v2_helper.find_seed_file({
1340
+ sbe_id,
1341
+ output_destination: {
1342
+ og: output_destination,
1343
+ clean: output_destination_clean,
1344
+ file_with_path
1345
+ },
1346
+ picture_quality,
1347
+ qLevel,
1348
+ sbe_s_e,
1349
+ cache_folder,
1350
+ file_id: clean_file_id,
1351
+ job_id,
1352
+ S3Dir,
1353
+ cleanup_corruption: true,
1354
+ document,
1355
+ ideal_runtime: playback.ideal_runtime,
1356
+ skip,
1357
+ has_subs
1358
+ })
1359
+ });
1360
 
1361
+ if (R_ && "stream_link" in R_ && R_.stream_link) {
1362
+ if (!NS_misc.isValidUrlFormat(R_.stream_link)) {
1363
+ return;
 
1364
  }
 
 
1365
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1366
 
1367
+ return R_;
1368
+ }
1369
+ }
1370
 
1371
  function F_stderr_parse_progress(arg0: {
1372
  stderr_line: string;
src/sub-router/showtime.ts CHANGED
@@ -31,6 +31,7 @@ import { NS_error } from "@x/core/client/error.ts";
31
  import type { Upload } from "@aws-sdk/lib-storage";
32
  import { NS_routes_c } from "@x/core/client/route.ts";
33
  import { NS_files } from "@x/core/server/files.ts";
 
34
 
35
  const APP_ACCOUNT_ABC = () =>
36
  NS_runtime.ENV_VAR_DB?.APP_ACCOUNT_ABC || process.env.APP_ACCOUNT_ABC;
@@ -46,6 +47,11 @@ const APP_ORIGIN = () =>
46
  const HF_BUCKET_NAME = () =>
47
  NS_runtime.ENV_VAR_DB?.HF_BUCKET_NAME || process.env.HF_BUCKET_NAME;
48
 
 
 
 
 
 
49
  const sub_router = new Elysia({ prefix: "/showtimeV2" }).get(
50
  "/delivery/:data_key",
51
  async (ctx) => {
@@ -56,11 +62,6 @@ const sub_router = new Elysia({ prefix: "/showtimeV2" }).get(
56
  const REQ0 = new URL(request.url);
57
  const REQ_ORIGIN = (ctx as any).publicOrigin || REQ0.origin;
58
 
59
- const is_dev = (() => {
60
- // return false;
61
- return APP_ENV() === "dev";
62
- })();
63
-
64
  const metadata = await (async () => {
65
  try {
66
  if (is_dev) {
@@ -86,1492 +87,1551 @@ const sub_router = new Elysia({ prefix: "/showtimeV2" }).get(
86
  return metadata;
87
  }
88
 
89
- const {
90
- playback,
91
- probe_metadata,
92
- output_destination,
93
- sbe_id,
94
- delivery_mode,
95
- delivery_format,
96
- out_mime,
97
- progress_update_endpoint,
98
- subtitles,
99
- fMP4
100
- } = metadata as T_delivery_payload;
101
-
102
- const is_download_mode = delivery_mode === "download";
103
- const is_stream_mode = delivery_mode === "stream";
104
-
105
- if (!is_download_mode && !is_stream_mode) {
106
- return ctx.status(400, "Unsupported delivery mode");
107
- }
108
-
109
- const clean_metadata = NS_ffmpeg_v2.F_scan_raw_probe_metadata({
110
- metadata: probe_metadata
111
  });
112
 
113
- const picture_quality = (() => {
114
- const _ = playback.picture_quality;
115
-
116
- if (typeof _ !== "string") {
117
- return "";
118
- }
119
-
120
- return _.trim().split(" ").join("");
121
- })();
122
-
123
- const sbe_s_e = (() => {
124
- const _ = metadata.sbe_s_e;
125
 
126
- if (typeof _ !== "string" || _.toLowerCase().includes("nan")) {
127
- return;
128
- }
129
 
130
- return _.trim().split(" ").join("");
131
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
- const qLevel = (() => {
134
- if (
135
- typeof metadata.qLevel === "string" &&
136
- metadata.qLevel.trim().length > 0
137
- ) {
138
- return metadata.qLevel.trim();
139
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- const _ = NS_ffmpeg_v2.F_get_qLevel({
142
- metadata: probe_metadata
143
- });
144
 
145
- return _;
146
- })();
147
 
148
- const ws = (() => {
149
- const is_https = new URL(REQ_ORIGIN).protocol === "https:";
 
150
 
151
- const ws_protocol = is_https ? "wss" : "ws";
 
152
 
153
- const _ = `${ws_protocol}://${REQ0.host}/ws`;
 
154
 
155
- const data = {
156
- connect: _,
157
- channel: data_key
158
- };
159
 
160
- return data;
161
- })();
162
 
163
- const output_destination_clean = NS_misc.cleanNameV2(output_destination);
 
 
 
 
 
 
164
 
165
- const clean_file_id =
166
- `${picture_quality}-${output_destination_clean}`.toLowerCase();
 
167
 
168
- const job_id = `${sbe_id}-${clean_file_id}`;
 
169
 
170
- const file_id = `${Date.now()}-${clean_file_id}`;
 
171
 
172
- const cache_folder = `cache/${sbe_id}`;
173
 
174
- const S3Dir = path.join(STORAGE_ROOT(), `${cache_folder}`);
175
 
176
- const bucket_file_path = `${cache_folder}/${file_id}`;
 
 
 
177
 
178
- // TWO STEP: on HF an HLS -> mp4 delivery goes through the bucket twice
179
- let two_step =
180
- IS_HF() &&
181
- is_download_mode &&
182
- delivery_format === "mp4" &&
183
- playback.type === "hls";
184
 
185
- const dl_to_s3 = IS_RENDER_COM() || two_step;
186
 
187
- let use_s3_pipe = is_download_mode && dl_to_s3;
 
188
 
189
- if (use_s3_pipe && (!S3() || !HF_BUCKET_NAME())) {
190
- return ctx.status(500, "S3 bucket is not configured");
191
- }
192
 
193
- // TWO STEP: the intermediate object lives OUTSIDE cache_folder, so the seed
194
- // lookup never mistakes it for a finished file
195
- const ts_key = (() => {
196
- const _ = NS_files.F_get_file_extension({
197
- filename: file_id,
198
- return_new_filename: true
199
- });
200
 
201
- return `tmp/${sbe_id}/${_.new_filename || file_id}.ts`;
202
- })();
203
 
204
- const PHASES = {
205
- single: { from: 0, to: 99, label: "Processing" },
206
- ts: { from: 0, to: 60, label: "Step 1/2: fetching video" },
207
- mp4: { from: 60, to: 99, label: "Step 2/2: converting to MP4" }
208
- } as const;
209
 
210
- type Phase = keyof typeof PHASES;
211
 
212
- let phase: Phase = two_step ? "ts" : "single";
 
 
 
 
 
213
 
214
- const xc = REQ0.searchParams.has("xc");
215
 
216
- (() => {
217
- const i_x = REQ0.searchParams.get("i_x");
218
 
219
- if (!NS_routes_c.F_is_url(i_x)) return;
 
220
 
221
- phase = "mp4";
222
 
223
- playback.url = i_x!;
224
- playback.headers = undefined;
225
- playback.type = "video";
226
- (playback as any).i_x = true;
227
- use_s3_pipe = false;
228
- })();
229
 
230
- const current_key = () => (phase === "ts" ? ts_key : bucket_file_path);
231
- const current_mime = () => (phase === "ts" ? "video/mp2t" : out_mime);
 
 
 
 
 
232
 
233
- // phase-local percent (0-100) -> overall percent, never above 99
234
- const to_overall = (phase_pct: number) => {
235
- const p = PHASES[phase];
236
 
237
- return Math.min(
238
- 99,
239
- Math.max(p.from, p.from + (phase_pct / 100) * (p.to - p.from))
240
- );
241
- };
242
 
243
- const _file_with_path = () => {
244
- try {
245
- node_fs.mkdirSync(S3Dir, { recursive: true });
246
 
247
- return path.join(S3Dir, file_id);
248
- } catch (e) {}
249
- };
250
 
251
- let file_with_path = (() => {
252
- if (!is_download_mode || use_s3_pipe) return;
253
 
254
- return _file_with_path();
255
- })();
256
 
257
- if (is_download_mode && !use_s3_pipe && !file_with_path) {
258
- return ctx.status(500, "Could not prepare the output directory");
259
- }
260
 
261
- const MAX_ATTEMPTS = 2;
262
 
263
- const MAX_LOG_LINES = 200;
 
 
 
 
 
264
 
265
- const err_message = (err: unknown) =>
266
- NS_error.F_collect_error_message({ ERR: err }).message;
267
 
268
- const live = {
269
- percent: 0,
270
- size: "",
271
- failed: false,
272
- done: false,
273
- launched: false,
274
- user_aborted: false,
275
- last_push: 0
276
- };
277
 
278
- let flags: string[] = [];
279
- let progress_data: Record<string, any> | undefined = undefined;
280
- let seek_size_bytes = 0;
281
- let logged_proc_errors: string[] = [];
282
- let logged_proc_info: string[] = [];
283
- let has_subs = false;
284
- let local_cleaned = false;
285
- let attempt = 0;
286
- let watchdog: ReturnType<typeof setTimeout> | undefined = undefined;
287
-
288
- let error_429 = false;
289
- let error_end_of_file = false;
290
- let error_403 = false;
291
-
292
- let claimed = false;
293
-
294
- // S3 PIPE: state of the current attempt's upload
295
- type S3Outcome = { success: true } | { success: false; error: unknown };
296
-
297
- type S3Attempt = {
298
- key: string;
299
- upload: Upload;
300
- done: Promise<S3Outcome>;
301
- counter: Transform;
302
- };
303
 
304
- let s3_attempt: S3Attempt | undefined = undefined;
 
 
305
 
306
- // TWO STEP: the finished .ts upload, kept until step 2 is done so it can be deleted
307
- let ts_attempt: S3Attempt | undefined = undefined;
 
308
 
309
- let s3_bytes = 0;
 
310
 
311
- let controller = new AbortController();
312
- const abort_signal = ctx?.request.signal;
313
 
314
- const on_request_abort = () => {
315
- live.user_aborted = true;
316
 
317
- kill_controller("client aborted");
318
- };
319
 
320
- const detach_request_abort = () => {
321
- abort_signal?.removeEventListener("abort", on_request_abort);
322
- };
323
 
324
- if (abort_signal?.aborted) {
325
- return;
326
- }
327
 
328
- abort_signal?.addEventListener("abort", on_request_abort, { once: true });
329
 
330
- const passthrough = is_stream_mode ? new PassThrough() : undefined;
 
331
 
332
- try {
333
- const current_job = (() => {
334
- const _ = transcode_jobs.get(job_id);
 
 
 
 
 
 
335
 
336
- if (!_ || xc) return;
 
 
 
 
 
 
 
 
337
 
338
- return _;
339
- })();
 
340
 
341
- if (current_job?.status === "running") {
342
- const percent = current_job?.percent;
343
- const percentage = typeof percent === "number" ? `${percent} %` : "";
344
- const transcoded_size = current_job?.transcoded_size;
345
- const progress_endpoint =
346
- current_job?.progress_endpoint || progress_update_endpoint;
347
 
348
- const message = percentage
349
- ? `ENGINE PROGRESS: ${percentage}`
350
- : "ENGINE PROCESSING ...";
351
 
352
- return {
353
- ...(ops === "just_check" ? { passed: true } : {}),
354
- processing: true,
355
- message,
356
- percent,
357
- transcoded_size,
358
- // TWO STEP: lets a re-polling client show which step is running
359
- phase: current_job?.phase,
360
- stage: current_job?.stage,
361
- track_process: progress_endpoint,
362
- file: output_destination,
363
- job_id,
364
- output_destination,
365
- qLevel,
366
- picture_quality,
367
- ws,
368
- flags: current_job.flags,
369
- logs: current_job.logs
370
- };
371
- }
372
 
373
- if (current_job?.status === "failed") {
374
- const logs = current_job?.logs;
375
 
376
- if (logs) id_log_errors({ logs });
 
377
 
378
- transcode_jobs.delete(job_id);
379
 
380
- if (ops === "just_check") {
381
- return {
382
- passed: false,
383
- delivery: output_destination,
384
- track_process: progress_update_endpoint,
385
- ws,
386
- flags: current_job.flags,
387
- logs: current_job?.logs
388
- };
389
- }
390
 
391
- const _ = await ping_probe({ playback });
 
392
 
393
- if (!_?.success) {
394
- throw new Error("Something went wrong: with probe");
395
- }
396
- }
397
 
398
- // check/serve seeded file
399
- const seed_file = await (async () => {
400
- if (ops === "cmd") {
401
- return;
402
- }
403
 
404
- const _ = current_job?.db_metadata?.stream_link;
 
 
405
 
406
- if (_) {
407
- return current_job.db_metadata;
408
- }
409
 
410
- const __ = await wide_search_runner({ document: true });
411
 
412
- return __;
413
- })();
414
-
415
- if (seed_file) {
416
- const file_size_bytes = parseFloat(`${seed_file?.size}`);
417
-
418
- const percent = 100;
419
- const transcoded_size = file_size_bytes
420
- ? NS_ffmpeg_v2.F_format_size(file_size_bytes)
421
- : current_job?.transcoded_size;
422
- const progress_endpoint =
423
- current_job?.progress_endpoint || progress_update_endpoint;
424
-
425
- if (!current_job?.db_metadata?.stream_link) {
426
- record_task_info({
427
- job_id,
428
- file_with_path: file_with_path || "",
429
- status: "done",
430
- info: {
431
- title: output_destination,
432
- percent,
433
- transcoded_size,
434
- ...("stream_link" in seed_file && seed_file.stream_link
435
- ? { db_metadata: seed_file }
436
- : {})
437
- }
438
- });
439
- }
440
 
441
- if (ops === "just_check") {
442
- return {
443
- passed: true,
444
- ready: true,
445
- percent,
446
- transcoded_size,
447
- output_destination,
448
- flags: current_job?.flags,
449
- logs: current_job?.logs,
450
- ...(seed_file && "stream_link" in seed_file && seed_file.stream_link
451
- ? { stream_link: seed_file.stream_link }
452
- : {}),
453
- track_process: progress_endpoint,
454
- ws
455
- };
456
- }
457
 
458
- // handle direct file or url redirect
459
- if (seed_file && "stream_link" in seed_file && seed_file.stream_link) {
460
- const stream_link = new URL(seed_file.stream_link);
461
- stream_link.searchParams.set("download", "true");
462
-
463
- const [E_, R_] = await NS_promise.F_catch_promise({
464
- promise: (async () => {
465
- const cloudflare =
466
- await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
467
-
468
- if (!cloudflare.available) return;
469
-
470
- const _ = await NS_pack_proxima.F_handle_pack_encode({
471
- base: new URL(cloudflare.available),
472
- payload: {
473
- endpoint: stream_link.toString(),
474
- commands: {
475
- response_headers: {
476
- set: {
477
- "content-disposition": `attachment; filename="${output_destination}"`
478
- }
479
- },
480
- use_h_version: true
481
- }
482
- }
483
- });
484
 
485
- return { proxy_url: _ };
486
- })()
487
- });
 
 
 
488
 
489
- const new_stream_link =
490
- (R_ as any)?.proxy_url || stream_link.toString();
 
491
 
492
- // 302, not 301: the link is proxied/temporary and browsers cache 301s
493
- return ctx.redirect(new_stream_link, 302);
494
- } else {
495
- // if seed is file set headers
496
- construct_response_headers({
497
- is_download: true
498
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
- return seed_file;
501
- }
502
- }
503
 
504
- // -------------------------------
505
- // Build flags
506
- // -------------------------------
507
- // (two_step: these are the step 1 flags, step 2 flags are built when it starts)
508
- const CMD = await F_cmd({ subs: !error_end_of_file });
509
 
510
- flags = CMD.flags;
511
 
512
  if (ops === "just_check") {
513
- const _ = await ping_probe({ playback });
514
-
515
- if (_.success) {
516
- return {
517
- passed: true,
518
- sbe_id,
519
- delivery: output_destination,
520
- qLevel,
521
- picture_quality,
522
- track_process: progress_update_endpoint,
523
- ws,
524
- flags
525
- };
526
- }
527
-
528
  return {
529
  passed: false,
530
  delivery: output_destination,
531
  track_process: progress_update_endpoint,
532
- flags
 
 
533
  };
534
  }
535
 
536
- if (ops === "cmd") {
537
- return {
538
- cmd: flags
539
- };
540
  }
 
541
 
542
- // the client gave up while we were preparing
543
- if (controller.signal.aborted) {
 
544
  return;
545
  }
546
 
547
- // -------------------------------
548
- // network response headers
549
- // -------------------------------
550
- construct_response_headers({
551
- is_streaming: is_stream_mode
552
- });
553
 
554
- NS_Misc.console.log({
555
- ENV: "dev",
556
- _browser: false,
557
- _log: ["[FFMPEG FLAGS]", flags, "\n"]
558
- });
 
 
 
 
 
 
559
 
560
- claimed = true;
 
 
 
 
 
561
 
562
- // -------------------------------
563
- // spawn
564
- // -------------------------------
565
- if (!is_stream_mode) {
566
  record_task_info({
567
  job_id,
568
  file_with_path: file_with_path || "",
569
- status: "running",
570
  info: {
571
  title: output_destination,
572
- ws,
573
- progress_endpoint: progress_update_endpoint,
574
- ...(two_step ? { phase, stage: PHASES[phase].label } : {}),
575
- flags
 
576
  }
577
  });
578
  }
579
 
580
- if (two_step && !(playback as any).i_x) {
581
- const [E_, R_] = await NS_promise.F_catch_fetch({
582
- parse: "json",
583
- promise: fetch(
584
- (() => {
585
- const _ = new URL(`/stream/delivery/${data_key}`, REQ_ORIGIN);
 
 
 
 
 
 
 
 
 
 
586
 
587
- _.searchParams.set("x_i", job_id);
588
- _.searchParams.set("xc", "");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
 
590
- return _;
591
- })()
592
- )
593
  });
594
 
595
- if (E_ || !R_) {
596
- if (fMP4) {
597
- phase = "single";
598
- two_step = false;
599
- use_s3_pipe = false;
600
- }
601
 
602
- run({
603
- ii: 0,
604
- ...(fMP4 ? { keep_subs: true, rebuild: true } : {})
605
- });
606
- }
607
  } else {
608
- run({
609
- ii: 0,
610
- ...((playback as any).i_x ? { keep_subs: true, rebuild: true } : {})
611
  });
 
 
612
  }
 
613
 
614
- live.launched = true;
 
 
 
 
615
 
616
- if (is_stream_mode) {
617
- return passthrough;
618
- }
619
 
620
- // download mode: the job lives on its own from here on
621
- detach_request_abort();
622
 
623
- const message = `COMPILER ENGINE started, this may take a while. ${progress_update_endpoint ? `check process -> ${progress_update_endpoint}` : "check back later"}`;
 
 
 
 
 
 
 
 
 
 
 
624
 
625
  return {
626
- message,
627
- title: output_destination,
628
- ws,
629
- track_process: progress_update_endpoint
630
  };
631
- } finally {
632
- // any early return or throw above means ffmpeg was never started
633
- if (!live.launched) {
634
- detach_request_abort();
635
-
636
- // drop our claim, but never touch another request's job or a done/failed record
637
- if (claimed && transcode_jobs.get(job_id)?.status === "running") {
638
- transcode_jobs.delete(job_id);
639
- }
640
- }
641
  }
642
 
643
- // ---------- ffmpeg lifecycle (hoisted) ----------
644
-
645
- /**
646
- * Starts (or restarts) ffmpeg. Never throws, failures go through fail().
647
- * `rebuild` forces new flags even on the first attempt of a phase
648
- * (step 2 needs flags that are different from step 1).
649
- */
650
- async function run(arg0: {
651
- ii: number;
652
- keep_subs?: boolean;
653
- logs?: string[];
654
- rebuild?: boolean;
655
- }) {
656
- const { ii, keep_subs, logs, rebuild } = arg0;
657
-
658
- if (ii >= MAX_ATTEMPTS) return;
659
-
660
- try {
661
- if (logs) id_log_errors({ logs });
662
 
663
- if (ii > 0 || rebuild) {
664
- const _CMD = await F_cmd({ subs: keep_subs });
 
 
665
 
666
- flags = _CMD.flags;
 
 
 
 
 
667
 
668
- NS_Misc.console.log({
669
- ENV: "dev",
670
- _browser: false,
671
- _log: [`[FFMPEG FLAGS] ${ii}::${rebuild}`, flags, "\n"]
672
- });
673
 
674
- if (controller.signal.aborted) controller = new AbortController();
675
 
676
- progress_data = undefined;
677
- seek_size_bytes = 0;
678
- live.percent = PHASES[phase].from;
679
- live.size = "";
680
- local_cleaned = false;
 
 
 
 
 
 
 
 
 
681
  }
682
-
683
- start_ffmpeg({ ii });
684
- } catch (err) {
685
- console.warn("spawn run error:", err, "\n");
686
-
687
- fail([err_message(err)]);
688
- }
689
  }
690
 
691
- function start_ffmpeg(arg0: { ii: number }) {
692
- const { ii } = arg0;
693
 
694
- const my_attempt = ++attempt;
 
695
 
696
- let settled = false;
 
 
 
 
 
 
 
697
 
698
- logged_proc_errors = [];
699
- logged_proc_info = [];
 
 
 
 
 
 
 
 
700
 
701
- const push_log = (arr: string[], line: string) => {
702
- arr.push(line);
703
 
704
- if (arr.length > MAX_LOG_LINES) {
705
- arr.splice(0, arr.length - MAX_LOG_LINES);
 
 
 
706
  }
707
- };
708
 
709
- let counter: Transform | undefined;
 
 
 
 
 
 
 
 
 
 
710
 
711
- if (use_s3_pipe) {
712
- s3_bytes = 0;
713
 
714
- // TWO STEP: step 1 uploads the .ts, step 2 the mp4
715
- const key = current_key();
 
716
 
717
- const c = new Transform({
718
- transform(chunk, _enc, cb) {
719
- s3_bytes += chunk.length;
720
 
721
- cb(null, chunk);
722
- }
723
- });
724
 
725
- c.on("error", () => {}); // real errors surface through `done`
 
 
 
 
 
 
 
 
 
726
 
727
- const upload = NS_bucket.makeS3Upload({
728
- key,
729
- body: c,
730
- ContentType: current_mime()
731
- });
 
732
 
733
- const done: Promise<S3Outcome> = upload.done().then(
734
- () => ({ success: true as const }),
735
- (error) => {
736
- console.warn("S3:ERROR", key, error, "\n");
737
 
738
- // the upload died, ffmpeg's output has nowhere to go: stop it
739
- if (my_attempt === attempt) kill_controller("bucket upload failed");
 
 
 
 
 
 
 
 
 
 
740
 
741
- return { success: false as const, error };
742
- }
743
- );
744
 
745
- s3_attempt = { key, upload, done, counter: c };
746
- counter = c;
747
- }
748
 
749
- // watchdog: kill ffmpeg if it never produces any output
750
- set_t_timeout();
751
 
752
- const proc = spawn(NS_ffmpeg_v2.ffmpeg_path, flags, {
753
- signal: controller.signal
754
- });
755
 
756
- if (passthrough) proc.stdout.pipe(passthrough);
 
 
 
 
757
 
758
- if (counter) {
759
- proc.stdout.on("error", () => {}); // EPIPE if the upload is torn down
760
- proc.stdout.pipe(counter);
761
- }
762
 
763
- proc.stderr.on("data", (data) => {
764
- if (my_attempt !== attempt) return;
 
 
 
 
765
 
766
- kill_controller_timeout();
 
 
767
 
768
- const log = data.toString().trim();
 
 
769
 
770
- if (is_dev) console.log("💠", log, "\n");
 
771
 
772
- if (!log) return;
773
 
774
- push_log(logged_proc_info, log);
775
 
776
- if (
777
- NS_ffmpeg_v2.proc_error_pattern().some((pattern) => pattern.test(log))
778
- ) {
779
- push_log(logged_proc_errors, log);
780
 
781
- return;
782
- }
783
 
784
- const parsed = F_stderr_parse_progress({
785
- stderr_line: log,
786
- full_duration: parseFloat(`${probe_metadata.format.duration}`),
787
- seek_size_bytes
788
- });
789
 
790
- if (!parsed || !Object.keys(parsed).length) return;
791
 
792
- progress_data = parsed;
 
793
 
794
- seek_size_bytes = parseFloat(`${parsed.size_bytes}`) || 0;
 
795
 
796
- live.size = parsed.size || live.size;
 
 
797
 
798
- // S3 PIPE: ffmpeg's own size can be N/A on a pipe, the counter is exact
799
- if (use_s3_pipe && s3_bytes > 0) {
800
- live.size = NS_ffmpeg_v2.F_format_size(s3_bytes) || live.size;
801
  }
 
802
 
803
- if (typeof parsed.percent === "number") {
804
- // TWO STEP: map this phase's 0-100 onto its share of the overall bar
805
- live.percent = to_overall(parsed.percent);
806
- }
807
 
808
- push_progress({});
 
 
 
809
  });
810
 
811
- proc.on("close", (code) => settle({ code }));
 
 
 
812
 
813
- proc.on("error", (err) => {
814
- console.error("⚠️❌ FFmpeg spawn error:", err, "\n");
815
 
816
- if (proc.pid === undefined) settle({ code: null, err });
817
- });
 
818
 
819
- function settle(arg0: { code: number | null; err?: Error }) {
820
- if (settled) return;
 
821
 
822
- settled = true;
 
823
 
824
- kill_controller_timeout();
 
 
825
 
826
- on_ffmpeg_end({ ii, ...arg0 }).catch((e) => {
827
- console.warn("on_ffmpeg_end error:", e, "\n");
828
 
829
- fail([err_message(e)]);
830
- });
831
- }
832
  }
833
 
834
- async function on_ffmpeg_end(arg0: {
835
- ii: number;
836
- code: number | null;
837
- err?: Error;
838
- }) {
839
- const { ii, code, err } = arg0;
840
 
841
- // S3 PIPE: the upload finishes just after ffmpeg's stdout closes. Wait
842
- // for it, success needs BOTH ffmpeg and the upload to have succeeded.
843
- const s3_result = s3_attempt ? await s3_attempt.done : undefined;
844
 
845
- const percent = progress_data?.percent;
846
 
847
- const failure_reason: string | undefined = (() => {
848
- if (err) return `FFmpeg could not start: ${err.message}`;
849
 
850
- if (s3_result && !s3_result.success) {
851
- return `Bucket upload failed: ${err_message(s3_result.error)}`;
852
- }
853
 
854
- if (code !== 0) {
855
- return controller.signal.aborted
856
- ? "FFmpeg was stopped (timeout or abort)"
857
- : `FFmpeg exited with code ${code}`;
858
- }
859
 
860
- if (typeof percent === "number" && percent > 0 && percent < 90) {
861
- return "Incomplete media data";
862
- }
863
- })();
864
 
865
- if (failure_reason) {
866
- console.error(`⚠️❌ [${phase}] ${failure_reason}`, "\n");
867
- console.error("Error details:", logged_proc_errors, "\n");
868
 
869
- // S3 PIPE: a truncated media is still "playable", so the half-written object must go. Wait for the delete: a retry reuses the same key.
870
- await abort_s3_upload();
 
 
 
871
 
872
- cleanup_local();
873
 
874
- // TWO STEP: retries are per phase, `ii` starts again at 0 in step 2
875
- const will_retry =
876
- is_download_mode && !live.user_aborted && ii + 1 < MAX_ATTEMPTS;
877
 
878
- if (will_retry) {
879
- let i = ii + 1;
880
 
881
- if (is_dev) console.log(`WILL RETRY [${phase}]: ${i}`, "\n");
882
 
883
- run({
884
- ii: i,
885
- keep_subs: F_keep_subs(),
886
- logs: logged_proc_info
887
- });
888
 
889
- return;
890
- }
 
 
891
 
892
- fail([failure_reason, ...logged_proc_errors]);
 
893
 
894
- return;
895
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
896
 
897
- NS_Misc.console.log({
898
- ENV: "all",
899
- _browser: false,
900
- _log: [
901
- `🏁 FFMPEG finished [${phase}]: (${percent}) -> ${progress_data?.size} ${file_with_path || output_destination}`,
902
- "\n"
903
- ]
904
  });
 
 
905
 
906
- if (is_stream_mode) {
907
- // finish_done();
 
 
 
 
908
 
909
- return;
 
 
 
 
 
 
 
 
 
 
910
  }
911
 
912
- // TWO STEP: the .ts is in the bucket, now convert it to mp4
913
- if (two_step && phase === "ts") {
914
- await begin_mp4_phase();
 
 
915
 
916
- return;
 
917
  }
 
918
 
919
- await deliver_download();
920
- }
 
921
 
922
- /**
923
- * TWO STEP: step 1 finished, the .ts object is in the bucket. Start step 2,
924
- * which reads that object and uploads the mp4.
925
- */
926
- async function begin_mp4_phase() {
927
- // an empty .ts would only produce an empty mp4
928
- if (s3_bytes <= 0) {
929
- fail(["Step 1 produced an empty file"]);
 
 
 
 
 
 
 
 
 
 
 
930
 
931
  return;
932
  }
933
 
934
- // keep the upload around so the temporary object can be deleted at the end
935
- ts_attempt = s3_attempt;
936
- s3_attempt = undefined;
937
 
938
- phase = "mp4";
 
939
 
940
- // subtitle/proxy errors seen while fetching the HLS source say nothing
941
- // about step 2
942
- error_end_of_file = false;
 
 
 
 
 
943
 
944
- live.percent = PHASES.mp4.from;
945
- live.size = "";
946
 
947
- // tell clients the second step started
948
- push_progress({ force: true });
949
 
950
- use_s3_pipe = false;
951
- file_with_path = _file_with_path();
 
952
 
953
- await run({ ii: 0, keep_subs: true, rebuild: true });
954
  }
955
 
956
- /**
957
- * ffmpeg finished successfully.
958
- * S3 pipe: the object is already in the bucket.
959
- * Disk: upload the local file, then delete it whether the upload worked or not.
960
- */
961
- async function deliver_download() {
962
- if (use_s3_pipe) {
963
- s3_attempt = undefined;
964
 
965
- live.size = NS_ffmpeg_v2.F_format_size(s3_bytes) || live.size;
 
 
 
 
 
 
 
966
 
967
- live.percent = 99;
968
- push_progress({ force: true });
969
 
970
- if (two_step && phase === "mp4") await delete_ts_temp();
 
 
971
 
972
- await document_and_finish();
973
 
974
- return;
975
- }
 
976
 
977
- if (!file_with_path) {
978
- fail(["No local output file"]);
979
 
980
- return;
981
- }
982
 
983
- if (is_dev) {
984
- finish_done();
985
- return;
986
- }
987
 
988
- live.percent = 99;
989
- push_progress({ force: true, uploading: true });
990
 
991
- let uploaded = false;
 
 
 
 
 
 
 
992
 
993
- try {
994
- uploaded = await upload_with_retry({ file_with_path });
995
- } catch (e) {
996
- console.warn("bucket upload error:", e, "\n");
997
- } finally {
998
- if (two_step && phase === "mp4") delete_ts_temp();
999
- cleanup_local();
1000
- }
1001
 
1002
- if (!uploaded) {
1003
- fail(["Bucket upload failed"]);
1004
 
1005
- return;
1006
- }
1007
 
1008
  await document_and_finish();
 
 
1009
  }
1010
 
1011
- async function document_and_finish() {
1012
- // document file
1013
- await wide_search_runner({
1014
- document: true,
1015
- skip: {
1016
- local: true,
1017
- db: true
1018
- }
1019
- }).catch(() => {});
1020
 
 
 
 
 
1021
  finish_done();
 
1022
  }
1023
 
1024
- async function upload_with_retry(arg0: { file_with_path: string }) {
1025
- const { file_with_path } = arg0;
1026
 
1027
- if (typeof file_with_path !== "string" || !file_with_path.trim().length) {
1028
- return false;
1029
- }
1030
 
1031
- const delays = [0, 2000, 5000];
 
 
 
 
 
 
 
1032
 
1033
- for (let i = 0; i < delays.length; i++) {
1034
- if (delays[i]) await Bun.sleep(delays[i]!);
1035
 
1036
- const r = await NS_bucket.upload_to_bucket({
1037
- job_id,
1038
- bucket_path: bucket_file_path,
1039
- file_with_path,
1040
- REQ0,
1041
- skip: { probe: i > 0 }
1042
- });
1043
 
1044
- if (r?.success) return true;
 
1045
 
1046
- if (r && !r.success && r.reason === "probe") return false;
 
 
 
 
 
 
1047
  }
 
1048
 
 
 
 
 
 
 
 
1049
  return false;
1050
  }
1051
 
1052
- /*
1053
- S3 PIPE: tears down the current attempt's upload and deletes whatever
1054
- reached the bucket under ITS key. Idempotent, and safe when there is no upload.
1055
- */
1056
- async function abort_s3_upload() {
1057
- const a = s3_attempt;
1058
 
1059
- if (!a) return;
 
1060
 
1061
- s3_attempt = undefined;
 
 
 
 
 
 
1062
 
1063
- a.counter.destroy();
1064
 
1065
- await NS_bucket.delete_self_s3({
1066
- key: a.key,
1067
- upload_in: a.upload,
1068
- done: a.done
1069
- });
1070
  }
1071
 
1072
- /*
1073
- TWO STEP: deletes the temporary .ts object. Best effort, never throws.
1074
- */
1075
- async function delete_ts_temp() {
1076
- if ((playback as any).i_x) return;
1077
 
1078
- const a = ts_attempt;
 
 
 
 
 
1079
 
1080
- if (!a) return;
1081
 
1082
- ts_attempt = undefined;
1083
 
1084
- NS_Misc.console.log({
1085
- ENV: "all",
1086
- _browser: false,
1087
- _log: [`🧹: ${a.key}`, "\n"]
1088
- });
1089
 
1090
- try {
1091
- await NS_bucket.delete_self_s3({
1092
- key: a.key,
1093
- upload_in: a.upload,
1094
- done: a.done
1095
- });
1096
- } catch (e) {
1097
- console.warn("delete_ts_temp:", e, "\n");
1098
- }
1099
- }
1100
 
1101
- // ---------- reporting (hoisted) ----------
 
 
 
 
1102
 
1103
- function relay_endpoint(data: Record<string, any>) {
1104
- if (!progress_update_endpoint) return;
1105
 
1106
- try {
1107
- relay_progress_info({
1108
- endpoint: new URL(progress_update_endpoint),
1109
- progress_data: data
1110
- });
1111
- } catch {}
1112
- }
1113
 
1114
- function push_progress(arg0: { force?: boolean; uploading?: boolean }) {
1115
- if (live.done || live.failed || is_stream_mode) return;
1116
 
1117
- // each push hits the store, the websocket and an outbound HTTP relay,
1118
- // so cap it. Completion always goes through finish_done.
1119
- const now = Date.now();
 
 
1120
 
1121
- if (!arg0.force && now - live.last_push < 1500) return;
 
 
 
 
 
 
 
 
 
1122
 
1123
- live.last_push = now;
1124
 
1125
- // TWO STEP: which step the user is looking at
1126
- const step_info = two_step ? { phase, stage: PHASES[phase].label } : {};
1127
 
1128
- record_task_info({
1129
- job_id,
1130
- file_with_path: file_with_path || "",
1131
- status: "running",
1132
- info: {
1133
- uploading: arg0.uploading,
1134
- ...step_info,
1135
- title: output_destination,
1136
- percent: live.percent,
1137
- transcoded_size: live.size,
1138
- progress_endpoint: progress_update_endpoint,
1139
- ws,
1140
- flags,
1141
- log_errors: logged_proc_errors,
1142
- logs: logged_proc_info
1143
- }
1144
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1145
 
1146
- const data = {
1147
- ...progress_data,
 
 
 
1148
  uploading: arg0.uploading,
1149
  ...step_info,
 
1150
  percent: live.percent,
1151
- size: live.size || progress_data?.size,
1152
- filename: output_destination,
1153
- location: `${REQ0.origin}/${REQ0.pathname}`,
 
1154
  log_errors: logged_proc_errors,
1155
  logs: logged_proc_info
1156
- };
1157
-
1158
- relay_progress_info_ws({
1159
- channel: data_key,
1160
- payload: {
1161
- channel: data_key,
1162
- status: "running",
1163
- data
1164
- }
1165
- });
1166
 
1167
- relay_endpoint(data);
1168
- }
 
 
 
 
 
 
 
 
 
1169
 
1170
- function finish_done() {
1171
- if (live.done || live.failed || is_stream_mode) return;
 
 
 
 
 
 
1172
 
1173
- live.done = true;
1174
- live.percent = 100;
1175
 
1176
- record_task_info({
1177
- job_id,
1178
- file_with_path: file_with_path || "",
1179
- status: "done",
1180
- info: {
1181
- title: output_destination,
1182
- percent: 100,
1183
- transcoded_size: live.size || progress_data?.size,
1184
- progress_endpoint: progress_update_endpoint,
1185
- flags,
1186
- log_errors: logged_proc_errors,
1187
- logs: logged_proc_info
1188
- }
1189
- });
1190
 
1191
- relay_progress_info_ws({
1192
- channel: data_key,
1193
- payload: {
1194
- channel: data_key,
1195
- status: "done",
1196
- filename: output_destination,
1197
- logs: logged_proc_info
1198
- }
1199
- });
1200
 
1201
- relay_endpoint({
 
 
 
 
 
1202
  percent: 100,
1203
- filename: output_destination,
1204
- location: `${REQ0.origin}/${REQ0.pathname}`,
 
1205
  log_errors: logged_proc_errors,
1206
  logs: logged_proc_info
1207
- });
 
1208
 
1209
- if (is_download_mode) {
1210
- transcode_jobs.delete(job_id);
 
 
 
 
 
1211
  }
 
 
 
 
 
 
 
 
 
 
 
 
1212
  }
 
1213
 
1214
- function fail(logs: string[]) {
1215
- if (live.failed || live.done || is_stream_mode) return;
1216
 
1217
- live.failed = true;
1218
 
1219
- record_task_info({
1220
- job_id,
1221
- file_with_path: file_with_path || "",
1222
- status: "failed",
1223
- info: {
1224
- title: output_destination,
1225
- ws,
1226
- // TWO STEP: say which step broke
1227
- ...(two_step ? { phase, stage: PHASES[phase].label } : {}),
1228
- error: logs.join(" | "),
1229
- log_errors: logged_proc_errors,
1230
- logs: logged_proc_info
1231
- }
1232
- });
1233
 
1234
- relay_progress_info_ws({
 
 
1235
  channel: data_key,
1236
- payload: {
1237
- channel: data_key,
1238
- status: "failed",
1239
- filename: output_destination,
1240
- logs: logged_proc_info
1241
- }
1242
- });
1243
-
1244
- relay_endpoint({
1245
  status: "failed",
1246
- ...(two_step ? { phase } : {}),
1247
  filename: output_destination,
1248
- location: `${REQ0.origin}/${REQ0.pathname}`,
1249
- log_errors: logged_proc_errors,
1250
  logs: logged_proc_info
1251
- });
 
1252
 
1253
- passthrough?.destroy(new Error(logs[0] || "failed"));
 
 
 
 
 
 
 
1254
 
1255
- cleanup_local();
1256
 
1257
- // S3 PIPE: covers failures that did not come through on_ffmpeg_end
1258
- // (spawn/run errors). A no-op when the upload was already torn down.
1259
- abort_s3_upload().catch((e) => console.warn("abort_s3_upload:", e));
1260
 
1261
- // TWO STEP: a failure in step 2 leaves the finished .ts behind: remove it
1262
- delete_ts_temp().catch(() => {});
1263
- }
1264
 
1265
- // ---------- helpers (hoisted) ----------
 
 
1266
 
1267
- /**
1268
- * Deletes the local output file. Safe to call from every exit path: it
1269
- * only runs once per attempt. Skipped in dev, where the local file is the
1270
- * store. No-op in S3 pipe mode, there is no local file.
1271
- */
1272
- function cleanup_local() {
1273
- if (!file_with_path || local_cleaned) return;
1274
 
1275
- if (is_dev && live.done) return;
 
 
 
 
 
 
1276
 
1277
- local_cleaned = true;
1278
 
1279
- NS_bucket.delete_self({ try_count: 0, file_with_path });
1280
- }
1281
 
1282
- function kill_controller(reason: string) {
1283
- controller.abort();
1284
 
1285
- console.log(`[CONTROLLER] aborted: ${reason}`);
1286
- }
1287
 
1288
- function set_t_timeout() {
1289
- kill_controller_timeout();
1290
 
1291
- watchdog = setTimeout(
1292
- () => kill_controller("no output from ffmpeg"),
1293
- NS_ffmpeg_v2.TIMEOUT_MS
1294
- );
 
 
 
 
 
 
 
 
 
1295
  }
 
1296
 
1297
- function kill_controller_timeout() {
1298
- if (watchdog) {
1299
- clearTimeout(watchdog);
1300
- watchdog = undefined;
1301
- }
1302
  }
1303
 
1304
- function id_log_errors(arg0: { logs: string[] }) {
1305
- const { logs } = arg0;
 
 
 
1306
 
1307
- if (!Array.isArray(logs)) {
1308
- return;
1309
- }
1310
 
1311
- const has = (needle: string) => {
1312
- return logs.some((xx) =>
1313
- xx.toString().toLowerCase().includes(needle.toLowerCase())
1314
- );
1315
- };
1316
 
1317
- if (has("http error 429 too many requests")) {
1318
- error_429 = true;
1319
- }
1320
 
1321
- if (has("failed too many times, skipping")) {
1322
- error_429 = true;
1323
- }
1324
 
1325
- if (has("error opening input: end of file")) {
1326
- error_end_of_file = true;
1327
- }
 
1328
 
1329
- if (has("error opening input files: server returned 403 forbidden")) {
1330
- error_403 = true;
1331
- }
 
 
1332
 
1333
- if (has("http error 404 not found")) {
1334
- error_403 = true;
1335
- }
 
 
 
1336
  }
1337
 
1338
- // keep subtitles unless one of THEIR urls caused the end-of-file error
1339
- function F_keep_subs() {
1340
- if (!logged_proc_info.length || !subtitles?.length) {
1341
- return true;
1342
- }
1343
 
1344
- const eof_lines = logged_proc_info
1345
- .map((xx) => xx.toString().toLowerCase())
1346
- .filter((xx) => xx.includes("error opening input: end of file"));
1347
 
1348
- if (!eof_lines.length) {
1349
- return true;
1350
- }
1351
 
1352
- const sub_failed = subtitles.some((sub) => {
1353
- const url = sub.url.toString().toLowerCase();
 
 
 
 
 
 
 
 
1354
 
1355
- return eof_lines.some((line) => line.includes(url));
1356
- });
1357
 
1358
- return !sub_failed;
1359
- }
1360
 
1361
- /**
1362
- * Sends a playback through the cloudflare proxy (used for the 403/429
1363
- * fallback and for private bucket urls). Returns the input unchanged when
1364
- * no proxy is available.
1365
- */
1366
- async function proxy_playback(p: T_playback): Promise<T_playback> {
1367
- // get_hf_sia_proxy
1368
- const R_ = IS_HF()
1369
- ? await NS_scrape_post_processing_helper.get_cloudflare_proxy({})
1370
- : await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
1371
-
1372
- if (!R_) return p;
1373
-
1374
- const proxy_base = R_.available || R_.fallback;
1375
-
1376
- if (NS_routes_c.F_is_url(proxy_base)) {
1377
- const _ = await NS_pack_proxima.F_handle_pack_encode({
1378
- base: proxy_base,
1379
- payload: {
1380
- endpoint: p.url,
1381
- commands: {
1382
- use_h_version: true,
1383
- redirect_origin: true,
1384
- headers: p.headers
1385
- }
1386
  }
1387
- });
 
1388
 
1389
- if (_) {
1390
- const __ = { ...p };
1391
 
1392
- __.url = _.toString();
1393
 
1394
- delete __.headers;
1395
 
1396
- return __;
1397
- }
1398
  }
1399
-
1400
- return p;
1401
  }
1402
 
1403
- /**
1404
- * TWO STEP: the playback ffmpeg reads in step 2, i.e. the .ts object that
1405
- * step 1 left in the bucket.
1406
- *
1407
- * The HLS source's headers (user agent, referer, ...) are deliberately NOT
1408
- * carried over, they do not belong on a bucket request.
1409
- */
1410
- async function ts_playback(): Promise<T_playback> {
1411
- if ((playback as any).i_x) {
1412
- return playback;
1413
- }
1414
-
1415
- const { url, headers } = NS_bucket.bucket_file_url({ key: ts_key });
1416
-
1417
- const p = { ...playback, url, headers, type: "video" } as T_playback;
1418
 
1419
- // private bucket: let the proxy attach the auth header. ffmpeg would send
1420
- // it to the CDN host the bucket redirects to as well, which rejects it.
1421
- return headers ? await proxy_playback(p) : p;
 
 
 
 
 
 
 
1422
  }
1423
 
1424
- async function F_cmd(arg0: { subs?: boolean }) {
1425
- const { subs } = arg0;
1426
-
1427
- // TWO STEP, step 2: input is the .ts in the bucket, output is the mp4 on
1428
- // stdout (delivery_mode "stream" is what your old "s3" mode mapped to)
1429
- if (phase === "mp4") {
1430
- has_subs = subs === false ? false : true;
1431
-
1432
- return await NS_ffmpeg_v2.F_build_ffmpeg_flags_t1({
1433
- playback: await ts_playback(),
1434
- subtitles: subs === false ? undefined : subtitles,
1435
- probe_metadata,
1436
- delivery_format,
1437
- delivery_mode: "download",
1438
- output_destination: file_with_path || "",
1439
- flash_ping: false,
1440
- dl_to_s3: false
1441
- });
1442
- }
1443
 
1444
- const new_playback = await (async () => {
1445
- if ((!error_429 && !error_403) || playback.url.includes("/proxima")) {
1446
- return playback;
1447
- }
1448
 
1449
- return await proxy_playback(playback);
1450
- })();
 
 
 
 
 
1451
 
1452
- // TWO STEP: a .ts cannot carry mov_text subtitles, they are added in step 2
1453
- has_subs = two_step ? false : subs === false ? false : true;
 
 
1454
 
1455
- const data = await NS_ffmpeg_v2.F_build_ffmpeg_flags_t1({
1456
- playback: new_playback,
1457
- subtitles: two_step || subs === false ? undefined : subtitles,
1458
  probe_metadata,
1459
- delivery_format: two_step ? "ts" : delivery_format,
1460
- delivery_mode,
1461
  output_destination: file_with_path || "",
1462
  flash_ping: false,
1463
- dl_to_s3: use_s3_pipe
1464
  });
1465
-
1466
- return data;
1467
  }
1468
 
1469
- function construct_response_headers(arg0: {
1470
- is_download?: boolean;
1471
- is_streaming?: boolean;
1472
- }) {
1473
- const { is_streaming, is_download } = arg0;
1474
-
1475
- /* ---------- set response headers ---------- */
1476
- if (is_download || is_streaming) {
1477
- ctx.set.headers["content-type"] = out_mime;
1478
  }
1479
 
1480
- ctx.set.headers["cache-control"] = "no-cache";
 
1481
 
1482
- if (is_download) {
1483
- ctx.set.headers["content-disposition"] =
1484
- `attachment; filename="${output_destination}"`;
1485
 
1486
- ctx.set.headers["accept-ranges"] = "bytes";
1487
- }
 
 
 
 
 
 
 
 
1488
 
1489
- if (clean_metadata?.format?.duration) {
1490
- ctx.set.headers["X-Content-Duration"] = clean_metadata.format?.duration;
1491
- }
1492
- }
1493
 
1494
- async function wide_search_runner(arg0: {
1495
- skip?: {
1496
- local?: boolean;
1497
- db?: boolean;
1498
- bucket?: boolean;
1499
- };
1500
- document: boolean;
1501
- }) {
1502
- const { skip, document } = arg0;
1503
 
1504
- if (!is_download_mode) return;
 
 
 
1505
 
1506
- const [E_, R_] = await NS_promise.F_catch_promise({
1507
- promise: NS_showtime_v2_helper.find_seed_file({
1508
- sbe_id,
1509
- output_destination: {
1510
- og: output_destination,
1511
- clean: output_destination_clean,
1512
- file_with_path
1513
- },
1514
- picture_quality,
1515
- qLevel,
1516
- sbe_s_e,
1517
- cache_folder,
1518
- file_id: clean_file_id,
1519
- job_id,
1520
- S3Dir,
1521
- cleanup_corruption: true,
1522
- document,
1523
- ideal_runtime: playback.ideal_runtime,
1524
- skip,
1525
- has_subs
1526
- })
1527
- });
1528
 
1529
- if (R_ && "stream_link" in R_ && R_.stream_link) {
1530
- if (!NS_misc.isValidUrlFormat(R_.stream_link)) {
1531
- return;
1532
- }
1533
- }
1534
 
1535
- return R_;
 
 
 
 
1536
  }
1537
- },
1538
- {
1539
- params: t.Object({
1540
- data_key: t.String({
1541
- minLength: 1,
1542
- error() {
1543
- return "Invalid process data, Expected string";
1544
- }
1545
- })
1546
- }),
1547
- query: t.Object({
1548
- raw: t.Optional(
1549
- t.Number({
1550
- minimum: 0,
1551
- maximum: 1,
1552
- error: () => "Invalid raw, Expected binary boolean"
1553
- })
1554
- ),
1555
- ops: t.Optional(
1556
- t.Union(
1557
- [
1558
- t.Literal("cmd"),
1559
- t.Literal("raw"),
1560
- t.Literal("decode"),
1561
- t.Literal("just_check")
1562
- ],
1563
- {
1564
- error() {
1565
- return "Invalid operation, must be one of: cmd, raw, decode";
1566
- }
1567
- }
1568
- )
1569
- )
1570
- })
1571
  }
1572
- );
1573
 
1574
- export const ShowtimeV2Router = sub_router;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1575
 
1576
  function F_stderr_parse_progress(arg0: {
1577
  stderr_line: string;
 
31
  import type { Upload } from "@aws-sdk/lib-storage";
32
  import { NS_routes_c } from "@x/core/client/route.ts";
33
  import { NS_files } from "@x/core/server/files.ts";
34
+ import { StreamRouterHandlePayload } from "./stream";
35
 
36
  const APP_ACCOUNT_ABC = () =>
37
  NS_runtime.ENV_VAR_DB?.APP_ACCOUNT_ABC || process.env.APP_ACCOUNT_ABC;
 
47
  const HF_BUCKET_NAME = () =>
48
  NS_runtime.ENV_VAR_DB?.HF_BUCKET_NAME || process.env.HF_BUCKET_NAME;
49
 
50
+ const is_dev = (() => {
51
+ // return false;
52
+ return APP_ENV() === "dev";
53
+ })();
54
+
55
  const sub_router = new Elysia({ prefix: "/showtimeV2" }).get(
56
  "/delivery/:data_key",
57
  async (ctx) => {
 
62
  const REQ0 = new URL(request.url);
63
  const REQ_ORIGIN = (ctx as any).publicOrigin || REQ0.origin;
64
 
 
 
 
 
 
65
  const metadata = await (async () => {
66
  try {
67
  if (is_dev) {
 
87
  return metadata;
88
  }
89
 
90
+ const [E_, R_] = await NS_promise.F_catch_promise({
91
+ promise: handle_data({
92
+ data_key,
93
+ metadata,
94
+ REQ0,
95
+ REQ_ORIGIN,
96
+ ctx: ctx as any,
97
+ ops
98
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  });
100
 
101
+ if (E_) {
102
+ const { message, status } = NS_error.F_collect_error_status_n_message({
103
+ ERR: E_
104
+ });
 
 
 
 
 
 
 
 
105
 
106
+ return ctx.status(status, message);
107
+ }
 
108
 
109
+ return R_;
110
+ },
111
+ {
112
+ params: t.Object({
113
+ data_key: t.String({
114
+ minLength: 1,
115
+ error() {
116
+ return "Invalid process data, Expected string";
117
+ }
118
+ })
119
+ }),
120
+ query: t.Object({
121
+ raw: t.Optional(
122
+ t.Number({
123
+ minimum: 0,
124
+ maximum: 1,
125
+ error: () => "Invalid raw, Expected binary boolean"
126
+ })
127
+ ),
128
+ ops: t.Optional(
129
+ t.Union(
130
+ [
131
+ t.Literal("cmd"),
132
+ t.Literal("raw"),
133
+ t.Literal("decode"),
134
+ t.Literal("just_check")
135
+ ],
136
+ {
137
+ error() {
138
+ return "Invalid operation, must be one of: cmd, raw, decode";
139
+ }
140
+ }
141
+ )
142
+ )
143
+ })
144
+ }
145
+ );
146
 
147
+ export const ShowtimeV2Router = sub_router;
148
+ export const ShowtimeV2RouterHandlePayload = handle_data;
149
+
150
+ async function handle_data(arg0: {
151
+ data_key: string;
152
+ metadata: T_delivery_payload;
153
+ REQ0: URL;
154
+ REQ_ORIGIN: string;
155
+ ctx: Context;
156
+ ops?: string;
157
+ throw_error?: boolean;
158
+ }) {
159
+ const { data_key, metadata, REQ0, REQ_ORIGIN, ctx, ops, throw_error } = arg0;
160
+
161
+ const {
162
+ playback,
163
+ probe_metadata,
164
+ output_destination,
165
+ sbe_id,
166
+ delivery_mode,
167
+ delivery_format,
168
+ out_mime,
169
+ progress_update_endpoint,
170
+ subtitles,
171
+ fMP4
172
+ } = metadata as T_delivery_payload;
173
+
174
+ const is_download_mode = delivery_mode === "download";
175
+ const is_stream_mode = delivery_mode === "stream";
176
+
177
+ if (!is_download_mode && !is_stream_mode) {
178
+ const message = "Unsupported delivery mode";
179
+
180
+ if (throw_error) throw Error(JSON.stringify(message));
181
+
182
+ return ctx.status(400, message);
183
+ }
184
 
185
+ const clean_metadata = NS_ffmpeg_v2.F_scan_raw_probe_metadata({
186
+ metadata: probe_metadata
187
+ });
188
 
189
+ const picture_quality = (() => {
190
+ const _ = playback.picture_quality;
191
 
192
+ if (typeof _ !== "string") {
193
+ return "";
194
+ }
195
 
196
+ return _.trim().split(" ").join("");
197
+ })();
198
 
199
+ const sbe_s_e = (() => {
200
+ const _ = metadata.sbe_s_e;
201
 
202
+ if (typeof _ !== "string" || _.toLowerCase().includes("nan")) {
203
+ return;
204
+ }
 
205
 
206
+ return _.trim().split(" ").join("");
207
+ })();
208
 
209
+ const qLevel = (() => {
210
+ if (
211
+ typeof metadata.qLevel === "string" &&
212
+ metadata.qLevel.trim().length > 0
213
+ ) {
214
+ return metadata.qLevel.trim();
215
+ }
216
 
217
+ const _ = NS_ffmpeg_v2.F_get_qLevel({
218
+ metadata: probe_metadata
219
+ });
220
 
221
+ return _;
222
+ })();
223
 
224
+ const ws = (() => {
225
+ const is_https = new URL(REQ_ORIGIN).protocol === "https:";
226
 
227
+ const ws_protocol = is_https ? "wss" : "ws";
228
 
229
+ const _ = `${ws_protocol}://${REQ0.host}/ws`;
230
 
231
+ const data = {
232
+ connect: _,
233
+ channel: data_key
234
+ };
235
 
236
+ return data;
237
+ })();
 
 
 
 
238
 
239
+ const output_destination_clean = NS_misc.cleanNameV2(output_destination);
240
 
241
+ const clean_file_id =
242
+ `${picture_quality}-${output_destination_clean}`.toLowerCase();
243
 
244
+ const job_id = `${sbe_id}-${clean_file_id}`;
 
 
245
 
246
+ const file_id = `${Date.now()}-${clean_file_id}`;
 
 
 
 
 
 
247
 
248
+ const cache_folder = `cache/${sbe_id}`;
 
249
 
250
+ const S3Dir = path.join(STORAGE_ROOT(), `${cache_folder}`);
 
 
 
 
251
 
252
+ const bucket_file_path = `${cache_folder}/${file_id}`;
253
 
254
+ // TWO STEP: on HF an HLS -> mp4 delivery goes through the bucket twice
255
+ let two_step =
256
+ IS_HF() &&
257
+ is_download_mode &&
258
+ delivery_format === "mp4" &&
259
+ playback.type === "hls";
260
 
261
+ const dl_to_s3 = IS_RENDER_COM() || two_step;
262
 
263
+ let use_s3_pipe = is_download_mode && dl_to_s3;
 
264
 
265
+ if (use_s3_pipe && (!S3() || !HF_BUCKET_NAME())) {
266
+ const message = "S3 bucket is not configured";
267
 
268
+ if (throw_error) throw Error(JSON.stringify(message));
269
 
270
+ return ctx.status(500, message);
271
+ }
 
 
 
 
272
 
273
+ // TWO STEP: the intermediate object lives OUTSIDE cache_folder, so the seed
274
+ // lookup never mistakes it for a finished file
275
+ const ts_key = (() => {
276
+ const _ = NS_files.F_get_file_extension({
277
+ filename: file_id,
278
+ return_new_filename: true
279
+ });
280
 
281
+ return `tmp/${sbe_id}/${_.new_filename || file_id}.ts`;
282
+ })();
 
283
 
284
+ const PHASES = {
285
+ single: { from: 0, to: 99, label: "Processing" },
286
+ ts: { from: 0, to: 60, label: "Step 1/2: fetching video" },
287
+ mp4: { from: 60, to: 99, label: "Step 2/2: converting to MP4" }
288
+ } as const;
289
 
290
+ type Phase = keyof typeof PHASES;
 
 
291
 
292
+ let phase: Phase = two_step ? "ts" : "single";
 
 
293
 
294
+ const xc = REQ0.searchParams.has("xc");
 
295
 
296
+ (() => {
297
+ const i_x = REQ0.searchParams.get("i_x");
298
 
299
+ if (!NS_routes_c.F_is_url(i_x)) return;
 
 
300
 
301
+ phase = "mp4";
302
 
303
+ playback.url = i_x!;
304
+ playback.headers = undefined;
305
+ playback.type = "video";
306
+ (playback as any).i_x = true;
307
+ use_s3_pipe = false;
308
+ })();
309
 
310
+ const current_key = () => (phase === "ts" ? ts_key : bucket_file_path);
311
+ const current_mime = () => (phase === "ts" ? "video/mp2t" : out_mime);
312
 
313
+ // phase-local percent (0-100) -> overall percent, never above 99
314
+ const to_overall = (phase_pct: number) => {
315
+ const p = PHASES[phase];
 
 
 
 
 
 
316
 
317
+ return Math.min(
318
+ 99,
319
+ Math.max(p.from, p.from + (phase_pct / 100) * (p.to - p.from))
320
+ );
321
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
+ const _file_with_path = () => {
324
+ try {
325
+ node_fs.mkdirSync(S3Dir, { recursive: true });
326
 
327
+ return path.join(S3Dir, file_id);
328
+ } catch (e) {}
329
+ };
330
 
331
+ let file_with_path = (() => {
332
+ if (!is_download_mode || use_s3_pipe) return;
333
 
334
+ return _file_with_path();
335
+ })();
336
 
337
+ if (is_download_mode && !use_s3_pipe && !file_with_path) {
338
+ const message = "Could not prepare the output directory";
339
 
340
+ if (throw_error) throw Error(JSON.stringify(message));
 
341
 
342
+ return ctx.status(500, message);
343
+ }
 
344
 
345
+ const MAX_ATTEMPTS = 2;
 
 
346
 
347
+ const MAX_LOG_LINES = 200;
348
 
349
+ const err_message = (err: unknown) =>
350
+ NS_error.F_collect_error_message({ ERR: err }).message;
351
 
352
+ const live = {
353
+ percent: 0,
354
+ size: "",
355
+ failed: false,
356
+ done: false,
357
+ launched: false,
358
+ user_aborted: false,
359
+ last_push: 0
360
+ };
361
 
362
+ let flags: string[] = [];
363
+ let progress_data: Record<string, any> | undefined = undefined;
364
+ let seek_size_bytes = 0;
365
+ let logged_proc_errors: string[] = [];
366
+ let logged_proc_info: string[] = [];
367
+ let has_subs = false;
368
+ let local_cleaned = false;
369
+ let attempt = 0;
370
+ let watchdog: ReturnType<typeof setTimeout> | undefined = undefined;
371
 
372
+ let error_429 = false;
373
+ let error_end_of_file = false;
374
+ let error_403 = false;
375
 
376
+ let claimed = false;
 
 
 
 
 
377
 
378
+ // S3 PIPE: state of the current attempt's upload
379
+ type S3Outcome = { success: true } | { success: false; error: unknown };
 
380
 
381
+ type S3Attempt = {
382
+ key: string;
383
+ upload: Upload;
384
+ done: Promise<S3Outcome>;
385
+ counter: Transform;
386
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
+ let s3_attempt: S3Attempt | undefined = undefined;
 
389
 
390
+ // TWO STEP: the finished .ts upload, kept until step 2 is done so it can be deleted
391
+ let ts_attempt: S3Attempt | undefined = undefined;
392
 
393
+ let s3_bytes = 0;
394
 
395
+ let controller = new AbortController();
396
+ const abort_signal = ctx?.request.signal;
 
 
 
 
 
 
 
 
397
 
398
+ const on_request_abort = () => {
399
+ live.user_aborted = true;
400
 
401
+ kill_controller("client aborted");
402
+ };
 
 
403
 
404
+ const detach_request_abort = () => {
405
+ abort_signal?.removeEventListener("abort", on_request_abort);
406
+ };
 
 
407
 
408
+ if (abort_signal?.aborted) {
409
+ return;
410
+ }
411
 
412
+ abort_signal?.addEventListener("abort", on_request_abort, { once: true });
 
 
413
 
414
+ const passthrough = is_stream_mode ? new PassThrough() : undefined;
415
 
416
+ try {
417
+ const current_job = (() => {
418
+ const _ = transcode_jobs.get(job_id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
+ if (!_ || xc) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
+ return _;
423
+ })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
+ if (current_job?.status === "running") {
426
+ const percent = current_job?.percent;
427
+ const percentage = typeof percent === "number" ? `${percent} %` : "";
428
+ const transcoded_size = current_job?.transcoded_size;
429
+ const progress_endpoint =
430
+ current_job?.progress_endpoint || progress_update_endpoint;
431
 
432
+ const message = percentage
433
+ ? `ENGINE PROGRESS: ${percentage}`
434
+ : "ENGINE PROCESSING ...";
435
 
436
+ return {
437
+ ...(ops === "just_check" ? { passed: true } : {}),
438
+ processing: true,
439
+ message,
440
+ percent,
441
+ transcoded_size,
442
+ // TWO STEP: lets a re-polling client show which step is running
443
+ phase: current_job?.phase,
444
+ stage: current_job?.stage,
445
+ track_process: progress_endpoint,
446
+ file: output_destination,
447
+ job_id,
448
+ output_destination,
449
+ qLevel,
450
+ picture_quality,
451
+ ws,
452
+ flags: current_job.flags,
453
+ logs: current_job.logs
454
+ };
455
+ }
456
 
457
+ if (current_job?.status === "failed") {
458
+ const logs = current_job?.logs;
 
459
 
460
+ if (logs) id_log_errors({ logs });
 
 
 
 
461
 
462
+ transcode_jobs.delete(job_id);
463
 
464
  if (ops === "just_check") {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  return {
466
  passed: false,
467
  delivery: output_destination,
468
  track_process: progress_update_endpoint,
469
+ ws,
470
+ flags: current_job.flags,
471
+ logs: current_job?.logs
472
  };
473
  }
474
 
475
+ const _ = await ping_probe({ playback });
476
+
477
+ if (!_?.success) {
478
+ throw new Error("Something went wrong: with probe");
479
  }
480
+ }
481
 
482
+ // check/serve seeded file
483
+ const seed_file = await (async () => {
484
+ if (ops === "cmd") {
485
  return;
486
  }
487
 
488
+ const _ = current_job?.db_metadata?.stream_link;
 
 
 
 
 
489
 
490
+ if (_) {
491
+ return current_job.db_metadata;
492
+ }
493
+
494
+ const __ = await wide_search_runner({ document: true });
495
+
496
+ return __;
497
+ })();
498
+
499
+ if (seed_file) {
500
+ const file_size_bytes = parseFloat(`${seed_file?.size}`);
501
 
502
+ const percent = 100;
503
+ const transcoded_size = file_size_bytes
504
+ ? NS_ffmpeg_v2.F_format_size(file_size_bytes)
505
+ : current_job?.transcoded_size;
506
+ const progress_endpoint =
507
+ current_job?.progress_endpoint || progress_update_endpoint;
508
 
509
+ if (!current_job?.db_metadata?.stream_link) {
 
 
 
510
  record_task_info({
511
  job_id,
512
  file_with_path: file_with_path || "",
513
+ status: "done",
514
  info: {
515
  title: output_destination,
516
+ percent,
517
+ transcoded_size,
518
+ ...("stream_link" in seed_file && seed_file.stream_link
519
+ ? { db_metadata: seed_file }
520
+ : {})
521
  }
522
  });
523
  }
524
 
525
+ if (ops === "just_check") {
526
+ return {
527
+ passed: true,
528
+ ready: true,
529
+ percent,
530
+ transcoded_size,
531
+ output_destination,
532
+ flags: current_job?.flags,
533
+ logs: current_job?.logs,
534
+ ...(seed_file && "stream_link" in seed_file && seed_file.stream_link
535
+ ? { stream_link: seed_file.stream_link }
536
+ : {}),
537
+ track_process: progress_endpoint,
538
+ ws
539
+ };
540
+ }
541
 
542
+ // handle direct file or url redirect
543
+ if (seed_file && "stream_link" in seed_file && seed_file.stream_link) {
544
+ const stream_link = new URL(seed_file.stream_link);
545
+ stream_link.searchParams.set("download", "true");
546
+
547
+ const [E_, R_] = await NS_promise.F_catch_promise({
548
+ promise: (async () => {
549
+ const cloudflare =
550
+ await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
551
+
552
+ if (!cloudflare.available) return;
553
+
554
+ const _ = await NS_pack_proxima.F_handle_pack_encode({
555
+ base: new URL(cloudflare.available),
556
+ payload: {
557
+ endpoint: stream_link.toString(),
558
+ commands: {
559
+ response_headers: {
560
+ set: {
561
+ "content-disposition": `attachment; filename="${output_destination}"`
562
+ }
563
+ },
564
+ use_h_version: true
565
+ }
566
+ }
567
+ });
568
 
569
+ return { proxy_url: _ };
570
+ })()
 
571
  });
572
 
573
+ const new_stream_link =
574
+ (R_ as any)?.proxy_url || stream_link.toString();
 
 
 
 
575
 
576
+ // 302, not 301: the link is proxied/temporary and browsers cache 301s
577
+ return ctx.redirect(new_stream_link, 302);
 
 
 
578
  } else {
579
+ // if seed is file set headers
580
+ construct_response_headers({
581
+ is_download: true
582
  });
583
+
584
+ return seed_file;
585
  }
586
+ }
587
 
588
+ // -------------------------------
589
+ // Build flags
590
+ // -------------------------------
591
+ // (two_step: these are the step 1 flags, step 2 flags are built when it starts)
592
+ const CMD = await F_cmd({ subs: !error_end_of_file });
593
 
594
+ flags = CMD.flags;
 
 
595
 
596
+ if (ops === "just_check") {
597
+ const _ = await ping_probe({ playback });
598
 
599
+ if (_.success) {
600
+ return {
601
+ passed: true,
602
+ sbe_id,
603
+ delivery: output_destination,
604
+ qLevel,
605
+ picture_quality,
606
+ track_process: progress_update_endpoint,
607
+ ws,
608
+ flags
609
+ };
610
+ }
611
 
612
  return {
613
+ passed: false,
614
+ delivery: output_destination,
615
+ track_process: progress_update_endpoint,
616
+ flags
617
  };
 
 
 
 
 
 
 
 
 
 
618
  }
619
 
620
+ if (ops === "cmd") {
621
+ return {
622
+ cmd: flags
623
+ };
624
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
 
626
+ // the client gave up while we were preparing
627
+ if (controller.signal.aborted) {
628
+ return;
629
+ }
630
 
631
+ // -------------------------------
632
+ // network response headers
633
+ // -------------------------------
634
+ construct_response_headers({
635
+ is_streaming: is_stream_mode
636
+ });
637
 
638
+ NS_Misc.console.log({
639
+ ENV: "dev",
640
+ _browser: false,
641
+ _log: ["[FFMPEG FLAGS]", flags, "\n"]
642
+ });
643
 
644
+ claimed = true;
645
 
646
+ // -------------------------------
647
+ // spawn
648
+ // -------------------------------
649
+ if (!is_stream_mode) {
650
+ record_task_info({
651
+ job_id,
652
+ file_with_path: file_with_path || "",
653
+ status: "running",
654
+ info: {
655
+ title: output_destination,
656
+ ws,
657
+ progress_endpoint: progress_update_endpoint,
658
+ ...(two_step ? { phase, stage: PHASES[phase].label } : {}),
659
+ flags
660
  }
661
+ });
 
 
 
 
 
 
662
  }
663
 
664
+ if (two_step && !(playback as any).i_x) {
665
+ const _ = new URL(`/stream/delivery/${data_key}`, REQ_ORIGIN);
666
 
667
+ _.searchParams.set("x_i", job_id);
668
+ _.searchParams.set("xc", "");
669
 
670
+ // const [E_, R_] = await NS_promise.F_catch_fetch({
671
+ // parse: "json",
672
+ // promise: fetch(
673
+ // (() => {
674
+ // return _;
675
+ // })()
676
+ // )
677
+ // });
678
 
679
+ const [E_, R_] = await NS_promise.F_catch_promise({
680
+ promise: StreamRouterHandlePayload({
681
+ data_key,
682
+ metadata,
683
+ REQ0: _,
684
+ REQ_ORIGIN,
685
+ ctx: ctx as any,
686
+ throw_error: true
687
+ })
688
+ });
689
 
690
+ console.log("E_, R_", E_, R_);
 
691
 
692
+ if (E_ || !R_) {
693
+ if (fMP4) {
694
+ phase = "single";
695
+ two_step = false;
696
+ use_s3_pipe = false;
697
  }
 
698
 
699
+ run({
700
+ ii: 0,
701
+ ...(fMP4 ? { keep_subs: true, rebuild: true } : {})
702
+ });
703
+ }
704
+ } else {
705
+ run({
706
+ ii: 0,
707
+ ...((playback as any).i_x ? { keep_subs: true, rebuild: true } : {})
708
+ });
709
+ }
710
 
711
+ live.launched = true;
 
712
 
713
+ if (is_stream_mode) {
714
+ return passthrough;
715
+ }
716
 
717
+ // download mode: the job lives on its own from here on
718
+ detach_request_abort();
 
719
 
720
+ const message = `COMPILER ENGINE started, this may take a while. ${progress_update_endpoint ? `check process -> ${progress_update_endpoint}` : "check back later"}`;
 
 
721
 
722
+ return {
723
+ message,
724
+ title: output_destination,
725
+ ws,
726
+ track_process: progress_update_endpoint
727
+ };
728
+ } finally {
729
+ // any early return or throw above means ffmpeg was never started
730
+ if (!live.launched) {
731
+ detach_request_abort();
732
 
733
+ // drop our claim, but never touch another request's job or a done/failed record
734
+ if (claimed && transcode_jobs.get(job_id)?.status === "running") {
735
+ transcode_jobs.delete(job_id);
736
+ }
737
+ }
738
+ }
739
 
740
+ // ---------- ffmpeg lifecycle (hoisted) ----------
 
 
 
741
 
742
+ /**
743
+ * Starts (or restarts) ffmpeg. Never throws, failures go through fail().
744
+ * `rebuild` forces new flags even on the first attempt of a phase
745
+ * (step 2 needs flags that are different from step 1).
746
+ */
747
+ async function run(arg0: {
748
+ ii: number;
749
+ keep_subs?: boolean;
750
+ logs?: string[];
751
+ rebuild?: boolean;
752
+ }) {
753
+ const { ii, keep_subs, logs, rebuild } = arg0;
754
 
755
+ if (ii >= MAX_ATTEMPTS) return;
 
 
756
 
757
+ try {
758
+ if (logs) id_log_errors({ logs });
 
759
 
760
+ if (ii > 0 || rebuild) {
761
+ const _CMD = await F_cmd({ subs: keep_subs });
762
 
763
+ flags = _CMD.flags;
 
 
764
 
765
+ NS_Misc.console.log({
766
+ ENV: "dev",
767
+ _browser: false,
768
+ _log: [`[FFMPEG FLAGS] ${ii}::${rebuild}`, flags, "\n"]
769
+ });
770
 
771
+ if (controller.signal.aborted) controller = new AbortController();
 
 
 
772
 
773
+ progress_data = undefined;
774
+ seek_size_bytes = 0;
775
+ live.percent = PHASES[phase].from;
776
+ live.size = "";
777
+ local_cleaned = false;
778
+ }
779
 
780
+ start_ffmpeg({ ii });
781
+ } catch (err) {
782
+ console.warn("spawn run error:", err, "\n");
783
 
784
+ fail([err_message(err)]);
785
+ }
786
+ }
787
 
788
+ function start_ffmpeg(arg0: { ii: number }) {
789
+ const { ii } = arg0;
790
 
791
+ const my_attempt = ++attempt;
792
 
793
+ let settled = false;
794
 
795
+ logged_proc_errors = [];
796
+ logged_proc_info = [];
 
 
797
 
798
+ const push_log = (arr: string[], line: string) => {
799
+ arr.push(line);
800
 
801
+ if (arr.length > MAX_LOG_LINES) {
802
+ arr.splice(0, arr.length - MAX_LOG_LINES);
803
+ }
804
+ };
 
805
 
806
+ let counter: Transform | undefined;
807
 
808
+ if (use_s3_pipe) {
809
+ s3_bytes = 0;
810
 
811
+ // TWO STEP: step 1 uploads the .ts, step 2 the mp4
812
+ const key = current_key();
813
 
814
+ const c = new Transform({
815
+ transform(chunk, _enc, cb) {
816
+ s3_bytes += chunk.length;
817
 
818
+ cb(null, chunk);
 
 
819
  }
820
+ });
821
 
822
+ c.on("error", () => {}); // real errors surface through `done`
 
 
 
823
 
824
+ const upload = NS_bucket.makeS3Upload({
825
+ key,
826
+ body: c,
827
+ ContentType: current_mime()
828
  });
829
 
830
+ const done: Promise<S3Outcome> = upload.done().then(
831
+ () => ({ success: true as const }),
832
+ (error) => {
833
+ console.warn("S3:ERROR", key, error, "\n");
834
 
835
+ // the upload died, ffmpeg's output has nowhere to go: stop it
836
+ if (my_attempt === attempt) kill_controller("bucket upload failed");
837
 
838
+ return { success: false as const, error };
839
+ }
840
+ );
841
 
842
+ s3_attempt = { key, upload, done, counter: c };
843
+ counter = c;
844
+ }
845
 
846
+ // watchdog: kill ffmpeg if it never produces any output
847
+ set_t_timeout();
848
 
849
+ const proc = spawn(NS_ffmpeg_v2.ffmpeg_path, flags, {
850
+ signal: controller.signal
851
+ });
852
 
853
+ if (passthrough) proc.stdout.pipe(passthrough);
 
854
 
855
+ if (counter) {
856
+ proc.stdout.on("error", () => {}); // EPIPE if the upload is torn down
857
+ proc.stdout.pipe(counter);
858
  }
859
 
860
+ proc.stderr.on("data", (data) => {
861
+ if (my_attempt !== attempt) return;
 
 
 
 
862
 
863
+ kill_controller_timeout();
 
 
864
 
865
+ const log = data.toString().trim();
866
 
867
+ if (is_dev) console.log("💠", log, "\n");
 
868
 
869
+ if (!log) return;
 
 
870
 
871
+ push_log(logged_proc_info, log);
 
 
 
 
872
 
873
+ if (
874
+ NS_ffmpeg_v2.proc_error_pattern().some((pattern) => pattern.test(log))
875
+ ) {
876
+ push_log(logged_proc_errors, log);
877
 
878
+ return;
879
+ }
 
880
 
881
+ const parsed = F_stderr_parse_progress({
882
+ stderr_line: log,
883
+ full_duration: parseFloat(`${probe_metadata.format.duration}`),
884
+ seek_size_bytes
885
+ });
886
 
887
+ if (!parsed || !Object.keys(parsed).length) return;
888
 
889
+ progress_data = parsed;
 
 
890
 
891
+ seek_size_bytes = parseFloat(`${parsed.size_bytes}`) || 0;
 
892
 
893
+ live.size = parsed.size || live.size;
894
 
895
+ // S3 PIPE: ffmpeg's own size can be N/A on a pipe, the counter is exact
896
+ if (use_s3_pipe && s3_bytes > 0) {
897
+ live.size = NS_ffmpeg_v2.F_format_size(s3_bytes) || live.size;
898
+ }
 
899
 
900
+ if (typeof parsed.percent === "number") {
901
+ // TWO STEP: map this phase's 0-100 onto its share of the overall bar
902
+ live.percent = to_overall(parsed.percent);
903
+ }
904
 
905
+ push_progress({});
906
+ });
907
 
908
+ proc.on("close", (code) => settle({ code }));
909
+
910
+ proc.on("error", (err) => {
911
+ console.error("⚠️❌ FFmpeg spawn error:", err, "\n");
912
+
913
+ if (proc.pid === undefined) settle({ code: null, err });
914
+ });
915
+
916
+ function settle(arg0: { code: number | null; err?: Error }) {
917
+ if (settled) return;
918
+
919
+ settled = true;
920
+
921
+ kill_controller_timeout();
922
+
923
+ on_ffmpeg_end({ ii, ...arg0 }).catch((e) => {
924
+ console.warn("on_ffmpeg_end error:", e, "\n");
925
 
926
+ fail([err_message(e)]);
 
 
 
 
 
 
927
  });
928
+ }
929
+ }
930
 
931
+ async function on_ffmpeg_end(arg0: {
932
+ ii: number;
933
+ code: number | null;
934
+ err?: Error;
935
+ }) {
936
+ const { ii, code, err } = arg0;
937
 
938
+ // S3 PIPE: the upload finishes just after ffmpeg's stdout closes. Wait
939
+ // for it, success needs BOTH ffmpeg and the upload to have succeeded.
940
+ const s3_result = s3_attempt ? await s3_attempt.done : undefined;
941
+
942
+ const percent = progress_data?.percent;
943
+
944
+ const failure_reason: string | undefined = (() => {
945
+ if (err) return `FFmpeg could not start: ${err.message}`;
946
+
947
+ if (s3_result && !s3_result.success) {
948
+ return `Bucket upload failed: ${err_message(s3_result.error)}`;
949
  }
950
 
951
+ if (code !== 0) {
952
+ return controller.signal.aborted
953
+ ? "FFmpeg was stopped (timeout or abort)"
954
+ : `FFmpeg exited with code ${code}`;
955
+ }
956
 
957
+ if (typeof percent === "number" && percent > 0 && percent < 90) {
958
+ return "Incomplete media data";
959
  }
960
+ })();
961
 
962
+ if (failure_reason) {
963
+ console.error(`⚠️❌ [${phase}] ${failure_reason}`, "\n");
964
+ console.error("Error details:", logged_proc_errors, "\n");
965
 
966
+ // S3 PIPE: a truncated media is still "playable", so the half-written object must go. Wait for the delete: a retry reuses the same key.
967
+ await abort_s3_upload();
968
+
969
+ cleanup_local();
970
+
971
+ // TWO STEP: retries are per phase, `ii` starts again at 0 in step 2
972
+ const will_retry =
973
+ is_download_mode && !live.user_aborted && ii + 1 < MAX_ATTEMPTS;
974
+
975
+ if (will_retry) {
976
+ let i = ii + 1;
977
+
978
+ if (is_dev) console.log(`WILL RETRY [${phase}]: ${i}`, "\n");
979
+
980
+ run({
981
+ ii: i,
982
+ keep_subs: F_keep_subs(),
983
+ logs: logged_proc_info
984
+ });
985
 
986
  return;
987
  }
988
 
989
+ fail([failure_reason, ...logged_proc_errors]);
 
 
990
 
991
+ return;
992
+ }
993
 
994
+ NS_Misc.console.log({
995
+ ENV: "all",
996
+ _browser: false,
997
+ _log: [
998
+ `🏁 FFMPEG finished [${phase}]: (${percent}) -> ${progress_data?.size} ${file_with_path || output_destination}`,
999
+ "\n"
1000
+ ]
1001
+ });
1002
 
1003
+ if (is_stream_mode) {
1004
+ // finish_done();
1005
 
1006
+ return;
1007
+ }
1008
 
1009
+ // TWO STEP: the .ts is in the bucket, now convert it to mp4
1010
+ if (two_step && phase === "ts") {
1011
+ await begin_mp4_phase();
1012
 
1013
+ return;
1014
  }
1015
 
1016
+ await deliver_download();
1017
+ }
 
 
 
 
 
 
1018
 
1019
+ /**
1020
+ * TWO STEP: step 1 finished, the .ts object is in the bucket. Start step 2,
1021
+ * which reads that object and uploads the mp4.
1022
+ */
1023
+ async function begin_mp4_phase() {
1024
+ // an empty .ts would only produce an empty mp4
1025
+ if (s3_bytes <= 0) {
1026
+ fail(["Step 1 produced an empty file"]);
1027
 
1028
+ return;
1029
+ }
1030
 
1031
+ // keep the upload around so the temporary object can be deleted at the end
1032
+ ts_attempt = s3_attempt;
1033
+ s3_attempt = undefined;
1034
 
1035
+ phase = "mp4";
1036
 
1037
+ // subtitle/proxy errors seen while fetching the HLS source say nothing
1038
+ // about step 2
1039
+ error_end_of_file = false;
1040
 
1041
+ live.percent = PHASES.mp4.from;
1042
+ live.size = "";
1043
 
1044
+ // tell clients the second step started
1045
+ push_progress({ force: true });
1046
 
1047
+ use_s3_pipe = false;
1048
+ file_with_path = _file_with_path();
 
 
1049
 
1050
+ await run({ ii: 0, keep_subs: true, rebuild: true });
1051
+ }
1052
 
1053
+ /**
1054
+ * ffmpeg finished successfully.
1055
+ * S3 pipe: the object is already in the bucket.
1056
+ * Disk: upload the local file, then delete it whether the upload worked or not.
1057
+ */
1058
+ async function deliver_download() {
1059
+ if (use_s3_pipe) {
1060
+ s3_attempt = undefined;
1061
 
1062
+ live.size = NS_ffmpeg_v2.F_format_size(s3_bytes) || live.size;
 
 
 
 
 
 
 
1063
 
1064
+ live.percent = 99;
1065
+ push_progress({ force: true });
1066
 
1067
+ if (two_step && phase === "mp4") await delete_ts_temp();
 
1068
 
1069
  await document_and_finish();
1070
+
1071
+ return;
1072
  }
1073
 
1074
+ if (!file_with_path) {
1075
+ fail(["No local output file"]);
 
 
 
 
 
 
 
1076
 
1077
+ return;
1078
+ }
1079
+
1080
+ if (is_dev) {
1081
  finish_done();
1082
+ return;
1083
  }
1084
 
1085
+ live.percent = 99;
1086
+ push_progress({ force: true, uploading: true });
1087
 
1088
+ let uploaded = false;
 
 
1089
 
1090
+ try {
1091
+ uploaded = await upload_with_retry({ file_with_path });
1092
+ } catch (e) {
1093
+ console.warn("bucket upload error:", e, "\n");
1094
+ } finally {
1095
+ if (two_step && phase === "mp4") delete_ts_temp();
1096
+ cleanup_local();
1097
+ }
1098
 
1099
+ if (!uploaded) {
1100
+ fail(["Bucket upload failed"]);
1101
 
1102
+ return;
1103
+ }
 
 
 
 
 
1104
 
1105
+ await document_and_finish();
1106
+ }
1107
 
1108
+ async function document_and_finish() {
1109
+ // document file
1110
+ await wide_search_runner({
1111
+ document: true,
1112
+ skip: {
1113
+ local: true,
1114
+ db: true
1115
  }
1116
+ }).catch(() => {});
1117
 
1118
+ finish_done();
1119
+ }
1120
+
1121
+ async function upload_with_retry(arg0: { file_with_path: string }) {
1122
+ const { file_with_path } = arg0;
1123
+
1124
+ if (typeof file_with_path !== "string" || !file_with_path.trim().length) {
1125
  return false;
1126
  }
1127
 
1128
+ const delays = [0, 2000, 5000];
 
 
 
 
 
1129
 
1130
+ for (let i = 0; i < delays.length; i++) {
1131
+ if (delays[i]) await Bun.sleep(delays[i]!);
1132
 
1133
+ const r = await NS_bucket.upload_to_bucket({
1134
+ job_id,
1135
+ bucket_path: bucket_file_path,
1136
+ file_with_path,
1137
+ REQ0,
1138
+ skip: { probe: i > 0 }
1139
+ });
1140
 
1141
+ if (r?.success) return true;
1142
 
1143
+ if (r && !r.success && r.reason === "probe") return false;
 
 
 
 
1144
  }
1145
 
1146
+ return false;
1147
+ }
 
 
 
1148
 
1149
+ /*
1150
+ S3 PIPE: tears down the current attempt's upload and deletes whatever
1151
+ reached the bucket under ITS key. Idempotent, and safe when there is no upload.
1152
+ */
1153
+ async function abort_s3_upload() {
1154
+ const a = s3_attempt;
1155
 
1156
+ if (!a) return;
1157
 
1158
+ s3_attempt = undefined;
1159
 
1160
+ a.counter.destroy();
 
 
 
 
1161
 
1162
+ await NS_bucket.delete_self_s3({
1163
+ key: a.key,
1164
+ upload_in: a.upload,
1165
+ done: a.done
1166
+ });
1167
+ }
 
 
 
 
1168
 
1169
+ /*
1170
+ TWO STEP: deletes the temporary .ts object. Best effort, never throws.
1171
+ */
1172
+ async function delete_ts_temp() {
1173
+ if ((playback as any).i_x) return;
1174
 
1175
+ const a = ts_attempt;
 
1176
 
1177
+ if (!a) return;
 
 
 
 
 
 
1178
 
1179
+ ts_attempt = undefined;
 
1180
 
1181
+ NS_Misc.console.log({
1182
+ ENV: "all",
1183
+ _browser: false,
1184
+ _log: [`🧹: ${a.key}`, "\n"]
1185
+ });
1186
 
1187
+ try {
1188
+ await NS_bucket.delete_self_s3({
1189
+ key: a.key,
1190
+ upload_in: a.upload,
1191
+ done: a.done
1192
+ });
1193
+ } catch (e) {
1194
+ console.warn("delete_ts_temp:", e, "\n");
1195
+ }
1196
+ }
1197
 
1198
+ // ---------- reporting (hoisted) ----------
1199
 
1200
+ function relay_endpoint(data: Record<string, any>) {
1201
+ if (!progress_update_endpoint) return;
1202
 
1203
+ try {
1204
+ relay_progress_info({
1205
+ endpoint: new URL(progress_update_endpoint),
1206
+ progress_data: data
 
 
 
 
 
 
 
 
 
 
 
 
1207
  });
1208
+ } catch {}
1209
+ }
1210
+
1211
+ function push_progress(arg0: { force?: boolean; uploading?: boolean }) {
1212
+ if (live.done || live.failed || is_stream_mode) return;
1213
+
1214
+ // each push hits the store, the websocket and an outbound HTTP relay,
1215
+ // so cap it. Completion always goes through finish_done.
1216
+ const now = Date.now();
1217
+
1218
+ if (!arg0.force && now - live.last_push < 1500) return;
1219
+
1220
+ live.last_push = now;
1221
+
1222
+ // TWO STEP: which step the user is looking at
1223
+ const step_info = two_step ? { phase, stage: PHASES[phase].label } : {};
1224
 
1225
+ record_task_info({
1226
+ job_id,
1227
+ file_with_path: file_with_path || "",
1228
+ status: "running",
1229
+ info: {
1230
  uploading: arg0.uploading,
1231
  ...step_info,
1232
+ title: output_destination,
1233
  percent: live.percent,
1234
+ transcoded_size: live.size,
1235
+ progress_endpoint: progress_update_endpoint,
1236
+ ws,
1237
+ flags,
1238
  log_errors: logged_proc_errors,
1239
  logs: logged_proc_info
1240
+ }
1241
+ });
 
 
 
 
 
 
 
 
1242
 
1243
+ const data = {
1244
+ ...progress_data,
1245
+ uploading: arg0.uploading,
1246
+ ...step_info,
1247
+ percent: live.percent,
1248
+ size: live.size || progress_data?.size,
1249
+ filename: output_destination,
1250
+ location: `${REQ0.origin}/${REQ0.pathname}`,
1251
+ log_errors: logged_proc_errors,
1252
+ logs: logged_proc_info
1253
+ };
1254
 
1255
+ relay_progress_info_ws({
1256
+ channel: data_key,
1257
+ payload: {
1258
+ channel: data_key,
1259
+ status: "running",
1260
+ data
1261
+ }
1262
+ });
1263
 
1264
+ relay_endpoint(data);
1265
+ }
1266
 
1267
+ function finish_done() {
1268
+ if (live.done || live.failed || is_stream_mode) return;
 
 
 
 
 
 
 
 
 
 
 
 
1269
 
1270
+ live.done = true;
1271
+ live.percent = 100;
 
 
 
 
 
 
 
1272
 
1273
+ record_task_info({
1274
+ job_id,
1275
+ file_with_path: file_with_path || "",
1276
+ status: "done",
1277
+ info: {
1278
+ title: output_destination,
1279
  percent: 100,
1280
+ transcoded_size: live.size || progress_data?.size,
1281
+ progress_endpoint: progress_update_endpoint,
1282
+ flags,
1283
  log_errors: logged_proc_errors,
1284
  logs: logged_proc_info
1285
+ }
1286
+ });
1287
 
1288
+ relay_progress_info_ws({
1289
+ channel: data_key,
1290
+ payload: {
1291
+ channel: data_key,
1292
+ status: "done",
1293
+ filename: output_destination,
1294
+ logs: logged_proc_info
1295
  }
1296
+ });
1297
+
1298
+ relay_endpoint({
1299
+ percent: 100,
1300
+ filename: output_destination,
1301
+ location: `${REQ0.origin}/${REQ0.pathname}`,
1302
+ log_errors: logged_proc_errors,
1303
+ logs: logged_proc_info
1304
+ });
1305
+
1306
+ if (is_download_mode) {
1307
+ transcode_jobs.delete(job_id);
1308
  }
1309
+ }
1310
 
1311
+ function fail(logs: string[]) {
1312
+ if (live.failed || live.done || is_stream_mode) return;
1313
 
1314
+ live.failed = true;
1315
 
1316
+ record_task_info({
1317
+ job_id,
1318
+ file_with_path: file_with_path || "",
1319
+ status: "failed",
1320
+ info: {
1321
+ title: output_destination,
1322
+ ws,
1323
+ // TWO STEP: say which step broke
1324
+ ...(two_step ? { phase, stage: PHASES[phase].label } : {}),
1325
+ error: logs.join(" | "),
1326
+ log_errors: logged_proc_errors,
1327
+ logs: logged_proc_info
1328
+ }
1329
+ });
1330
 
1331
+ relay_progress_info_ws({
1332
+ channel: data_key,
1333
+ payload: {
1334
  channel: data_key,
 
 
 
 
 
 
 
 
 
1335
  status: "failed",
 
1336
  filename: output_destination,
 
 
1337
  logs: logged_proc_info
1338
+ }
1339
+ });
1340
 
1341
+ relay_endpoint({
1342
+ status: "failed",
1343
+ ...(two_step ? { phase } : {}),
1344
+ filename: output_destination,
1345
+ location: `${REQ0.origin}/${REQ0.pathname}`,
1346
+ log_errors: logged_proc_errors,
1347
+ logs: logged_proc_info
1348
+ });
1349
 
1350
+ passthrough?.destroy(new Error(logs[0] || "failed"));
1351
 
1352
+ cleanup_local();
 
 
1353
 
1354
+ // S3 PIPE: covers failures that did not come through on_ffmpeg_end
1355
+ // (spawn/run errors). A no-op when the upload was already torn down.
1356
+ abort_s3_upload().catch((e) => console.warn("abort_s3_upload:", e));
1357
 
1358
+ // TWO STEP: a failure in step 2 leaves the finished .ts behind: remove it
1359
+ delete_ts_temp().catch(() => {});
1360
+ }
1361
 
1362
+ // ---------- helpers (hoisted) ----------
 
 
 
 
 
 
1363
 
1364
+ /**
1365
+ * Deletes the local output file. Safe to call from every exit path: it
1366
+ * only runs once per attempt. Skipped in dev, where the local file is the
1367
+ * store. No-op in S3 pipe mode, there is no local file.
1368
+ */
1369
+ function cleanup_local() {
1370
+ if (!file_with_path || local_cleaned) return;
1371
 
1372
+ if (is_dev && live.done) return;
1373
 
1374
+ local_cleaned = true;
 
1375
 
1376
+ NS_bucket.delete_self({ try_count: 0, file_with_path });
1377
+ }
1378
 
1379
+ function kill_controller(reason: string) {
1380
+ controller.abort();
1381
 
1382
+ console.log(`[CONTROLLER] aborted: ${reason}`);
1383
+ }
1384
 
1385
+ function set_t_timeout() {
1386
+ kill_controller_timeout();
1387
+
1388
+ watchdog = setTimeout(
1389
+ () => kill_controller("no output from ffmpeg"),
1390
+ NS_ffmpeg_v2.TIMEOUT_MS
1391
+ );
1392
+ }
1393
+
1394
+ function kill_controller_timeout() {
1395
+ if (watchdog) {
1396
+ clearTimeout(watchdog);
1397
+ watchdog = undefined;
1398
  }
1399
+ }
1400
 
1401
+ function id_log_errors(arg0: { logs: string[] }) {
1402
+ const { logs } = arg0;
1403
+
1404
+ if (!Array.isArray(logs)) {
1405
+ return;
1406
  }
1407
 
1408
+ const has = (needle: string) => {
1409
+ return logs.some((xx) =>
1410
+ xx.toString().toLowerCase().includes(needle.toLowerCase())
1411
+ );
1412
+ };
1413
 
1414
+ if (has("http error 429 too many requests")) {
1415
+ error_429 = true;
1416
+ }
1417
 
1418
+ if (has("failed too many times, skipping")) {
1419
+ error_429 = true;
1420
+ }
 
 
1421
 
1422
+ if (has("error opening input: end of file")) {
1423
+ error_end_of_file = true;
1424
+ }
1425
 
1426
+ if (has("error opening input files: server returned 403 forbidden")) {
1427
+ error_403 = true;
1428
+ }
1429
 
1430
+ if (has("http error 404 not found")) {
1431
+ error_403 = true;
1432
+ }
1433
+ }
1434
 
1435
+ // keep subtitles unless one of THEIR urls caused the end-of-file error
1436
+ function F_keep_subs() {
1437
+ if (!logged_proc_info.length || !subtitles?.length) {
1438
+ return true;
1439
+ }
1440
 
1441
+ const eof_lines = logged_proc_info
1442
+ .map((xx) => xx.toString().toLowerCase())
1443
+ .filter((xx) => xx.includes("error opening input: end of file"));
1444
+
1445
+ if (!eof_lines.length) {
1446
+ return true;
1447
  }
1448
 
1449
+ const sub_failed = subtitles.some((sub) => {
1450
+ const url = sub.url.toString().toLowerCase();
 
 
 
1451
 
1452
+ return eof_lines.some((line) => line.includes(url));
1453
+ });
 
1454
 
1455
+ return !sub_failed;
1456
+ }
 
1457
 
1458
+ /**
1459
+ * Sends a playback through the cloudflare proxy (used for the 403/429
1460
+ * fallback and for private bucket urls). Returns the input unchanged when
1461
+ * no proxy is available.
1462
+ */
1463
+ async function proxy_playback(p: T_playback): Promise<T_playback> {
1464
+ // get_hf_sia_proxy
1465
+ const R_ = IS_HF()
1466
+ ? await NS_scrape_post_processing_helper.get_cloudflare_proxy({})
1467
+ : await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
1468
 
1469
+ if (!R_) return p;
 
1470
 
1471
+ const proxy_base = R_.available || R_.fallback;
 
1472
 
1473
+ if (NS_routes_c.F_is_url(proxy_base)) {
1474
+ const _ = await NS_pack_proxima.F_handle_pack_encode({
1475
+ base: proxy_base,
1476
+ payload: {
1477
+ endpoint: p.url,
1478
+ commands: {
1479
+ use_h_version: true,
1480
+ redirect_origin: true,
1481
+ headers: p.headers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1482
  }
1483
+ }
1484
+ });
1485
 
1486
+ if (_) {
1487
+ const __ = { ...p };
1488
 
1489
+ __.url = _.toString();
1490
 
1491
+ delete __.headers;
1492
 
1493
+ return __;
 
1494
  }
 
 
1495
  }
1496
 
1497
+ return p;
1498
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
1499
 
1500
+ /**
1501
+ * TWO STEP: the playback ffmpeg reads in step 2, i.e. the .ts object that
1502
+ * step 1 left in the bucket.
1503
+ *
1504
+ * The HLS source's headers (user agent, referer, ...) are deliberately NOT
1505
+ * carried over, they do not belong on a bucket request.
1506
+ */
1507
+ async function ts_playback(): Promise<T_playback> {
1508
+ if ((playback as any).i_x) {
1509
+ return playback;
1510
  }
1511
 
1512
+ const { url, headers } = NS_bucket.bucket_file_url({ key: ts_key });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1513
 
1514
+ const p = { ...playback, url, headers, type: "video" } as T_playback;
 
 
 
1515
 
1516
+ // private bucket: let the proxy attach the auth header. ffmpeg would send
1517
+ // it to the CDN host the bucket redirects to as well, which rejects it.
1518
+ return headers ? await proxy_playback(p) : p;
1519
+ }
1520
+
1521
+ async function F_cmd(arg0: { subs?: boolean }) {
1522
+ const { subs } = arg0;
1523
 
1524
+ // TWO STEP, step 2: input is the .ts in the bucket, output is the mp4 on
1525
+ // stdout (delivery_mode "stream" is what your old "s3" mode mapped to)
1526
+ if (phase === "mp4") {
1527
+ has_subs = subs === false ? false : true;
1528
 
1529
+ return await NS_ffmpeg_v2.F_build_ffmpeg_flags_t1({
1530
+ playback: await ts_playback(),
1531
+ subtitles: subs === false ? undefined : subtitles,
1532
  probe_metadata,
1533
+ delivery_format,
1534
+ delivery_mode: "download",
1535
  output_destination: file_with_path || "",
1536
  flash_ping: false,
1537
+ dl_to_s3: false
1538
  });
 
 
1539
  }
1540
 
1541
+ const new_playback = await (async () => {
1542
+ if ((!error_429 && !error_403) || playback.url.includes("/proxima")) {
1543
+ return playback;
 
 
 
 
 
 
1544
  }
1545
 
1546
+ return await proxy_playback(playback);
1547
+ })();
1548
 
1549
+ // TWO STEP: a .ts cannot carry mov_text subtitles, they are added in step 2
1550
+ has_subs = two_step ? false : subs === false ? false : true;
 
1551
 
1552
+ const data = await NS_ffmpeg_v2.F_build_ffmpeg_flags_t1({
1553
+ playback: new_playback,
1554
+ subtitles: two_step || subs === false ? undefined : subtitles,
1555
+ probe_metadata,
1556
+ delivery_format: two_step ? "ts" : delivery_format,
1557
+ delivery_mode,
1558
+ output_destination: file_with_path || "",
1559
+ flash_ping: false,
1560
+ dl_to_s3: use_s3_pipe
1561
+ });
1562
 
1563
+ return data;
1564
+ }
 
 
1565
 
1566
+ function construct_response_headers(arg0: {
1567
+ is_download?: boolean;
1568
+ is_streaming?: boolean;
1569
+ }) {
1570
+ const { is_streaming, is_download } = arg0;
 
 
 
 
1571
 
1572
+ /* ---------- set response headers ---------- */
1573
+ if (is_download || is_streaming) {
1574
+ ctx.set.headers["content-type"] = out_mime;
1575
+ }
1576
 
1577
+ ctx.set.headers["cache-control"] = "no-cache";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1578
 
1579
+ if (is_download) {
1580
+ ctx.set.headers["content-disposition"] =
1581
+ `attachment; filename="${output_destination}"`;
 
 
1582
 
1583
+ ctx.set.headers["accept-ranges"] = "bytes";
1584
+ }
1585
+
1586
+ if (clean_metadata?.format?.duration) {
1587
+ ctx.set.headers["X-Content-Duration"] = clean_metadata.format?.duration;
1588
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1589
  }
 
1590
 
1591
+ async function wide_search_runner(arg0: {
1592
+ skip?: {
1593
+ local?: boolean;
1594
+ db?: boolean;
1595
+ bucket?: boolean;
1596
+ };
1597
+ document: boolean;
1598
+ }) {
1599
+ const { skip, document } = arg0;
1600
+
1601
+ if (!is_download_mode) return;
1602
+
1603
+ const [E_, R_] = await NS_promise.F_catch_promise({
1604
+ promise: NS_showtime_v2_helper.find_seed_file({
1605
+ sbe_id,
1606
+ output_destination: {
1607
+ og: output_destination,
1608
+ clean: output_destination_clean,
1609
+ file_with_path
1610
+ },
1611
+ picture_quality,
1612
+ qLevel,
1613
+ sbe_s_e,
1614
+ cache_folder,
1615
+ file_id: clean_file_id,
1616
+ job_id,
1617
+ S3Dir,
1618
+ cleanup_corruption: true,
1619
+ document,
1620
+ ideal_runtime: playback.ideal_runtime,
1621
+ skip,
1622
+ has_subs
1623
+ })
1624
+ });
1625
+
1626
+ if (R_ && "stream_link" in R_ && R_.stream_link) {
1627
+ if (!NS_misc.isValidUrlFormat(R_.stream_link)) {
1628
+ return;
1629
+ }
1630
+ }
1631
+
1632
+ return R_;
1633
+ }
1634
+ }
1635
 
1636
  function F_stderr_parse_progress(arg0: {
1637
  stderr_line: string;
src/sub-router/stream.ts CHANGED
@@ -25,6 +25,7 @@ import { NS_showtime_v2_helper } from "../lib/dl-showtime-helper";
25
  import { once } from "node:events";
26
  import { NS_Misc } from "@x/core/client/misc.ts";
27
  import { NS_bucket } from "../lib/bucket";
 
28
 
29
  type Range = { offset: number; length: number };
30
  type Init = { url: string; key: Key; range?: Range };
@@ -93,1012 +94,1061 @@ const sub_router = new Elysia({ prefix: "/stream" }).get(
93
  return metadata;
94
  }
95
 
96
- const { playback, probe_metadata, sbe_id, progress_update_endpoint } =
97
- metadata as T_delivery_payload;
 
 
 
 
 
 
 
 
98
 
99
- let { output_destination, delivery_mode, delivery_format, out_mime } =
100
- metadata as T_delivery_payload;
 
 
101
 
102
- const x_i = REQ0.searchParams.get("x_i");
103
- const xc = REQ0.searchParams.has("xc");
104
 
105
- const two_step = x_i && x_i.length ? true : false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
- const picture_quality = (() => {
108
- const _ = playback.picture_quality;
 
 
 
 
 
 
 
 
 
 
 
109
 
110
- if (typeof _ !== "string") {
111
- return "";
112
- }
113
 
114
- return _.trim().split(" ").join("");
115
- })();
116
 
117
- const sbe_s_e = (() => {
118
- const _ = metadata.sbe_s_e;
119
 
120
- if (typeof _ !== "string" || _.toLowerCase().includes("nan")) {
121
- return;
122
- }
123
 
124
- return _.trim().split(" ").join("");
125
- })();
126
 
127
- const qLevel = (() => {
128
- if (
129
- typeof metadata.qLevel === "string" &&
130
- metadata.qLevel.trim().length > 0
131
- ) {
132
- return metadata.qLevel.trim();
133
- }
134
 
135
- const _ = NS_ffmpeg_v2.F_get_qLevel({
136
- metadata: probe_metadata
137
- });
138
 
139
- return _;
140
- })();
141
 
142
- const ws = (() => {
143
- const is_https = new URL(REQ_ORIGIN).protocol === "https:";
 
144
 
145
- const ws_protocol = is_https ? "wss" : "ws";
 
146
 
147
- const _ = `${ws_protocol}://${REQ0.host}/ws`;
 
 
 
 
 
 
148
 
149
- const data = {
150
- connect: _,
151
- channel: data_key
152
- };
153
 
154
- return data;
155
- })();
 
 
 
156
 
157
- let segments: Segment[] | undefined = undefined;
158
 
159
- let active_playback = playback;
160
- let proxied = false;
161
- let init: Init | undefined = undefined;
162
 
163
- const PHASES = {
164
- single: { from: 0, to: 99, label: "Processing" },
165
- ts: { from: 0, to: 60, label: "Step 1/2: fetching video" },
166
- mp4: { from: 60, to: 99, label: "Step 2/2: converting to MP4" }
167
- } as const;
168
 
169
- type Phase = keyof typeof PHASES;
 
170
 
171
- let phase: Phase = "single";
172
 
173
- out_mime = "video/mp2t";
174
- delivery_format = "ts";
 
175
 
176
- try {
177
- const seg_error = await get_segments({ _playback: playback });
 
 
 
178
 
179
- if (seg_error) {
180
- return ctx.status(422, seg_error);
181
- }
182
- } catch (error) {
183
- throw error;
184
- }
185
 
186
- await (async () => {
187
- if (!two_step) return;
188
 
189
- delivery_mode = "s3";
 
190
 
191
- phase = "ts";
192
- })();
193
 
194
- if (delivery_mode !== "s3") {
195
- return ctx.status(400, "Unsupported delivery mode or format");
 
 
196
  }
 
 
 
197
 
198
- const ext_key = (() => {
199
- const _ = NS_files.F_get_file_extension({
200
- filename: output_destination,
201
- return_new_filename: true
202
- });
203
 
204
- return `${_.new_filename}.${delivery_format}`;
205
- })();
206
 
207
- output_destination = ext_key;
 
208
 
209
- const output_destination_clean = NS_misc.cleanNameV2(output_destination);
 
210
 
211
- const clean_file_id = (() => {
212
- return `${picture_quality}-${output_destination_clean}`.toLowerCase();
213
- })();
214
 
215
- const job_id = x_i || `${sbe_id}-${clean_file_id}`;
 
 
 
 
216
 
217
- const file_id = `${Date.now()}-${clean_file_id}`;
 
218
 
219
- const cache_folder = two_step ? `cacheStream/${sbe_id}` : `cache/${sbe_id}`;
220
 
221
- const bucket_file_path = (() => {
222
- const _ = `${cache_folder}/${file_id}`;
223
 
224
- return _;
225
- })();
 
226
 
227
- const MAX_RETRIES = 3;
228
-
229
- const err_message = (err: unknown) =>
230
- NS_error.F_collect_error_message({ ERR: err }).message;
231
-
232
- // -------------------------------
233
- // Shared job state
234
- // -------------------------------
235
- // one object that the segment and S3 callbacks write to, so neither one wipes out the other's numbers
236
- const live = {
237
- percent: 0,
238
- size: "",
239
- failed: false,
240
- done: false,
241
- launched: false,
242
- last_push: 0 // throttle for push_progress
243
- };
244
 
245
- // bumped on every (re)start so callbacks from an older attempt can be ignored
246
- let attempt = 0;
247
 
248
- // -------------------------------
249
- // Setup abort handling
250
- // -------------------------------
251
- // The request signal only matters until the job has launched. After we answered the client the job runs in the background, and a client disconnect must not kill it.
252
- let controller = new AbortController();
253
 
254
- const abort_signal = ctx?.request.signal;
 
255
 
256
- const on_request_abort = () => kill_controller({ cleanup_files: true });
 
257
 
258
- const detach_request_abort = () =>
259
- abort_signal?.removeEventListener("abort", on_request_abort);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- if (abort_signal?.aborted) {
262
- kill_controller({ cleanup_files: true });
263
 
264
- return;
265
- }
 
 
 
266
 
267
- abort_signal?.addEventListener("abort", on_request_abort, { once: true });
268
 
269
- let error_429 = false;
270
- let error_403 = false;
271
 
272
- // true once THIS request recorded the "running" claim for job_id
273
- let claimed = false;
274
 
275
- // phase-local percent (0-100) -> overall percent, never above 99
276
- function to_overall(phase_pct: number) {
277
- const p = PHASES[phase];
278
 
279
- return Math.min(
280
- 99,
281
- Math.max(p.from, p.from + (phase_pct / 100) * (p.to - p.from))
282
- );
283
- }
284
 
285
- try {
286
- const current_job = (() => {
287
- const _ = transcode_jobs.get(job_id);
288
 
289
- if (!_ || xc) return;
 
290
 
291
- return _;
292
- })();
293
 
294
- if (current_job?.status === "running") {
295
- const percent = current_job?.percent;
296
- const percentage = typeof percent === "number" ? `${percent} %` : "";
297
- const transcoded_size = current_job?.transcoded_size;
298
- const progress_endpoint =
299
- current_job?.progress_endpoint || progress_update_endpoint;
300
 
301
- const message = percentage
302
- ? `ENGINE PROGRESS: ${percentage}`
303
- : "ENGINE PROCESSING ...";
 
 
304
 
305
- return {
306
- processing: true,
307
- message,
308
- percent,
309
- transcoded_size,
310
- // TWO STEP: lets a re-polling client show which step is running
311
- phase: current_job?.phase,
312
- stage: current_job?.stage,
313
- track_process: progress_endpoint,
314
- file: output_destination,
315
- job_id,
316
- output_destination,
317
- qLevel,
318
- picture_quality,
319
- ws,
320
- flags: current_job.flags,
321
- logs: current_job.logs,
322
- ...(ops === "just_check" ? { passed: true } : {})
323
- };
324
- }
325
 
326
- if (current_job?.status === "failed") {
327
- const logs = current_job?.logs;
328
 
329
- if (logs) id_log_errors({ logs });
 
330
 
331
- if (ops === "just_check") {
332
- return {
333
- passed: false,
334
- delivery: output_destination,
335
- track_process: progress_update_endpoint,
336
- ws,
337
- flags: current_job.flags,
338
- logs: current_job?.logs
339
- };
340
- }
341
 
342
- transcode_jobs.delete(job_id);
 
 
343
 
344
- const probe_playback = await new_playback();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
- const _ = await ping_probe({ playback: probe_playback });
 
347
 
348
- if (!_.success) {
349
- throw new Error("Something went wrong: with probe");
350
- }
351
- }
352
 
353
- const seed_file = await (async () => {
354
- if (ops === "cmd") {
355
- return;
356
- }
 
 
 
 
 
 
357
 
358
- const _ = current_job?.db_metadata?.stream_link;
359
 
360
- if (_) {
361
- return current_job.db_metadata;
362
- }
363
 
364
- const __ = await wide_search_runner({
365
- document: true,
366
- i_x: two_step ? true : false,
367
- skip: { local: true }
368
- });
369
 
370
- return __;
371
- })();
 
 
372
 
373
- // wide search runner >> seed
374
- const _ = await (async () => {
375
- if (!seed_file) return;
376
-
377
- const file_size_bytes = parseFloat(`${seed_file?.size}`);
378
-
379
- const percent = 100;
380
- const transcoded_size = file_size_bytes
381
- ? NS_ffmpeg_v2.F_format_size(file_size_bytes)
382
- : current_job?.transcoded_size;
383
- const progress_endpoint =
384
- current_job?.progress_endpoint || progress_update_endpoint;
385
-
386
- if (!current_job?.db_metadata?.stream_link) {
387
- record_task_info({
388
- job_id,
389
- status: "done",
390
- info: {
391
- title: output_destination,
392
- percent,
393
- transcoded_size,
394
- ...("stream_link" in seed_file && seed_file.stream_link
395
- ? { db_metadata: seed_file }
396
- : {})
397
- }
398
- });
399
- }
400
 
401
- const _ =
402
- seed_file && "stream_link" in seed_file && seed_file.stream_link;
403
 
404
- if (two_step && _) {
405
- return {};
406
- }
407
 
408
- const new_stream_link = await (async () => {
409
- if (!_) {
410
- return;
411
- }
 
412
 
413
- const stream_link = new URL(seed_file.stream_link);
414
- stream_link.searchParams.set("download", "true");
415
-
416
- const [E_, R_] = await NS_promise.F_catch_promise({
417
- promise: (async () => {
418
- const cloudflare =
419
- await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
420
-
421
- if (!cloudflare.available) return;
422
-
423
- const _ = await NS_pack_proxima.F_handle_pack_encode({
424
- base: new URL(cloudflare.available),
425
- payload: {
426
- endpoint: stream_link.toString(),
427
- commands: {
428
- response_headers: {
429
- set: {
430
- "content-disposition": `attachment; filename="${output_destination}"`
431
- }
432
- },
433
- use_h_version: true
434
- }
435
- }
436
- });
437
 
438
- return { proxy_url: _ };
439
- })()
440
- });
441
 
442
- const new_stream_link =
443
- (R_ as any)?.proxy_url.toString() || stream_link.toString();
444
 
445
- return new_stream_link;
446
- })();
 
 
 
 
447
 
448
- if (ops === "just_check") {
449
- return {
450
- passed: true,
451
- ready: true,
 
 
452
  percent,
453
  transcoded_size,
454
- output_destination,
455
- flags: current_job?.flags,
456
- logs: current_job?.logs,
457
- ...(new_stream_link ? { stream_link: new_stream_link } : {}),
458
- track_process: progress_endpoint
459
- };
460
- }
461
 
462
- if (new_stream_link) return ctx.redirect(new_stream_link, 302);
463
- })();
464
 
465
- if (_) {
466
- return _;
467
  }
468
 
469
- let _playback = await new_playback();
 
 
 
470
 
471
- claimed = true;
 
472
 
473
- record_task_info({
474
- job_id,
475
- status: "running",
 
476
 
477
- info: {
478
- title: output_destination,
479
- ws,
480
- progress_endpoint: progress_update_endpoint
481
- }
482
- });
483
 
484
- // the client gave up while we were resolving the playlist
485
- if (controller.signal.aborted) {
486
- return;
487
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
  if (ops === "just_check") {
490
  return {
491
  passed: true,
492
- segments,
493
- playback: _playback
 
 
 
 
 
 
494
  };
495
  }
496
 
497
- spawn({ retry: 0 });
 
498
 
499
- const message = `COMPILER ENGINE started, this may take a while. ${progress_update_endpoint ? `check process -> ${progress_update_endpoint}` : "check back later"}`;
 
 
500
 
501
- live.launched = true;
502
 
503
- detach_request_abort();
504
 
505
- return {
506
- message,
 
 
 
507
  title: output_destination,
508
  ws,
509
- track_process: progress_update_endpoint
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  };
 
511
 
512
- /*
513
- Starts (or restarts) the download -> upload pipeline.
514
- */
515
- async function spawn(arg0: { retry: number }) {
516
- const { retry } = arg0;
517
 
518
- if (retry >= MAX_RETRIES) return;
519
 
520
- try {
521
- if (retry === 0) {
522
- if (controller.signal.aborted) return;
523
- } else {
524
- const next_playback = await new_playback();
525
 
526
- if (next_playback === _playback) return;
527
 
528
- _playback = next_playback;
 
 
 
 
 
529
 
530
- controller.abort();
531
- controller = new AbortController();
 
 
 
532
 
533
- live.done = false;
534
- live.failed = false;
535
- live.percent = 0;
536
- live.size = "";
537
 
538
- const res = await get_segments({ _playback });
 
 
 
 
539
 
540
- if (res?.error) {
541
- fail([res.error]);
542
 
543
- return;
544
- }
545
- }
546
 
547
- const current_segments = segments;
 
548
 
549
- if (!current_segments?.length) {
550
- fail(["No segments to upload"]);
 
 
 
 
 
 
 
551
 
552
  return;
553
  }
 
554
 
555
- const my_attempt = ++attempt;
556
- const stale = () => my_attempt !== attempt;
557
 
558
- const bytes = segmentStream({
559
- segments: current_segments,
560
- init,
561
- headers: _playback.headers || {},
562
- concurrency: 6,
563
- ac: controller,
564
- onError: (arg0) => {
565
- if (is_dev) console.error("💠", JSON.stringify(arg0), "\n");
566
 
567
- if (stale()) return;
 
568
 
569
- fail([err_message(arg0.err)]);
570
- },
571
- onSegment: (arg0) => {
572
- if (is_dev) console.log("💠", JSON.stringify(arg0), "\n");
573
 
574
- if (stale()) return;
 
 
 
 
 
 
 
575
 
576
- live.percent = arg0.percent;
577
 
578
- push_progress({ size: live.size, percent: live.percent });
579
- }
580
- });
 
581
 
582
- putStreamWriter({
583
- stream: bytes,
584
- ContentType: out_mime,
585
- key: bucket_file_path,
586
- segmentsTotal: current_segments.length,
587
- ac: controller,
588
- onError: (arg0) => {
589
- if (is_dev) console.error("💠", JSON.stringify(arg0), "\n");
590
 
591
- if (stale()) return;
592
 
593
- fail([err_message(arg0.err)]);
594
- },
595
- onProgress: (s) => {
596
- if (is_dev) console.log("💠", JSON.stringify(arg0), "\n");
597
 
598
- if (stale()) return;
 
 
 
 
 
 
 
599
 
600
- live.size = s.size || live.size;
601
 
602
- if (s.state === "running") {
603
- push_progress({ size: live.size, percent: live.percent });
604
- }
 
 
 
 
 
 
 
 
605
  }
606
- }).then(
607
- (r) => {
608
- if (stale()) return;
609
-
610
- live.done = true;
611
- live.size = NS_files.F_format_size(r.bytes) || live.size;
612
- live.percent = 100;
613
-
614
- push_done({ size: live.size });
615
-
616
- try {
617
- // document file
618
- (async () => {
619
- const _ = await wide_search_runner({
620
- document: two_step ? false : true,
621
- i_x: two_step ? true : false,
622
- skip: {
623
- local: true,
624
- db: true
625
- }
626
- });
627
- })();
628
-
629
- NS_Misc.console.log({
630
- ENV: "all",
631
- _browser: false,
632
- _log: [
633
- `🏁 SEG STREAM finished [${phase}]: (${live.percent} %) -> ${live.size} -> ${output_destination}`,
634
- "\n"
635
- ]
636
  });
637
- } catch (e) {
638
- console.warn("post-done step failed:", e);
639
- }
640
- },
641
- (err) => {
642
- if (stale()) return;
 
 
 
 
 
 
 
 
 
 
643
 
644
- console.warn("RUN ERROR:", err);
645
 
646
- fail([err_message(err)]);
647
 
648
- spawn({ retry: retry + 1 });
649
- }
650
- );
651
- } catch (err) {
652
- console.warn("spawn error:", err);
653
 
654
- fail([err_message(err)]);
655
- }
656
  }
657
- } finally {
658
- if (!live.launched) {
659
- detach_request_abort();
 
660
 
661
- if (claimed && transcode_jobs.get(job_id)?.status === "running") {
662
- transcode_jobs.delete(job_id);
663
- }
664
  }
665
  }
 
666
 
667
- /*
668
- Returns the playback to use. Switches to the proxy only after a
669
- 403/429, only once, and returns the SAME object when there is nothing new to try (callers compare by reference).
670
- */
671
- async function new_playback() {
672
- if (
673
- (!error_429 && !error_403) ||
674
- proxied ||
675
- playback.url.toString().includes("/proxima")
676
- ) {
677
- return active_playback;
678
- }
679
 
680
- const R_ = await NS_scrape_post_processing_helper.get_cloudflare_proxy(
681
- {}
682
- );
683
 
684
- if (!R_) return playback;
685
 
686
- const proxy_base = R_.available || R_.fallback;
687
 
688
- if (NS_routes_c.F_is_url(proxy_base)) {
689
- const _ = await NS_pack_proxima.F_handle_pack_encode({
690
- base: proxy_base,
691
- payload: {
692
- endpoint: playback.url.toString(),
693
- commands: {
694
- use_h_version: true,
695
- redirect_origin: true,
696
- headers: playback.headers
697
- }
698
  }
699
- });
 
700
 
701
- if (_) {
702
- const __ = { ...playback };
703
 
704
- __.url = _.toString();
705
 
706
- delete __.headers;
707
 
708
- proxied = true;
709
- active_playback = __;
710
 
711
- return __;
712
- }
713
  }
714
-
715
- return active_playback;
716
  }
717
 
718
- async function get_segments(arg0: {
719
- _playback: T_delivery_payload["playback"];
720
- }): Promise<{ error: string } | undefined> {
721
- let { _playback } = arg0;
722
 
723
- const resolve = () =>
724
- NS_promise.F_catch_promise({
725
- promise: resolveMediaPlaylist({
726
- url: _playback.url,
727
- headers: _playback.headers || {}
728
- })
729
- });
730
 
731
- let [E_, R_] = await resolve();
 
 
 
 
 
 
732
 
733
- if (E_ || !R_) {
734
- const { message, status } = NS_error.F_collect_error_status_n_message({
735
- ERR: E_
736
- });
737
 
738
- id_log_errors({ logs: [message] });
 
 
 
739
 
740
- if (error_403 || error_429) {
741
- _playback = await new_playback();
742
 
743
- [E_, R_] = await resolve();
 
744
 
745
- if (E_ || !R_) {
746
- const { message, status } =
747
- NS_error.F_collect_error_status_n_message({
748
- ERR: E_
749
- });
750
 
751
- fail([message]);
 
 
 
 
 
752
 
753
- throw NS_error.F_create_error({ status, message });
754
- }
755
- } else {
756
  fail([message]);
757
 
758
  throw NS_error.F_create_error({ status, message });
759
  }
 
 
 
 
760
  }
 
761
 
762
- const { url, text, separate_audio } = R_!;
763
 
764
- const media = parseMedia({ text, base: url });
765
 
766
- if (!media.segments.length) {
767
- return { error: "No segments found" };
768
- }
769
-
770
- if (!media.ended) {
771
- return { error: "Live playlists are not supported" };
772
- }
773
 
774
- if (media.initUrl || media?.init?.url) {
775
- if (!two_step) {
776
- // return { error: "fMP4/CMAF needs remuxing, not supported" };
777
- }
778
 
779
- out_mime = "video/mp4";
780
- delivery_format = "mp4";
 
781
  }
782
 
783
- if (media.init) {
784
- // what is still NOT possible without ffmpeg
785
- if (media.init_changes) {
786
- return {
787
- error:
788
- "Playlist switches init segments (discontinuity), needs remuxing"
789
- };
790
- }
791
- }
792
 
793
- if (separate_audio) {
794
- return { error: "Audio is a separate track, needs muxing" };
 
 
 
 
 
795
  }
 
796
 
797
- init = media.init;
798
- segments = media.segments;
799
  }
800
 
801
- // ---------- helpers (hoisted) ----------
 
 
802
 
803
- function id_log_errors(arg0: { logs: string[] }) {
804
- const { logs } = arg0;
805
 
806
- if (!Array.isArray(logs)) {
807
- return;
808
- }
809
 
810
- logs.forEach((xx) => {
811
- const _ = xx.toString().toLowerCase();
812
-
813
- if (
814
- _.includes(
815
- "Error opening input files: Server returned 403 Forbidden".toLowerCase()
816
- ) ||
817
- _.includes("Fetch failed: status: 403".toLowerCase()) ||
818
- _.includes("Segment failed (403)".toLowerCase()) ||
819
- _.includes("Segment failed (404)".toLowerCase()) ||
820
- _.includes("http error 404 not found".toLowerCase())
821
- ) {
822
- error_403 = true;
823
- }
824
-
825
- if (
826
- _.includes("HTTP error 429 Too Many Requests".toLowerCase()) ||
827
- _.includes("Fetch failed: status: 429".toLowerCase()) ||
828
- _.includes("Segment failed (429)".toLowerCase()) ||
829
- _.includes("failed too many times, skipping".toLowerCase())
830
- ) {
831
- error_429 = true;
832
- }
833
- });
834
  }
835
 
836
- function fail(logs: string[]) {
837
- if (live.failed || live.done) return;
838
 
839
- live.failed = true;
 
 
 
 
 
 
 
 
 
 
840
 
841
- id_log_errors({ logs: logs });
 
 
 
 
 
 
 
 
 
842
 
843
- push_failed({ logs, size: live.size });
844
- }
845
 
846
- function push_failed(arg0: { size?: string; logs: string[] }) {
847
- const { size, logs } = arg0,
848
- percent = live.percent,
849
- title = output_destination,
850
- transcoded_size = size || live.size || "",
851
- status = "failed";
852
 
853
- record_task_info({
854
- job_id,
855
- status,
856
- info: {
857
- title,
858
- percent: percent && two_step ? to_overall(percent) : percent,
859
- transcoded_size,
860
- ws,
861
- ...(two_step ? { phase, stage: PHASES[phase].label } : {}),
862
- logs
863
- }
864
- });
865
 
866
- relay_progress_info_ws({
867
- channel: data_key,
868
- payload: {
869
- channel: data_key,
870
- status,
871
- title,
872
- percent: percent && two_step ? to_overall(percent) : percent,
873
- transcoded_size,
874
- logs
875
- }
876
- });
877
 
878
- relay_progress_info({
879
- endpoint: progress_update_endpoint
880
- ? new URL(progress_update_endpoint)
881
- : undefined,
882
- progress_data: {
883
- status,
884
- title,
885
- percent: percent && two_step ? to_overall(percent) : percent,
886
- ...(two_step ? { phase } : {}),
887
- location: `${REQ0.origin}/${REQ0.pathname}`,
888
- logs
889
- }
890
- });
891
- }
 
 
 
 
 
892
 
893
- function push_done(arg0: { size?: string }) {
894
- const { size } = arg0,
895
- percent = 100,
896
- title = output_destination,
897
- transcoded_size = size || live.size || "",
898
- status = "done";
 
 
 
 
 
899
 
900
- record_task_info({
901
- job_id,
 
 
 
902
  status,
903
- info: {
904
- title,
905
- percent: percent && two_step ? to_overall(percent) : percent,
906
- transcoded_size,
907
- progress_endpoint: progress_update_endpoint
908
- }
909
- });
 
910
 
911
- relay_progress_info_ws({
912
- channel: data_key,
913
- payload: {
914
- channel: data_key,
915
- status,
916
- data: {
917
- status,
918
- title,
919
- percent: percent && two_step ? to_overall(percent) : percent,
920
- transcoded_size
921
- }
922
- }
923
- });
 
 
 
 
924
 
925
- relay_progress_info({
926
- endpoint: progress_update_endpoint
927
- ? new URL(progress_update_endpoint)
928
- : undefined,
929
- progress_data: {
 
930
  status,
931
- percent: percent && two_step ? to_overall(percent) : percent,
932
  title,
933
- location: `${REQ0.origin}/${REQ0.pathname}`
 
934
  }
935
- });
936
- }
937
 
938
- function push_progress(arg0: { size: string; percent?: number }) {
939
- if (live.done || live.failed) return;
 
 
 
 
 
 
 
 
 
 
940
 
941
- const now = Date.now();
 
942
 
943
- if (now - live.last_push < 1500) return;
944
 
945
- live.last_push = now;
946
 
947
- const { size, percent } = arg0,
948
- transcoded_size = size || live.size || "",
949
- status = "running";
950
 
951
- const step_info = two_step ? { phase, stage: PHASES[phase].label } : {};
 
 
952
 
953
- record_task_info({
954
- job_id,
955
- status,
956
- info: {
957
- title: output_destination,
958
- percent: percent && two_step ? to_overall(percent) : percent,
959
- transcoded_size,
960
- ...step_info,
961
- progress_endpoint: progress_update_endpoint,
962
- ws
963
- }
964
- });
965
 
966
- const data = {
967
- status,
 
 
968
  title: output_destination,
969
  percent: percent && two_step ? to_overall(percent) : percent,
970
  transcoded_size,
971
  ...step_info,
972
- location: `${REQ0.origin}/${REQ0.pathname}`
973
- };
 
 
974
 
975
- relay_progress_info_ws({
976
- channel: data_key,
977
- payload: {
978
- channel: data_key,
979
- status,
980
- data
981
- }
982
- });
983
 
984
- relay_progress_info({
985
- endpoint: progress_update_endpoint
986
- ? new URL(progress_update_endpoint)
987
- : undefined,
988
- progress_data: data
989
- });
990
- }
 
991
 
992
- function kill_controller(arg0: { cleanup_files?: boolean }) {
993
- const { cleanup_files } = arg0;
 
 
 
 
 
994
 
995
- controller.abort();
 
996
 
997
- // user aborted before the job was launched: mark it failed. Any
998
- // multipart upload is cleaned up by putStreamWriter when the abort
999
- // reaches it
1000
- if (cleanup_files && !live.done) {
1001
- fail(["Request aborted"]);
1002
- }
1003
 
1004
- console.log("[CONTROLLER] Request aborted");
 
 
 
 
1005
  }
1006
 
1007
- async function wide_search_runner(arg0: {
1008
- skip?: {
1009
- local?: boolean;
1010
- db?: boolean;
1011
- bucket?: boolean;
1012
- };
1013
- document: boolean;
1014
- i_x?: boolean;
1015
- }) {
1016
- const { skip, document, i_x } = arg0;
1017
-
1018
- const [E_, R_] = await NS_promise.F_catch_promise({
1019
- promise: NS_showtime_v2_helper.find_seed_file({
1020
- sbe_id,
1021
- output_destination: {
1022
- og: output_destination,
1023
- clean: output_destination_clean
1024
- },
1025
- picture_quality,
1026
- qLevel,
1027
- sbe_s_e,
1028
- cache_folder,
1029
- file_id: clean_file_id,
1030
- job_id,
1031
- cleanup_corruption: true,
1032
- document,
1033
- ideal_runtime: playback.ideal_runtime,
1034
- skip,
1035
- has_subs: false
1036
- })
1037
- });
1038
 
1039
- if (R_ && "stream_link" in R_ && R_.stream_link) {
1040
- if (!NS_misc.isValidUrlFormat(R_.stream_link)) {
1041
- return;
1042
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
 
1044
- if (i_x) {
1045
- const _ = new URL(`/showtimeV2/delivery/${data_key}`, REQ_ORIGIN);
1046
 
1047
- _.searchParams.set("i_x", R_.stream_link);
1048
- _.searchParams.set("xc", "");
1049
 
1050
- phase = "mp4";
1051
 
1052
- NS_Misc.console.log({
1053
- ENV: "all",
1054
- _browser: false,
1055
- _log: [`🏁 PHRASE [${phase}]: INIT`, "\n"]
1056
- });
1057
 
1058
- fetch(_);
1059
- }
1060
- }
1061
 
1062
- return R_;
 
 
 
 
 
 
 
 
 
 
1063
  }
1064
- },
1065
- {
1066
- params: t.Object({
1067
- data_key: t.String({
1068
- minLength: 1,
1069
- error() {
1070
- return "Invalid process data, Expected string";
1071
- }
1072
- })
1073
- }),
1074
- query: t.Object({
1075
- raw: t.Optional(
1076
- t.Number({
1077
- minimum: 0,
1078
- maximum: 1,
1079
- error: () => "Invalid raw, Expected binary boolean"
1080
- })
1081
- ),
1082
- ops: t.Optional(
1083
- t.Union(
1084
- [
1085
- t.Literal("cmd"),
1086
- t.Literal("raw"),
1087
- t.Literal("decode"),
1088
- t.Literal("just_check")
1089
- ],
1090
- {
1091
- error() {
1092
- return "Invalid operation, must be one of: cmd, raw, decode";
1093
- }
1094
- }
1095
- )
1096
- )
1097
- })
1098
- }
1099
- );
1100
 
1101
- export const StreamRouter = sub_router;
 
 
1102
 
1103
  async function fetchText(arg0: { url: string; headers: Record<string, any> }) {
1104
  const { url, headers } = arg0;
 
25
  import { once } from "node:events";
26
  import { NS_Misc } from "@x/core/client/misc.ts";
27
  import { NS_bucket } from "../lib/bucket";
28
+ import { ShowtimeV2RouterHandlePayload } from "./showtime";
29
 
30
  type Range = { offset: number; length: number };
31
  type Init = { url: string; key: Key; range?: Range };
 
94
  return metadata;
95
  }
96
 
97
+ const [E_, R_] = await NS_promise.F_catch_promise({
98
+ promise: handle_data({
99
+ data_key,
100
+ metadata,
101
+ REQ0,
102
+ REQ_ORIGIN,
103
+ ctx: ctx as any,
104
+ ops
105
+ })
106
+ });
107
 
108
+ if (E_) {
109
+ const { message, status } = NS_error.F_collect_error_status_n_message({
110
+ ERR: E_
111
+ });
112
 
113
+ return ctx.status(status, message);
114
+ }
115
 
116
+ return R_;
117
+ },
118
+ {
119
+ params: t.Object({
120
+ data_key: t.String({
121
+ minLength: 1,
122
+ error() {
123
+ return "Invalid process data, Expected string";
124
+ }
125
+ })
126
+ }),
127
+ query: t.Object({
128
+ raw: t.Optional(
129
+ t.Number({
130
+ minimum: 0,
131
+ maximum: 1,
132
+ error: () => "Invalid raw, Expected binary boolean"
133
+ })
134
+ ),
135
+ ops: t.Optional(
136
+ t.Union(
137
+ [
138
+ t.Literal("cmd"),
139
+ t.Literal("raw"),
140
+ t.Literal("decode"),
141
+ t.Literal("just_check")
142
+ ],
143
+ {
144
+ error() {
145
+ return "Invalid operation, must be one of: cmd, raw, decode";
146
+ }
147
+ }
148
+ )
149
+ )
150
+ })
151
+ }
152
+ );
153
 
154
+ export const StreamRouter = sub_router;
155
+ export const StreamRouterHandlePayload = handle_data;
156
+
157
+ async function handle_data(arg0: {
158
+ data_key: string;
159
+ metadata: T_delivery_payload;
160
+ REQ0: URL;
161
+ REQ_ORIGIN: string;
162
+ ctx: Context;
163
+ ops?: string;
164
+ throw_error?: boolean;
165
+ }) {
166
+ const { data_key, metadata, REQ0, REQ_ORIGIN, ctx, ops, throw_error } = arg0;
167
 
168
+ const { playback, probe_metadata, sbe_id, progress_update_endpoint } =
169
+ metadata;
 
170
 
171
+ let { output_destination, delivery_mode, delivery_format, out_mime } =
172
+ metadata;
173
 
174
+ const x_i = REQ0.searchParams.get("x_i");
175
+ const xc = REQ0.searchParams.has("xc");
176
 
177
+ const two_step = x_i && x_i.length ? true : false;
 
 
178
 
179
+ const picture_quality = (() => {
180
+ const _ = playback.picture_quality;
181
 
182
+ if (typeof _ !== "string") {
183
+ return "";
184
+ }
 
 
 
 
185
 
186
+ return _.trim().split(" ").join("");
187
+ })();
 
188
 
189
+ const sbe_s_e = (() => {
190
+ const _ = metadata.sbe_s_e;
191
 
192
+ if (typeof _ !== "string" || _.toLowerCase().includes("nan")) {
193
+ return;
194
+ }
195
 
196
+ return _.trim().split(" ").join("");
197
+ })();
198
 
199
+ const qLevel = (() => {
200
+ if (
201
+ typeof metadata.qLevel === "string" &&
202
+ metadata.qLevel.trim().length > 0
203
+ ) {
204
+ return metadata.qLevel.trim();
205
+ }
206
 
207
+ const _ = NS_ffmpeg_v2.F_get_qLevel({
208
+ metadata: probe_metadata
209
+ });
 
210
 
211
+ return _;
212
+ })();
213
+
214
+ const ws = (() => {
215
+ const is_https = new URL(REQ_ORIGIN).protocol === "https:";
216
 
217
+ const ws_protocol = is_https ? "wss" : "ws";
218
 
219
+ const _ = `${ws_protocol}://${REQ0.host}/ws`;
 
 
220
 
221
+ const data = {
222
+ connect: _,
223
+ channel: data_key
224
+ };
 
225
 
226
+ return data;
227
+ })();
228
 
229
+ let segments: Segment[] | undefined = undefined;
230
 
231
+ let active_playback = playback;
232
+ let proxied = false;
233
+ let init: Init | undefined = undefined;
234
 
235
+ const PHASES = {
236
+ single: { from: 0, to: 99, label: "Processing" },
237
+ ts: { from: 0, to: 60, label: "Step 1/2: fetching video" },
238
+ mp4: { from: 60, to: 99, label: "Step 2/2: converting to MP4" }
239
+ } as const;
240
 
241
+ type Phase = keyof typeof PHASES;
 
 
 
 
 
242
 
243
+ let phase: Phase = "single";
 
244
 
245
+ out_mime = "video/mp2t";
246
+ delivery_format = "ts";
247
 
248
+ try {
249
+ const seg_error = await get_segments({ _playback: playback });
250
 
251
+ if (seg_error) {
252
+ if (throw_error) throw Error(JSON.stringify(seg_error));
253
+
254
+ return ctx.status(422, seg_error);
255
  }
256
+ } catch (error) {
257
+ throw error;
258
+ }
259
 
260
+ await (async () => {
261
+ if (!two_step) return;
 
 
 
262
 
263
+ delivery_mode = "s3";
 
264
 
265
+ phase = "ts";
266
+ })();
267
 
268
+ if (delivery_mode !== "s3") {
269
+ const message = "Unsupported delivery mode or format";
270
 
271
+ if (throw_error) throw Error(JSON.stringify(message));
272
+ return ctx.status(400, message);
273
+ }
274
 
275
+ const ext_key = (() => {
276
+ const _ = NS_files.F_get_file_extension({
277
+ filename: output_destination,
278
+ return_new_filename: true
279
+ });
280
 
281
+ return `${_.new_filename}.${delivery_format}`;
282
+ })();
283
 
284
+ output_destination = ext_key;
285
 
286
+ const output_destination_clean = NS_misc.cleanNameV2(output_destination);
 
287
 
288
+ const clean_file_id = (() => {
289
+ return `${picture_quality}-${output_destination_clean}`.toLowerCase();
290
+ })();
291
 
292
+ const job_id = x_i || `${sbe_id}-${clean_file_id}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
+ const file_id = `${Date.now()}-${clean_file_id}`;
 
295
 
296
+ const cache_folder = two_step ? `cacheStream/${sbe_id}` : `cache/${sbe_id}`;
 
 
 
 
297
 
298
+ const bucket_file_path = (() => {
299
+ const _ = `${cache_folder}/${file_id}`;
300
 
301
+ return _;
302
+ })();
303
 
304
+ const MAX_RETRIES = 3;
305
+
306
+ const err_message = (err: unknown) =>
307
+ NS_error.F_collect_error_message({ ERR: err }).message;
308
+
309
+ // -------------------------------
310
+ // Shared job state
311
+ // -------------------------------
312
+ // one object that the segment and S3 callbacks write to, so neither one wipes out the other's numbers
313
+ const live = {
314
+ percent: 0,
315
+ size: "",
316
+ failed: false,
317
+ done: false,
318
+ launched: false,
319
+ last_push: 0 // throttle for push_progress
320
+ };
321
 
322
+ // bumped on every (re)start so callbacks from an older attempt can be ignored
323
+ let attempt = 0;
324
 
325
+ // -------------------------------
326
+ // Setup abort handling
327
+ // -------------------------------
328
+ // The request signal only matters until the job has launched. After we answered the client the job runs in the background, and a client disconnect must not kill it.
329
+ let controller = new AbortController();
330
 
331
+ const abort_signal = ctx?.request.signal;
332
 
333
+ const on_request_abort = () => kill_controller({ cleanup_files: true });
 
334
 
335
+ const detach_request_abort = () =>
336
+ abort_signal?.removeEventListener("abort", on_request_abort);
337
 
338
+ if (abort_signal?.aborted) {
339
+ kill_controller({ cleanup_files: true });
 
340
 
341
+ return;
342
+ }
 
 
 
343
 
344
+ abort_signal?.addEventListener("abort", on_request_abort, { once: true });
 
 
345
 
346
+ let error_429 = false;
347
+ let error_403 = false;
348
 
349
+ // true once THIS request recorded the "running" claim for job_id
350
+ let claimed = false;
351
 
352
+ // phase-local percent (0-100) -> overall percent, never above 99
353
+ function to_overall(phase_pct: number) {
354
+ const p = PHASES[phase];
 
 
 
355
 
356
+ return Math.min(
357
+ 99,
358
+ Math.max(p.from, p.from + (phase_pct / 100) * (p.to - p.from))
359
+ );
360
+ }
361
 
362
+ try {
363
+ const current_job = (() => {
364
+ const _ = transcode_jobs.get(job_id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
+ if (!_ || xc) return;
 
367
 
368
+ return _;
369
+ })();
370
 
371
+ if (current_job?.status === "running") {
372
+ const percent = current_job?.percent;
373
+ const percentage = typeof percent === "number" ? `${percent} %` : "";
374
+ const transcoded_size = current_job?.transcoded_size;
375
+ const progress_endpoint =
376
+ current_job?.progress_endpoint || progress_update_endpoint;
 
 
 
 
377
 
378
+ const message = percentage
379
+ ? `ENGINE PROGRESS: ${percentage}`
380
+ : "ENGINE PROCESSING ...";
381
 
382
+ return {
383
+ processing: true,
384
+ message,
385
+ percent,
386
+ transcoded_size,
387
+ // TWO STEP: lets a re-polling client show which step is running
388
+ phase: current_job?.phase,
389
+ stage: current_job?.stage,
390
+ track_process: progress_endpoint,
391
+ file: output_destination,
392
+ job_id,
393
+ output_destination,
394
+ qLevel,
395
+ picture_quality,
396
+ ws,
397
+ flags: current_job.flags,
398
+ logs: current_job.logs,
399
+ ...(ops === "just_check" ? { passed: true } : {})
400
+ };
401
+ }
402
 
403
+ if (current_job?.status === "failed") {
404
+ const logs = current_job?.logs;
405
 
406
+ if (logs) id_log_errors({ logs });
 
 
 
407
 
408
+ if (ops === "just_check") {
409
+ return {
410
+ passed: false,
411
+ delivery: output_destination,
412
+ track_process: progress_update_endpoint,
413
+ ws,
414
+ flags: current_job.flags,
415
+ logs: current_job?.logs
416
+ };
417
+ }
418
 
419
+ transcode_jobs.delete(job_id);
420
 
421
+ const probe_playback = await new_playback();
 
 
422
 
423
+ const _ = await ping_probe({ playback: probe_playback });
 
 
 
 
424
 
425
+ if (!_.success) {
426
+ throw new Error("Something went wrong: with probe");
427
+ }
428
+ }
429
 
430
+ const seed_file = await (async () => {
431
+ if (ops === "cmd") {
432
+ return;
433
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
 
435
+ const _ = current_job?.db_metadata?.stream_link;
 
436
 
437
+ if (_) {
438
+ return current_job.db_metadata;
439
+ }
440
 
441
+ const __ = await wide_search_runner({
442
+ document: true,
443
+ i_x: two_step ? true : false,
444
+ skip: { local: true }
445
+ });
446
 
447
+ return __;
448
+ })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
 
450
+ // wide search runner >> seed
451
+ const _ = await (async () => {
452
+ if (!seed_file) return;
453
 
454
+ const file_size_bytes = parseFloat(`${seed_file?.size}`);
 
455
 
456
+ const percent = 100;
457
+ const transcoded_size = file_size_bytes
458
+ ? NS_ffmpeg_v2.F_format_size(file_size_bytes)
459
+ : current_job?.transcoded_size;
460
+ const progress_endpoint =
461
+ current_job?.progress_endpoint || progress_update_endpoint;
462
 
463
+ if (!current_job?.db_metadata?.stream_link) {
464
+ record_task_info({
465
+ job_id,
466
+ status: "done",
467
+ info: {
468
+ title: output_destination,
469
  percent,
470
  transcoded_size,
471
+ ...("stream_link" in seed_file && seed_file.stream_link
472
+ ? { db_metadata: seed_file }
473
+ : {})
474
+ }
475
+ });
476
+ }
 
477
 
478
+ const _ =
479
+ seed_file && "stream_link" in seed_file && seed_file.stream_link;
480
 
481
+ if (two_step && _) {
482
+ return {};
483
  }
484
 
485
+ const new_stream_link = await (async () => {
486
+ if (!_) {
487
+ return;
488
+ }
489
 
490
+ const stream_link = new URL(seed_file.stream_link);
491
+ stream_link.searchParams.set("download", "true");
492
 
493
+ const [E_, R_] = await NS_promise.F_catch_promise({
494
+ promise: (async () => {
495
+ const cloudflare =
496
+ await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
497
 
498
+ if (!cloudflare.available) return;
 
 
 
 
 
499
 
500
+ const _ = await NS_pack_proxima.F_handle_pack_encode({
501
+ base: new URL(cloudflare.available),
502
+ payload: {
503
+ endpoint: stream_link.toString(),
504
+ commands: {
505
+ response_headers: {
506
+ set: {
507
+ "content-disposition": `attachment; filename="${output_destination}"`
508
+ }
509
+ },
510
+ use_h_version: true
511
+ }
512
+ }
513
+ });
514
+
515
+ return { proxy_url: _ };
516
+ })()
517
+ });
518
+
519
+ const new_stream_link =
520
+ (R_ as any)?.proxy_url.toString() || stream_link.toString();
521
+
522
+ return new_stream_link;
523
+ })();
524
 
525
  if (ops === "just_check") {
526
  return {
527
  passed: true,
528
+ ready: true,
529
+ percent,
530
+ transcoded_size,
531
+ output_destination,
532
+ flags: current_job?.flags,
533
+ logs: current_job?.logs,
534
+ ...(new_stream_link ? { stream_link: new_stream_link } : {}),
535
+ track_process: progress_endpoint
536
  };
537
  }
538
 
539
+ if (new_stream_link) return ctx.redirect(new_stream_link, 302);
540
+ })();
541
 
542
+ if (_) {
543
+ return _;
544
+ }
545
 
546
+ let _playback = await new_playback();
547
 
548
+ claimed = true;
549
 
550
+ record_task_info({
551
+ job_id,
552
+ status: "running",
553
+
554
+ info: {
555
  title: output_destination,
556
  ws,
557
+ progress_endpoint: progress_update_endpoint
558
+ }
559
+ });
560
+
561
+ // the client gave up while we were resolving the playlist
562
+ if (controller.signal.aborted) {
563
+ return;
564
+ }
565
+
566
+ if (ops === "just_check") {
567
+ return {
568
+ passed: true,
569
+ segments,
570
+ playback: _playback
571
  };
572
+ }
573
 
574
+ spawn({ retry: 0 });
 
 
 
 
575
 
576
+ const message = `COMPILER ENGINE started, this may take a while. ${progress_update_endpoint ? `check process -> ${progress_update_endpoint}` : "check back later"}`;
577
 
578
+ live.launched = true;
 
 
 
 
579
 
580
+ detach_request_abort();
581
 
582
+ return {
583
+ message,
584
+ title: output_destination,
585
+ ws,
586
+ track_process: progress_update_endpoint
587
+ };
588
 
589
+ /*
590
+ Starts (or restarts) the download -> upload pipeline.
591
+ */
592
+ async function spawn(arg0: { retry: number }) {
593
+ const { retry } = arg0;
594
 
595
+ if (retry >= MAX_RETRIES) return;
 
 
 
596
 
597
+ try {
598
+ if (retry === 0) {
599
+ if (controller.signal.aborted) return;
600
+ } else {
601
+ const next_playback = await new_playback();
602
 
603
+ if (next_playback === _playback) return;
 
604
 
605
+ _playback = next_playback;
 
 
606
 
607
+ controller.abort();
608
+ controller = new AbortController();
609
 
610
+ live.done = false;
611
+ live.failed = false;
612
+ live.percent = 0;
613
+ live.size = "";
614
+
615
+ const res = await get_segments({ _playback });
616
+
617
+ if (res?.error) {
618
+ fail([res.error]);
619
 
620
  return;
621
  }
622
+ }
623
 
624
+ const current_segments = segments;
 
625
 
626
+ if (!current_segments?.length) {
627
+ fail(["No segments to upload"]);
 
 
 
 
 
 
628
 
629
+ return;
630
+ }
631
 
632
+ const my_attempt = ++attempt;
633
+ const stale = () => my_attempt !== attempt;
 
 
634
 
635
+ const bytes = segmentStream({
636
+ segments: current_segments,
637
+ init,
638
+ headers: _playback.headers || {},
639
+ concurrency: IS_HF() ? 12 : 6,
640
+ ac: controller,
641
+ onError: (arg0) => {
642
+ if (is_dev) console.error("💠", JSON.stringify(arg0), "\n");
643
 
644
+ if (stale()) return;
645
 
646
+ fail([err_message(arg0.err)]);
647
+ },
648
+ onSegment: (arg0) => {
649
+ if (is_dev) console.log("💠", JSON.stringify(arg0), "\n");
650
 
651
+ if (stale()) return;
 
 
 
 
 
 
 
652
 
653
+ live.percent = arg0.percent;
654
 
655
+ push_progress({ size: live.size, percent: live.percent });
656
+ }
657
+ });
 
658
 
659
+ putStreamWriter({
660
+ stream: bytes,
661
+ ContentType: out_mime,
662
+ key: bucket_file_path,
663
+ segmentsTotal: current_segments.length,
664
+ ac: controller,
665
+ onError: (arg0) => {
666
+ if (is_dev) console.error("💠", JSON.stringify(arg0), "\n");
667
 
668
+ if (stale()) return;
669
 
670
+ fail([err_message(arg0.err)]);
671
+ },
672
+ onProgress: (s) => {
673
+ if (is_dev) console.log("💠", JSON.stringify(arg0), "\n");
674
+
675
+ if (stale()) return;
676
+
677
+ live.size = s.size || live.size;
678
+
679
+ if (s.state === "running") {
680
+ push_progress({ size: live.size, percent: live.percent });
681
  }
682
+ }
683
+ }).then(
684
+ (r) => {
685
+ if (stale()) return;
686
+
687
+ live.done = true;
688
+ live.size = NS_files.F_format_size(r.bytes) || live.size;
689
+ live.percent = 100;
690
+
691
+ push_done({ size: live.size });
692
+
693
+ try {
694
+ // document file
695
+ (async () => {
696
+ const _ = await wide_search_runner({
697
+ document: two_step ? false : true,
698
+ i_x: two_step ? true : false,
699
+ skip: {
700
+ local: true,
701
+ db: true
702
+ }
 
 
 
 
 
 
 
 
 
703
  });
704
+ })();
705
+
706
+ NS_Misc.console.log({
707
+ ENV: "all",
708
+ _browser: false,
709
+ _log: [
710
+ `🏁 SEG STREAM finished [${phase}]: (${live.percent} %) -> ${live.size} -> ${output_destination}`,
711
+ "\n"
712
+ ]
713
+ });
714
+ } catch (e) {
715
+ console.warn("post-done step failed:", e);
716
+ }
717
+ },
718
+ (err) => {
719
+ if (stale()) return;
720
 
721
+ console.warn("RUN ERROR:", err);
722
 
723
+ fail([err_message(err)]);
724
 
725
+ spawn({ retry: retry + 1 });
726
+ }
727
+ );
728
+ } catch (err) {
729
+ console.warn("spawn error:", err);
730
 
731
+ fail([err_message(err)]);
 
732
  }
733
+ }
734
+ } finally {
735
+ if (!live.launched) {
736
+ detach_request_abort();
737
 
738
+ if (claimed && transcode_jobs.get(job_id)?.status === "running") {
739
+ transcode_jobs.delete(job_id);
 
740
  }
741
  }
742
+ }
743
 
744
+ /*
745
+ Returns the playback to use. Switches to the proxy only after a
746
+ 403/429, only once, and returns the SAME object when there is nothing new to try (callers compare by reference).
747
+ */
748
+ async function new_playback() {
749
+ if (
750
+ (!error_429 && !error_403) ||
751
+ proxied ||
752
+ playback.url.toString().includes("/proxima")
753
+ ) {
754
+ return active_playback;
755
+ }
756
 
757
+ const R_ = await NS_scrape_post_processing_helper.get_cloudflare_proxy({});
 
 
758
 
759
+ if (!R_) return playback;
760
 
761
+ const proxy_base = R_.available || R_.fallback;
762
 
763
+ if (NS_routes_c.F_is_url(proxy_base)) {
764
+ const _ = await NS_pack_proxima.F_handle_pack_encode({
765
+ base: proxy_base,
766
+ payload: {
767
+ endpoint: playback.url.toString(),
768
+ commands: {
769
+ use_h_version: true,
770
+ redirect_origin: true,
771
+ headers: playback.headers
 
772
  }
773
+ }
774
+ });
775
 
776
+ if (_) {
777
+ const __ = { ...playback };
778
 
779
+ __.url = _.toString();
780
 
781
+ delete __.headers;
782
 
783
+ proxied = true;
784
+ active_playback = __;
785
 
786
+ return __;
 
787
  }
 
 
788
  }
789
 
790
+ return active_playback;
791
+ }
 
 
792
 
793
+ async function get_segments(arg0: {
794
+ _playback: T_delivery_payload["playback"];
795
+ }): Promise<{ error: string } | undefined> {
796
+ let { _playback } = arg0;
 
 
 
797
 
798
+ const resolve = () =>
799
+ NS_promise.F_catch_promise({
800
+ promise: resolveMediaPlaylist({
801
+ url: _playback.url,
802
+ headers: _playback.headers || {}
803
+ })
804
+ });
805
 
806
+ let [E_, R_] = await resolve();
 
 
 
807
 
808
+ if (E_ || !R_) {
809
+ const { message, status } = NS_error.F_collect_error_status_n_message({
810
+ ERR: E_
811
+ });
812
 
813
+ id_log_errors({ logs: [message] });
 
814
 
815
+ if (error_403 || error_429) {
816
+ _playback = await new_playback();
817
 
818
+ [E_, R_] = await resolve();
 
 
 
 
819
 
820
+ if (E_ || !R_) {
821
+ const { message, status } = NS_error.F_collect_error_status_n_message(
822
+ {
823
+ ERR: E_
824
+ }
825
+ );
826
 
 
 
 
827
  fail([message]);
828
 
829
  throw NS_error.F_create_error({ status, message });
830
  }
831
+ } else {
832
+ fail([message]);
833
+
834
+ throw NS_error.F_create_error({ status, message });
835
  }
836
+ }
837
 
838
+ const { url, text, separate_audio } = R_!;
839
 
840
+ const media = parseMedia({ text, base: url });
841
 
842
+ if (!media.segments.length) {
843
+ return { error: "No segments found" };
844
+ }
 
 
 
 
845
 
846
+ if (!media.ended) {
847
+ return { error: "Live playlists are not supported" };
848
+ }
 
849
 
850
+ if (media.initUrl || media?.init?.url) {
851
+ if (!two_step) {
852
+ // return { error: "fMP4/CMAF needs remuxing, not supported" };
853
  }
854
 
855
+ out_mime = "video/mp4";
856
+ delivery_format = "mp4";
857
+ }
 
 
 
 
 
 
858
 
859
+ if (media.init) {
860
+ // what is still NOT possible without ffmpeg
861
+ if (media.init_changes) {
862
+ return {
863
+ error:
864
+ "Playlist switches init segments (discontinuity), needs remuxing"
865
+ };
866
  }
867
+ }
868
 
869
+ if (separate_audio) {
870
+ return { error: "Audio is a separate track, needs muxing" };
871
  }
872
 
873
+ init = media.init;
874
+ segments = media.segments;
875
+ }
876
 
877
+ // ---------- helpers (hoisted) ----------
 
878
 
879
+ function id_log_errors(arg0: { logs: string[] }) {
880
+ const { logs } = arg0;
 
881
 
882
+ if (!Array.isArray(logs)) {
883
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
  }
885
 
886
+ logs.forEach((xx) => {
887
+ const _ = xx.toString().toLowerCase();
888
 
889
+ if (
890
+ _.includes(
891
+ "Error opening input files: Server returned 403 Forbidden".toLowerCase()
892
+ ) ||
893
+ _.includes("Fetch failed: status: 403".toLowerCase()) ||
894
+ _.includes("Segment failed (403)".toLowerCase()) ||
895
+ _.includes("Segment failed (404)".toLowerCase()) ||
896
+ _.includes("http error 404 not found".toLowerCase())
897
+ ) {
898
+ error_403 = true;
899
+ }
900
 
901
+ if (
902
+ _.includes("HTTP error 429 Too Many Requests".toLowerCase()) ||
903
+ _.includes("Fetch failed: status: 429".toLowerCase()) ||
904
+ _.includes("Segment failed (429)".toLowerCase()) ||
905
+ _.includes("failed too many times, skipping".toLowerCase())
906
+ ) {
907
+ error_429 = true;
908
+ }
909
+ });
910
+ }
911
 
912
+ function fail(logs: string[]) {
913
+ if (live.failed || live.done) return;
914
 
915
+ live.failed = true;
 
 
 
 
 
916
 
917
+ id_log_errors({ logs: logs });
 
 
 
 
 
 
 
 
 
 
 
918
 
919
+ push_failed({ logs, size: live.size });
920
+ }
 
 
 
 
 
 
 
 
 
921
 
922
+ function push_failed(arg0: { size?: string; logs: string[] }) {
923
+ const { size, logs } = arg0,
924
+ percent = live.percent,
925
+ title = output_destination,
926
+ transcoded_size = size || live.size || "",
927
+ status = "failed";
928
+
929
+ record_task_info({
930
+ job_id,
931
+ status,
932
+ info: {
933
+ title,
934
+ percent: percent && two_step ? to_overall(percent) : percent,
935
+ transcoded_size,
936
+ ws,
937
+ ...(two_step ? { phase, stage: PHASES[phase].label } : {}),
938
+ logs
939
+ }
940
+ });
941
 
942
+ relay_progress_info_ws({
943
+ channel: data_key,
944
+ payload: {
945
+ channel: data_key,
946
+ status,
947
+ title,
948
+ percent: percent && two_step ? to_overall(percent) : percent,
949
+ transcoded_size,
950
+ logs
951
+ }
952
+ });
953
 
954
+ relay_progress_info({
955
+ endpoint: progress_update_endpoint
956
+ ? new URL(progress_update_endpoint)
957
+ : undefined,
958
+ progress_data: {
959
  status,
960
+ title,
961
+ percent: percent && two_step ? to_overall(percent) : percent,
962
+ ...(two_step ? { phase } : {}),
963
+ location: `${REQ0.origin}/${REQ0.pathname}`,
964
+ logs
965
+ }
966
+ });
967
+ }
968
 
969
+ function push_done(arg0: { size?: string }) {
970
+ const { size } = arg0,
971
+ percent = 100,
972
+ title = output_destination,
973
+ transcoded_size = size || live.size || "",
974
+ status = "done";
975
+
976
+ record_task_info({
977
+ job_id,
978
+ status,
979
+ info: {
980
+ title,
981
+ percent: percent && two_step ? to_overall(percent) : percent,
982
+ transcoded_size,
983
+ progress_endpoint: progress_update_endpoint
984
+ }
985
+ });
986
 
987
+ relay_progress_info_ws({
988
+ channel: data_key,
989
+ payload: {
990
+ channel: data_key,
991
+ status,
992
+ data: {
993
  status,
 
994
  title,
995
+ percent: percent && two_step ? to_overall(percent) : percent,
996
+ transcoded_size
997
  }
998
+ }
999
+ });
1000
 
1001
+ relay_progress_info({
1002
+ endpoint: progress_update_endpoint
1003
+ ? new URL(progress_update_endpoint)
1004
+ : undefined,
1005
+ progress_data: {
1006
+ status,
1007
+ percent: percent && two_step ? to_overall(percent) : percent,
1008
+ title,
1009
+ location: `${REQ0.origin}/${REQ0.pathname}`
1010
+ }
1011
+ });
1012
+ }
1013
 
1014
+ function push_progress(arg0: { size: string; percent?: number }) {
1015
+ if (live.done || live.failed) return;
1016
 
1017
+ const now = Date.now();
1018
 
1019
+ if (now - live.last_push < 1500) return;
1020
 
1021
+ live.last_push = now;
 
 
1022
 
1023
+ const { size, percent } = arg0,
1024
+ transcoded_size = size || live.size || "",
1025
+ status = "running";
1026
 
1027
+ const step_info = two_step ? { phase, stage: PHASES[phase].label } : {};
 
 
 
 
 
 
 
 
 
 
 
1028
 
1029
+ record_task_info({
1030
+ job_id,
1031
+ status,
1032
+ info: {
1033
  title: output_destination,
1034
  percent: percent && two_step ? to_overall(percent) : percent,
1035
  transcoded_size,
1036
  ...step_info,
1037
+ progress_endpoint: progress_update_endpoint,
1038
+ ws
1039
+ }
1040
+ });
1041
 
1042
+ const data = {
1043
+ status,
1044
+ title: output_destination,
1045
+ percent: percent && two_step ? to_overall(percent) : percent,
1046
+ transcoded_size,
1047
+ ...step_info,
1048
+ location: `${REQ0.origin}/${REQ0.pathname}`
1049
+ };
1050
 
1051
+ relay_progress_info_ws({
1052
+ channel: data_key,
1053
+ payload: {
1054
+ channel: data_key,
1055
+ status,
1056
+ data
1057
+ }
1058
+ });
1059
 
1060
+ relay_progress_info({
1061
+ endpoint: progress_update_endpoint
1062
+ ? new URL(progress_update_endpoint)
1063
+ : undefined,
1064
+ progress_data: data
1065
+ });
1066
+ }
1067
 
1068
+ function kill_controller(arg0: { cleanup_files?: boolean }) {
1069
+ const { cleanup_files } = arg0;
1070
 
1071
+ controller.abort();
 
 
 
 
 
1072
 
1073
+ // user aborted before the job was launched: mark it failed. Any
1074
+ // multipart upload is cleaned up by putStreamWriter when the abort
1075
+ // reaches it
1076
+ if (cleanup_files && !live.done) {
1077
+ fail(["Request aborted"]);
1078
  }
1079
 
1080
+ console.log("[CONTROLLER] Request aborted");
1081
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1082
 
1083
+ async function wide_search_runner(arg0: {
1084
+ skip?: {
1085
+ local?: boolean;
1086
+ db?: boolean;
1087
+ bucket?: boolean;
1088
+ };
1089
+ document: boolean;
1090
+ i_x?: boolean;
1091
+ }) {
1092
+ const { skip, document, i_x } = arg0;
1093
+
1094
+ const [E_, R_] = await NS_promise.F_catch_promise({
1095
+ promise: NS_showtime_v2_helper.find_seed_file({
1096
+ sbe_id,
1097
+ output_destination: {
1098
+ og: output_destination,
1099
+ clean: output_destination_clean
1100
+ },
1101
+ picture_quality,
1102
+ qLevel,
1103
+ sbe_s_e,
1104
+ cache_folder,
1105
+ file_id: clean_file_id,
1106
+ job_id,
1107
+ cleanup_corruption: true,
1108
+ document,
1109
+ ideal_runtime: playback.ideal_runtime,
1110
+ skip,
1111
+ has_subs: false
1112
+ })
1113
+ });
1114
+
1115
+ if (R_ && "stream_link" in R_ && R_.stream_link) {
1116
+ if (!NS_misc.isValidUrlFormat(R_.stream_link)) {
1117
+ return;
1118
+ }
1119
 
1120
+ if (i_x) {
1121
+ const _ = new URL(`/showtimeV2/delivery/${data_key}`, REQ_ORIGIN);
1122
 
1123
+ _.searchParams.set("i_x", R_.stream_link);
1124
+ _.searchParams.set("xc", "");
1125
 
1126
+ phase = "mp4";
1127
 
1128
+ NS_Misc.console.log({
1129
+ ENV: "all",
1130
+ _browser: false,
1131
+ _log: [`🏁 PHRASE [${phase}]: INIT`, "\n"]
1132
+ });
1133
 
1134
+ // fetch(_);
 
 
1135
 
1136
+ NS_promise.F_catch_promise({
1137
+ promise: ShowtimeV2RouterHandlePayload({
1138
+ data_key,
1139
+ metadata,
1140
+ REQ0: _,
1141
+ REQ_ORIGIN,
1142
+ ctx: ctx as any,
1143
+ throw_error: true
1144
+ })
1145
+ });
1146
+ }
1147
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1148
 
1149
+ return R_;
1150
+ }
1151
+ }
1152
 
1153
  async function fetchText(arg0: { url: string; headers: Record<string, any> }) {
1154
  const { url, headers } = arg0;