import { Context, Elysia, t } from "elysia"; import { NS_misc } from "../../lib/misc"; import createError from "http-errors"; import { NS_promise } from "../../lib/promise"; import { NS_ffmpeg_v2 } from "../../lib/ffmpeg-v2"; import { NS_crypto_client } from "../../../../../../packages/lib/helpers/client/crypto"; import { redis } from "../../lib/upstash"; import { offline_store } from "../../lib/offline-store"; import { NS_validator_v2, T_live, T_playback } from "./validator-v2"; import UserAgent from "user-agents"; const APP_ENV = process.env.APP_ENV; const APP_ACCOUNT_ABC = process.env.APP_ACCOUNT_ABC; export type T_delivery_payload = { playback: T_playback; out_mime: "video/mp4" | "video/x-matroska" | "video/mp2t"; subtitles?: T_live["subtitles"]; output_destination: NonNullable; delivery_mode: T_live["delivery_mode"]; delivery_format: T_live["delivery_format"]; progress_update_endpoint?: T_live["progress_update_endpoint"]; sbe_id: NonNullable; probe_metadata: Record; }; const sub_router = new Elysia({ prefix: "/ffmpeg/v2" }).post( "/probe", async (ctx) => { const { query, request, status, body } = ctx; const REQ0 = new URL(request.url); const REQ_ORIGIN = (ctx as any).publicOrigin || REQ0.origin; const REQ_headers = request.headers; const req0_headers_object = Object.fromEntries(REQ_headers.entries()); const req_signal = request.signal; const { playback, live } = body; if (!NS_misc.isValidUrlFormat(playback.url)) { if (playback.protocol !== "local") { throw createError(400, "invalid playback URL"); } } if ( live?.progress_update_endpoint && !NS_misc.isValidUrlFormat(live.progress_update_endpoint) ) { throw createError(400, "invalid progress update URL"); } // HANDLE HEADERS (() => { const playback_headers = playback?.headers; if ( !playback_headers || !( typeof playback_headers === "object" && !Array.isArray(playback_headers) ) ) { playback.headers = {}; } const ua = NS_misc.F_get_obj_kv({ name: "User-Agent", obj: playback_headers }); if (ua) return; try { const userAgent = UserAgent.random({ deviceCategory: "desktop" }) || new UserAgent(); if (userAgent?.toString()) { (playback.headers as any)["User-Agent"] = userAgent.toString(); } } catch {} })(); // handle picture quality (() => { if (!playback.picture_quality) { return; } const picture_quality = playback.picture_quality; if ( typeof picture_quality !== "string" || !picture_quality.trim() || picture_quality.trim().toLowerCase() === "n/a" ) { playback.picture_quality = undefined; return; } playback.picture_quality = picture_quality .trim() .split(" ") .filter((xx) => xx.length) .join(""); })(); // RUN RAW PROBE const [E_, R_] = await NS_promise.F_catch_promise({ promise: NS_ffmpeg_v2.F_probe_t1({ context: ctx, playback }) }); // console.error("\n", E_, "\n"); if (E_) { throw E_; } if (!live) { return R_; } const probe_metadata = R_ as any; const clean_metadata = NS_ffmpeg_v2.F_scan_raw_probe_metadata({ metadata: probe_metadata }); if (clean_metadata?.format) { const format_name = clean_metadata.format.name; if (typeof format_name === "string" && format_name.includes("hls")) { playback.type = "hls"; } else { playback.type = playback.type || "video"; } } const subtitles = (() => { if (!live.subtitles || !live.subtitles.length) { return; } if (clean_metadata?.subStreams && clean_metadata.subStreams.length) { return; } const original_subs = live.subtitles; const filtered = original_subs .filter((xx) => { return NS_misc.isValidUrlFormat(xx.url); }) .slice(0, 5); return filtered; })(); const out_mime: T_delivery_payload["out_mime"] = (() => { if (live.delivery_mode === "stream") { return "video/mp4"; } switch (live.delivery_format) { case "mp4": { return "video/mp4"; } break; case "mkv": { return "video/x-matroska"; } break; case "ts": { return "video/mp2t"; } break; default: { return "video/mp4"; } break; } })(); let output_destination = ( live.delivery_name || `${live.delivery_mode === "stream" ? "stream" : "video"}-${Date.now()}.${live.delivery_format}` ).trim(); if ( !output_destination .toLowerCase() .endsWith(`.${live.delivery_format}`.toLowerCase()) ) { output_destination = `${output_destination}.${live.delivery_format}`; } if (!live.sbe_id) { live.sbe_id = "000000"; } const demand_data = { playback, out_mime, subtitles, output_destination, delivery_mode: live.delivery_mode, delivery_format: live.delivery_format, progress_update_endpoint: live.progress_update_endpoint, sbe_id: live.sbe_id }; const delivery_payload: T_delivery_payload = { ...demand_data, probe_metadata }; // store the data in Redis const cache_key = await (async () => { const cache_id = NS_crypto_client.F_crypto_randomUUID({ length: 40 }); let key = `llsvkm:${cache_id}`; if (APP_ACCOUNT_ABC) key = `${key}:${APP_ACCOUNT_ABC}`; // 30 days = 30 * 24 * 60 * 60 = 2,592,000 seconds const ONE_MONTH_IN_SECONDS = 2592000; (delivery_payload as any).cache_id = key; if (APP_ENV === "dev") { offline_store.set(key, delivery_payload); } else { await redis.set(key, delivery_payload, { ex: ONE_MONTH_IN_SECONDS }); } return key; })(); const url_o = new URL(`/showtimeV2/delivery/${cache_key}`, REQ_ORIGIN); return { delivery_endpoint: url_o.toString(), metadata: delivery_payload }; }, { body: t.Object({ playback: NS_validator_v2.playback, live: t.Optional(NS_validator_v2.live) }) } ); export const FfmpegV2Router = sub_router;