| 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<T_live["delivery_name"]>; |
| delivery_mode: T_live["delivery_mode"]; |
| delivery_format: T_live["delivery_format"]; |
| progress_update_endpoint?: T_live["progress_update_endpoint"]; |
| sbe_id: NonNullable<T_live["sbe_id"]>; |
| probe_metadata: Record<string, any>; |
| }; |
|
|
| 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"); |
| } |
|
|
| |
| (() => { |
| 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 {} |
| })(); |
|
|
| |
| (() => { |
| 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(""); |
| })(); |
|
|
| |
| const [E_, R_] = await NS_promise.F_catch_promise({ |
| promise: NS_ffmpeg_v2.F_probe_t1({ |
| context: ctx, |
| playback |
| }) |
| }); |
|
|
| |
|
|
| 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 |
| }; |
|
|
| |
| 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}`; |
|
|
| |
| 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; |
|
|