GUI-STUDIO
Add HF FFmpeg proxy support and enhance quality detection
89c8aa2
Raw
History Blame Contribute Delete
6.64 kB
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");
}
// 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;