per-group track config: probe bitrate, per-file track map, folder multi-picker
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,12 @@ function trackName(tags?: { title?: string; handler_name?: string }): string {
|
||||
return tags?.title || (handler && !/Handler$/.test(handler) ? handler : '');
|
||||
}
|
||||
|
||||
// ffprobe bit_rate (bits/s, as a string) → rounded kbps, or null when the container doesn't report it.
|
||||
function kbps(bitRate?: string): number | null {
|
||||
const n = Number(bitRate);
|
||||
return Number.isFinite(n) && n > 0 ? Math.round(n / 1000) : null;
|
||||
}
|
||||
|
||||
// Serve a video with a chosen audio track selected: fast `-c copy` remux (video untouched, other
|
||||
// audio dropped) cached under the user's data dir, so it streams with byte-range seeking like /raw.
|
||||
// In-flight remuxes are shared so concurrent requests for the same track don't race on the temp file.
|
||||
@@ -390,13 +396,13 @@ router.get('/audio-tracks', async (ctx) => {
|
||||
const absPath = resolveUserPath(rootDir, relPath);
|
||||
|
||||
const proc = Bun.spawn(
|
||||
['ffprobe', '-v', 'error', '-select_streams', 'a', '-show_entries', 'stream=channels,codec_name:stream_tags=language,title,handler_name', '-of', 'json', absPath],
|
||||
['ffprobe', '-v', 'error', '-select_streams', 'a', '-show_entries', 'stream=channels,codec_name,bit_rate:stream_tags=language,title,handler_name', '-of', 'json', absPath],
|
||||
{ stdout: 'pipe', stderr: 'ignore' },
|
||||
);
|
||||
const out = await new Response(proc.stdout).text();
|
||||
await proc.exited;
|
||||
|
||||
type ProbeAudio = { channels?: number; codec_name?: string; tags?: { language?: string; title?: string; handler_name?: string } };
|
||||
type ProbeAudio = { channels?: number; codec_name?: string; bit_rate?: string; tags?: { language?: string; title?: string; handler_name?: string } };
|
||||
let streams: ProbeAudio[] = [];
|
||||
try {
|
||||
streams = (JSON.parse(out).streams as ProbeAudio[]) ?? [];
|
||||
@@ -408,6 +414,7 @@ router.get('/audio-tracks', async (ctx) => {
|
||||
id,
|
||||
codec: s.codec_name ?? '',
|
||||
channels: s.channels ?? 0,
|
||||
bitrate: kbps(s.bit_rate),
|
||||
lang: s.tags?.language ?? '',
|
||||
title: trackName(s.tags),
|
||||
}));
|
||||
@@ -424,19 +431,19 @@ const VIDEO_EXTENSIONS = new Set([
|
||||
'ts', 'm2ts', 'mts', '3gp', 'ogv', 'vob', 'divx', 'asf', 'f4v', 'rm', 'rmvb',
|
||||
]);
|
||||
|
||||
type FolderAudioTrack = { id: number; codec: string; channels: number; lang: string; title: string };
|
||||
type FolderAudioTrack = { id: number; codec: string; channels: number; bitrate: number | null; lang: string; title: string };
|
||||
type FolderSubtitleTrack = { id: number; codec: string; lang: string; title: string };
|
||||
type ProbedTracks = { audio: FolderAudioTrack[]; subtitle: FolderSubtitleTrack[] };
|
||||
|
||||
async function probeVideoTracks(absPath: string): Promise<ProbedTracks> {
|
||||
const proc = Bun.spawn(
|
||||
['ffprobe', '-v', 'error', '-show_entries', 'stream=codec_type,codec_name,channels:stream_tags=language,title,handler_name', '-of', 'json', absPath],
|
||||
['ffprobe', '-v', 'error', '-show_entries', 'stream=codec_type,codec_name,channels,bit_rate:stream_tags=language,title,handler_name', '-of', 'json', absPath],
|
||||
{ stdout: 'pipe', stderr: 'ignore' },
|
||||
);
|
||||
const out = await new Response(proc.stdout).text();
|
||||
await proc.exited;
|
||||
|
||||
type ProbeStream = { codec_type?: string; codec_name?: string; channels?: number; tags?: { language?: string; title?: string; handler_name?: string } };
|
||||
type ProbeStream = { codec_type?: string; codec_name?: string; channels?: number; bit_rate?: string; tags?: { language?: string; title?: string; handler_name?: string } };
|
||||
let streams: ProbeStream[] = [];
|
||||
try {
|
||||
streams = (JSON.parse(out).streams as ProbeStream[]) ?? [];
|
||||
@@ -446,7 +453,7 @@ async function probeVideoTracks(absPath: string): Promise<ProbedTracks> {
|
||||
|
||||
const audio = streams
|
||||
.filter((s) => s.codec_type === 'audio')
|
||||
.map((s, id) => ({ id, codec: s.codec_name ?? '', channels: s.channels ?? 0, lang: s.tags?.language ?? '', title: trackName(s.tags) }));
|
||||
.map((s, id) => ({ id, codec: s.codec_name ?? '', channels: s.channels ?? 0, bitrate: kbps(s.bit_rate), lang: s.tags?.language ?? '', title: trackName(s.tags) }));
|
||||
// subtitle `id` is the index among ALL subtitle streams (what `-map 0:s:id` expects), assigned
|
||||
// before filtering out image-based tracks that can't become soft subs.
|
||||
const subtitle = streams
|
||||
|
||||
Reference in New Issue
Block a user