nexstream-api / backend /src /utils /media /format.util.ts
circleci
deploy 60e0f6e
a39cdc9
Raw
History Blame Contribute Delete
11.3 kB
import { Format } from '../../types/index.js';
export interface RawFormat {
url?: string;
bitrate?: number;
average_bitrate?: number;
height?: number;
width?: number;
fps?: string | number;
vcodec?: string;
acodec?: string;
filesize?: number;
filesize_approx?: number;
formatId?: string | number;
format_id?: string | number;
itag?: string | number;
ext?: string;
extension?: string;
resolution?: string;
quality_label?: string;
quality?: string;
abr?: number;
tbr?: number;
isAudio?: boolean;
is_audio?: boolean;
hasAudio?: boolean;
has_audio?: boolean;
isVideo?: boolean;
is_video?: boolean;
hasVideo?: boolean;
has_video?: boolean;
isMuxed?: boolean;
is_muxed?: boolean;
audioUrl?: string;
audio_url?: string;
language?: string;
format_note?: string;
language_preference?: number;
is_original?: boolean;
is_dubbed?: boolean;
audio_track?: {
id?: string;
display_name?: string;
audio_is_default?: boolean;
};
[key: string]: unknown;
}
export interface AudioTrackInfo {
language?: string;
languageName?: string;
isOriginal?: boolean;
}
export function resolveAudioTrack(format: RawFormat): AudioTrackInfo {
const audioTrack = format.audio_track;
let language: string | undefined =
(typeof format.language === 'string' && format.language) || undefined;
if (!language && audioTrack?.id) {
language = String(audioTrack.id).split('.')[0] || undefined;
}
let languageName: string | undefined = audioTrack?.display_name || undefined;
const note = String(format.format_note || '').toLowerCase();
const noteSaysOriginal = note.includes('original');
const noteSaysDubbed = note.includes('dub');
const langPref = Number(format.language_preference);
const isOriginal =
format.is_original === true ||
audioTrack?.audio_is_default === true ||
(Number.isFinite(langPref) && langPref >= 10) ||
noteSaysOriginal ||
(note.includes('default') && !noteSaysDubbed);
if (!languageName && format.format_note) {
const cleaned = String(format.format_note)
.replace(/,\s*(ultralow|low|medium|high).*$/iu, '')
.replace(/\s*\((default|original)\)/giu, '')
.trim();
languageName = cleaned || undefined;
}
if (!language && !languageName) return {};
return { language, languageName, isOriginal: isOriginal || undefined };
}
export function estimateFilesize(format: RawFormat, duration: number): number {
if (format.filesize) return Number(format.filesize);
if (format.filesize_approx) return Number(format.filesize_approx);
const bitrate =
Number(format.average_bitrate || format.bitrate) ||
(format.tbr ? Number(format.tbr) * 1024 : 0);
if (bitrate && duration) {
return (bitrate * duration) / 8;
}
return 0;
}
export function getFormatHeight(format: RawFormat): number {
if (format.height) return Number(format.height);
const resolution =
format.resolution || format.quality_label || format.quality || '';
const match = String(resolution).match(/(\d{3,4})p/u);
if (match) return parseInt(match[1], 10);
return 0;
}
function resolveResolution(format: RawFormat): {
resolution: string;
height: number;
} {
let height = getFormatHeight(format);
let resolution = String(
format.resolution || format.quality_label || format.quality || ''
);
const dimMatch = resolution.match(/(\d+)x(\d+)/u);
if (dimMatch) {
const width = parseInt(dimMatch[1], 10);
const hValue = parseInt(dimMatch[2], 10);
height = Math.min(width, hValue);
}
if (height) {
resolution = `${height}p`;
} else if (resolution) {
const hMatch = resolution.match(/(\d{3,4})p?/u);
if (hMatch) {
resolution = `${hMatch[1]}p`;
height = parseInt(hMatch[1], 10);
}
}
if (!resolution || resolution === '') resolution = 'Unknown';
return { resolution, height };
}
function resolveCodecs(format: RawFormat) {
const acodec = String(
format.acodec ||
(format.vcodec && format.vcodec !== 'none' ? 'yes' : 'none')
);
const vcodecStr = String(format.vcodec || '');
const isMuxed =
format.isMuxed ||
format.is_muxed ||
(vcodecStr !== 'none' &&
vcodecStr !== '' &&
acodec !== 'none' &&
acodec !== '');
return { acodec, vcodecStr, isMuxed: Boolean(isMuxed) };
}
function resolveVideoAudioFlags(format: RawFormat, acodec: string) {
const isVideoFlag =
format.isVideo === true ||
format.is_video === true ||
format.hasVideo === true ||
format.has_video === true;
const isVideo = isVideoFlag || (format.vcodec && format.vcodec !== 'none');
const isAudio = Boolean(
format.isAudio ||
format.is_video === false ||
format.is_audio ||
format.hasAudio ||
format.has_video ||
(acodec !== 'none' && acodec !== '')
);
return { isVideo: Boolean(isVideo), isAudio };
}
function mapRawToFormat(format: RawFormat, duration: number): Format | null {
if (!format.url) return null;
const { resolution, height } = resolveResolution(format);
const { acodec, vcodecStr, isMuxed } = resolveCodecs(format);
const { isVideo, isAudio } = resolveVideoAudioFlags(format, acodec);
const filesize = calculateFinalSize(format, duration);
// force mp4 container for all video
const rawExtension = format.extension || format.ext || 'mp4';
const extension = isVideo && !isAudio ? 'mp4' : rawExtension;
return {
formatId: String(format.formatId || format.format_id || format.itag),
extension,
url: format.url,
resolution,
quality: resolution,
filesize,
fps: Number(format.fps) || 0,
height,
vcodec: vcodecStr || 'yes',
acodec,
isMuxed,
isVideo,
isAudio,
audioUrl: format.audioUrl || format.audio_url || undefined,
itag: Number(format.itag) || 0,
width: Number(format.width) || 0,
};
}
function calculateFinalSize(format: RawFormat, duration: number) {
return Math.round(estimateFilesize(format, duration));
}
const AV1_FORMAT_IDS = new Set([
'394',
'395',
'396',
'397',
'398',
'399',
'400',
'401',
'571',
]);
export function processVideoFormats(info: {
duration?: number;
formats?: RawFormat[];
streaming_data?: { formats?: RawFormat[]; adaptive_formats?: RawFormat[] };
}): Format[] {
const rawList: RawFormat[] = [
...(info.formats || []),
...(info.streaming_data?.formats || []),
...(info.streaming_data?.adaptive_formats || []),
];
if (rawList.length === 0) return [];
const uniqueFormats = new Map<string, Format>();
const duration = info.duration || 0;
const processed = rawList
.map((format: RawFormat) => mapRawToFormat(format, duration))
.filter((item): item is Format => item !== null)
.filter(
(format: Format) =>
format.isVideo || String(format.formatId).startsWith('photo')
)
.filter((format: Format) => {
// av1 must drop before dedup competition
const vcodec = String(format.vcodec || '');
if (vcodec.startsWith('av01')) return false;
return !AV1_FORMAT_IDS.has(String(format.formatId || ''));
});
for (const format of processed) {
const resKey = String(format.formatId).startsWith('photo')
? String(format.formatId)
: format.resolution && format.resolution !== 'Unknown'
? format.resolution
: `Unknown-${format.height || format.formatId}`;
const key = `${resKey}-${format.extension}`;
const existing = uniqueFormats.get(key);
// prioritize muxed formats
if (
!existing ||
(format.isMuxed && !existing.isMuxed) ||
(format.isMuxed === existing.isMuxed &&
(format.filesize || 0) > (existing.filesize || 0))
) {
uniqueFormats.set(key, format);
}
}
return Array.from(uniqueFormats.values()).sort(
(first: Format, second: Format) =>
(second.height || 0) - (first.height || 0)
);
}
// kept for callers that need explicit filtering
export function dropAV1Formats(formats: Format[]): Format[] {
return formats.filter((format) => {
const vcodec = String(format.vcodec || '');
if (vcodec.startsWith('av01')) return false;
return !AV1_FORMAT_IDS.has(String(format.formatId || ''));
});
}
export function processAudioFormats(info: {
formats?: RawFormat[];
streaming_data?: { formats?: RawFormat[]; adaptive_formats?: RawFormat[] };
}): Format[] {
const formats: RawFormat[] = [
...(info.formats || []),
...(info.streaming_data?.formats || []),
...(info.streaming_data?.adaptive_formats || []),
];
if (formats.length === 0) return [];
const uniqueFormats = new Map<string, Format>();
const processed = formats
.map((format: RawFormat): Format | null => {
if (!format.url) return null;
const { acodec, vcodecStr } = resolveCodecs(format);
const { isVideo, isAudio } = resolveVideoAudioFlags(format, acodec);
// check audio streams
if (!isAudio || isVideo) return null;
// drop opus/webm; poor device compatibility
const rawExt = String(format.ext || format.extension || '');
if (rawExt === 'webm' || rawExt === 'opus' || acodec.includes('opus'))
return null;
const abr = Number(format.abr || format.tbr || 0);
const quality = abr > 0 ? `${Math.round(abr)}kbps` : 'Audio';
let extension = String(format.ext || format.extension || 'm4a');
const formatId = String(
format.formatId || format.format_id || format.itag
);
if (
extension === 'mp4' ||
extension === 'm4a' ||
acodec.includes('mp4a') ||
formatId.includes('m4a')
) {
extension = 'm4a';
}
const track = resolveAudioTrack(format);
return {
formatId,
extension,
url: format.url,
quality,
resolution: quality,
filesize: Number(
format.filesize ||
format.filesize_approx ||
estimateFilesize(format, 0)
),
fps: 0,
height: 0,
vcodec: vcodecStr,
acodec,
isMuxed: false,
isVideo: false,
isAudio: true,
itag: Number(format.itag) || 0,
language: track.language,
languageName: track.languageName,
isOriginal: track.isOriginal,
};
})
.filter((item): item is Format => item !== null);
for (const format of processed) {
const qualityKey =
format.quality && format.quality !== 'Audio'
? format.quality
: `Audio-${format.filesize || format.formatId}`;
// keep dubs distinct by language
const langKey = format.language ? `-${format.language}` : '';
const key = `${qualityKey}-${format.extension}${langKey}`;
const existing = uniqueFormats.get(key);
if (!existing || (format.filesize || 0) > (existing.filesize || 0)) {
uniqueFormats.set(key, format);
}
}
return Array.from(uniqueFormats.values()).sort(
(first: Format, second: Format) => {
const origA = first.isOriginal ? 1 : 0;
const origB = second.isOriginal ? 1 : 0;
if (origA !== origB) return origB - origA;
const abrA = parseInt(first.quality || '0', 10) || 0;
const abrB = parseInt(second.quality || '0', 10) || 0;
return abrB - abrA;
}
);
}