nexstream-api / backend /src /services /video /stream-resolver.ts
circleci
deploy 60e0f6e
a39cdc9
Raw
History Blame Contribute Delete
6.64 kB
import { Request } from 'express';
import { getCookieArgs } from '../../utils/api/controller.util.js';
import { getVideoInfo } from '../ytdlp.service.js';
import {
isAvc,
isDirect,
selectVideoFormat,
selectAudioFormat,
buildProxyUrl,
getOutputMetadata,
} from '../../utils/media/stream.util.js';
import { estimateFilesize } from '../../utils/media/format.util.js';
import { getSanitizedFilename } from '../../utils/media/video.util.js';
import { decodeUrlIfNeeded } from '../../utils/network/validation.util.js';
import { VideoInfo, Format } from '../../types/index.js';
export function resolveFormatDetails(
info: VideoInfo,
formatId: string,
isSpotify: boolean,
audioLang?: string
) {
const requestedFormat = info.formats.find(
(format: Format) => String(format.formatId) === String(formatId)
);
// missing format is not audio-only
const isAudioStream = (format: Format | undefined) =>
Boolean(format) && (!format?.vcodec || format.vcodec === 'none');
const isAudioOnly =
formatId === 'mp3' || isSpotify || isAudioStream(requestedFormat);
const finalVideoFormat = isAudioOnly
? null
: selectVideoFormat(info.formats, formatId);
const hasAudio = (format: Format | null) =>
Boolean(format?.acodec && format?.acodec !== 'none');
const needsWebm = Boolean(finalVideoFormat && !isAvc(finalVideoFormat));
// audioUrl means a separate audio stream
const pairedAudioUrl =
!isAudioOnly && typeof finalVideoFormat?.audioUrl === 'string'
? finalVideoFormat.audioUrl
: null;
let finalAudioFormat: Format | null;
if (pairedAudioUrl) {
finalAudioFormat = {
formatId: `${finalVideoFormat?.formatId ?? 'edge'}-audio`,
url: pairedAudioUrl,
extension: 'm4a',
acodec: 'aac',
vcodec: 'none',
isAudio: true,
isVideo: false,
isMuxed: false,
};
} else if (isAudioOnly || !hasAudio(finalVideoFormat)) {
// include audioFormats so dash picks audio-only
const audioPool = [...info.formats, ...(info.audioFormats ?? [])];
finalAudioFormat = selectAudioFormat(
audioPool,
formatId,
isAudioOnly,
needsWebm,
audioLang
);
} else {
finalAudioFormat = null;
}
return { isAudioOnly, finalVideoFormat, finalAudioFormat, requestedFormat };
}
function determineExtension(
isAudioOnly: boolean,
finalVideoFormat: Format | null,
finalAudioFormat: Format | null,
requestedFormat: Format | undefined,
formatId: string
) {
let emeExtension = isAudioOnly ? finalAudioFormat?.extension || 'mp3' : 'mp4';
if (formatId.startsWith('photo')) {
emeExtension = requestedFormat?.extension || 'jpg';
} else if (finalVideoFormat) {
emeExtension = 'mp4';
}
return emeExtension;
}
export function buildStreamResponse(
info: VideoInfo,
videoTunnel: string | null | undefined,
audioTunnel: string | null | undefined,
isAudioOnly: boolean,
filename: string,
totalSize: number,
outputMeta: Record<string, unknown>,
directUrl?: string
) {
if (videoTunnel && audioTunnel && !isAudioOnly) {
return {
status: 'local-processing',
type: 'proxy',
tunnel: [videoTunnel, audioTunnel],
output: { filename, totalSize, ...outputMeta },
videoUrl: videoTunnel,
audioUrl: audioTunnel,
title: info.title,
filename,
};
}
const tunnelPath = videoTunnel || audioTunnel;
if (!tunnelPath || tunnelPath.includes('PENDING_DECIPHER')) {
throw new Error(
'No valid proxy tunnel could be resolved or stream is encrypted.'
);
}
return {
status: 'local-processing',
type: 'proxy',
tunnel: [tunnelPath],
output: { filename, totalSize, ...outputMeta },
videoUrl: isAudioOnly ? undefined : videoTunnel,
audioUrl: audioTunnel,
title: info.title,
filename,
directUrl,
};
}
export function parseRequestParams(req: Request) {
const videoURLParam = req.query.url as string;
const clientId = (req.query.id as string) || undefined;
const formatId = req.query.formatId as string;
const audioLang = (req.query.audioLang as string) || undefined;
const decoded = decodeUrlIfNeeded(videoURLParam);
const videoURL = decoded ? decoded.split('&id=')[0].split('?id=')[0] : '';
return { videoURL, clientId, formatId, audioLang };
}
export async function resolveManifests(
req: Request,
videoURL: string,
clientId: string | undefined,
formatId: string
) {
const cookieArgs = await getCookieArgs(videoURL, clientId);
let info: VideoInfo | null = await getVideoInfo(
videoURL,
cookieArgs,
false,
null,
clientId,
true
).catch(() => {
/* ignore */ return null;
});
if (!info) throw new Error('Failed to fetch media information.');
// download intent forces full decipher
if (info.isPartial && req.query.full === '1') {
const full = await getVideoInfo(
videoURL,
cookieArgs,
true,
null,
clientId
).catch(() => null);
if (full?.formats?.length) info = full;
}
const isSpotify = videoURL.includes('spotify.com');
const resolvedTargetURL = isSpotify ? info.targetUrl || videoURL : videoURL;
const audioLang = (req.query.audioLang as string) || undefined;
const { isAudioOnly, finalVideoFormat, finalAudioFormat, requestedFormat } =
resolveFormatDetails(info, formatId, isSpotify, audioLang);
const videoTunnel = buildProxyUrl(
req,
finalVideoFormat,
resolvedTargetURL as string
);
const audioTunnel = buildProxyUrl(
req,
finalAudioFormat,
resolvedTargetURL as string
);
// raw url for open-cors direct download
const directUrl =
!isAudioOnly &&
!finalAudioFormat &&
finalVideoFormat &&
isDirect(finalVideoFormat)
? finalVideoFormat.url
: undefined;
const emeExtension = determineExtension(
isAudioOnly,
finalVideoFormat,
finalAudioFormat,
requestedFormat,
formatId
);
const filename = getSanitizedFilename(
info.title,
info.uploader,
emeExtension,
isSpotify
);
const outputMeta = getOutputMetadata(isAudioOnly, emeExtension, info);
let totalSize = 0;
try {
totalSize =
(estimateFilesize(
finalVideoFormat || ({} as Format),
info.duration || 0
) || 0) +
(estimateFilesize(
finalAudioFormat || ({} as Format),
info.duration || 0
) || 0);
} catch (error: unknown) {
console.warn('[Size] Estimation failed:', (error as Error).message);
}
return {
info,
videoTunnel,
audioTunnel,
isAudioOnly,
filename,
totalSize,
outputMeta,
directUrl,
};
}