mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 14:53:24 +00:00
173 lines
3.5 KiB
TypeScript
173 lines
3.5 KiB
TypeScript
import type { Item } from "parse-hls";
|
|
import hls from "parse-hls";
|
|
import { default as toWebVTT } from "srt-webvtt";
|
|
|
|
import type {
|
|
EmbedOutput,
|
|
FileBasedStream,
|
|
Qualities,
|
|
RunnerOptions,
|
|
ScrapeMedia,
|
|
Stream,
|
|
} from "@movie-web/providers";
|
|
import {
|
|
makeProviders,
|
|
makeStandardFetcher,
|
|
targets,
|
|
} from "@movie-web/providers";
|
|
|
|
export interface InitEvent {
|
|
sourceIds: string[];
|
|
}
|
|
|
|
export interface UpdateEvent {
|
|
id: string;
|
|
percentage: number;
|
|
status: UpdateEventStatus;
|
|
error?: unknown;
|
|
reason?: string;
|
|
}
|
|
|
|
export type UpdateEventStatus = "success" | "failure" | "notfound" | "pending";
|
|
|
|
export interface DiscoverEmbedsEvent {
|
|
sourceId: string;
|
|
embeds: {
|
|
id: string;
|
|
embedScraperId: string;
|
|
}[];
|
|
}
|
|
|
|
export type RunnerEvent =
|
|
| string
|
|
| InitEvent
|
|
| UpdateEvent
|
|
| DiscoverEmbedsEvent;
|
|
|
|
export async function getVideoStream({
|
|
sourceId,
|
|
media,
|
|
forceVTT,
|
|
onEvent,
|
|
}: {
|
|
sourceId?: string;
|
|
media: ScrapeMedia;
|
|
forceVTT?: boolean;
|
|
onEvent?: (event: RunnerEvent) => void;
|
|
}): Promise<Stream | null> {
|
|
const providers = makeProviders({
|
|
fetcher: makeStandardFetcher(fetch),
|
|
target: targets.NATIVE,
|
|
consistentIpForRequests: true,
|
|
});
|
|
|
|
const options: RunnerOptions = {
|
|
media,
|
|
events: {
|
|
init: onEvent,
|
|
update: onEvent,
|
|
discoverEmbeds: onEvent,
|
|
start: onEvent,
|
|
},
|
|
};
|
|
|
|
let stream: Stream | null = null;
|
|
|
|
if (sourceId) {
|
|
let embedOutput: EmbedOutput | undefined;
|
|
|
|
const sourceResult = await providers
|
|
.runSourceScraper({
|
|
id: sourceId,
|
|
media,
|
|
})
|
|
.catch(() => undefined);
|
|
|
|
if (sourceResult) {
|
|
for (const embed of sourceResult.embeds) {
|
|
const embedResult = await providers
|
|
.runEmbedScraper({
|
|
id: embed.embedId,
|
|
url: embed.url,
|
|
})
|
|
.catch(() => undefined);
|
|
|
|
if (embedResult) {
|
|
embedOutput = embedResult;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (embedOutput) {
|
|
stream = embedOutput.stream[0] ?? null;
|
|
} else if (sourceResult) {
|
|
stream = sourceResult.stream?.[0] ?? null;
|
|
}
|
|
} else {
|
|
stream = await providers
|
|
.runAll(options)
|
|
.then((result) => result?.stream ?? null);
|
|
}
|
|
|
|
if (!stream) return null;
|
|
|
|
if (forceVTT) {
|
|
if (stream.captions && stream.captions.length > 0) {
|
|
for (const caption of stream.captions) {
|
|
if (caption.type === "srt") {
|
|
const response = await fetch(caption.url);
|
|
const srtSubtitle = await response.blob();
|
|
const vttSubtitleUrl = await toWebVTT(srtSubtitle);
|
|
caption.url = vttSubtitleUrl;
|
|
caption.type = "vtt";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return stream;
|
|
}
|
|
|
|
export function findHighestQuality(
|
|
stream: FileBasedStream,
|
|
): Qualities | undefined {
|
|
const qualityOrder: Qualities[] = [
|
|
"4k",
|
|
"1080",
|
|
"720",
|
|
"480",
|
|
"360",
|
|
"unknown",
|
|
];
|
|
for (const quality of qualityOrder) {
|
|
if (stream.qualities[quality]) {
|
|
return quality;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export interface HLSTracks {
|
|
video: Item[];
|
|
audio: Item[];
|
|
subtitles: Item[];
|
|
}
|
|
|
|
export async function extractTracksFromHLS(
|
|
playlistUrl: string,
|
|
headers: Record<string, string>,
|
|
): Promise<HLSTracks | null> {
|
|
try {
|
|
const response = await fetch(playlistUrl, { headers }).then((res) =>
|
|
res.text(),
|
|
);
|
|
const playlist = hls.parse(response);
|
|
return {
|
|
video: playlist.streamRenditions,
|
|
audio: playlist.audioRenditions,
|
|
subtitles: playlist.subtitlesRenditions,
|
|
};
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|