From 458f88104344fda2e2ca41c9504d946d367a0cb7 Mon Sep 17 00:00:00 2001 From: Luan Date: Sat, 27 Dec 2025 15:29:08 -0300 Subject: [PATCH] feat(StreamingInfo): Label Voice Boost audio streams (#1105) * feat(StreamingInfo): Label Voice Boost audio streams * chore(StreamingInfoOptions): Clarify vb desc a bit * fix(StreamingInfoOptions): Fix `label_original` description * chore: lint --- src/types/StreamingInfoOptions.ts | 16 +++++++- src/utils/StreamingInfo.ts | 66 ++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/src/types/StreamingInfoOptions.ts b/src/types/StreamingInfoOptions.ts index d3b9eaaa..d6a735c9 100644 --- a/src/types/StreamingInfoOptions.ts +++ b/src/types/StreamingInfoOptions.ts @@ -9,7 +9,7 @@ export interface StreamingInfoOptions { */ captions_format?: 'vtt' | 'ttml'; /** - * The label to use for the non-DRC streams when a video has DRC and streams. + * The label to use for the non-DRC/VB streams when a video has DRC and Voice Boost streams. * * Defaults to `"Original"` */ @@ -27,7 +27,19 @@ export interface StreamingInfoOptions { * Defaults to `(audio_track_display_name) => audio_track_display_name + " (Stable Volume)"` */ label_drc_multiple?: (audio_track_display_name: string) => string; - + /** + * The label to use for the VB (Voice Boost) streams when a video has VB streams. + * + * Defaults to `"Voice Boost"` + */ + label_vb?: string; + /** + * A function that generates the label to use for the Voice Boost streams when a video has multiple audio tracks and Voice Boost streams. + * The non-Voice Boost streams use the unmodified audio track label provided by YouTube. + * + * Defaults to `(audio_track_display_name) => audio_track_display_name + " (Voice Boost)"` + */ + label_vb_multiple?: (audio_track_display_name: string) => string; /** * If `true`, the generated manifest will contain URLs that are suitable for use with the SABR protocol. */ diff --git a/src/utils/StreamingInfo.ts b/src/utils/StreamingInfo.ts index aa0707e9..58728e70 100644 --- a/src/utils/StreamingInfo.ts +++ b/src/utils/StreamingInfo.ts @@ -157,15 +157,22 @@ interface DrcLabels { label_drc_multiple: (audio_track_display_name: string) => string; } +interface VbLabels { + label_original: string; + label_vb: string; + label_vb_multiple: (audio_track_display_name: string) => string; +} + function getFormatGroupings(formats: Format[], is_post_live_dvr: boolean) { const group_info = new Map(); const has_multiple_audio_tracks = formats.some((fmt) => !!fmt.audio_track); for (const format of formats) { - if ((!format.index_range || !format.init_range) && !format.is_type_otf && !is_post_live_dvr) { + if (((!format.index_range || !format.init_range) && !format.is_type_otf && !is_post_live_dvr)) { continue; } + const mime_type = format.mime_type.split(';')[0]; // Codec without any profile or level information @@ -177,8 +184,9 @@ function getFormatGroupings(formats: Format[], is_post_live_dvr: boolean) { const audio_track_id = format.audio_track?.id || ''; const drc = format.is_drc ? 'drc' : ''; + const vb = format.is_vb ? 'vb' : ''; - const group_id = `${mime_type}-${just_codec}-${color_info}-${audio_track_id}-${drc}`; + const group_id = `${mime_type}-${just_codec}-${color_info}-${audio_track_id}-${drc}-${vb}`; if (!group_info.has(group_id)) { group_info.set(group_id, []); @@ -324,7 +332,7 @@ async function getSegmentInfo( is_sabr?: boolean ) { let transformed_url = ''; - + if (is_sabr) { const formatKey = `${format.itag || ''}:${format.xtags || ''}`; transformed_url = `sabr://${format.has_video ? 'video' : 'audio'}?key=${formatKey}`; @@ -333,7 +341,7 @@ async function getSegmentInfo( url.searchParams.set('cpn', cpn || ''); transformed_url = url_transformer(url).toString(); } - + if (format.is_type_otf) { if (!actions) throw new InnertubeError('Unable to get segment durations for this OTF stream without an Actions instance', { format }); @@ -417,6 +425,10 @@ async function getAudioRepresentation( uid_parts.push('drc'); } + if (format.is_vb) { + uid_parts.push('vb'); + } + const rep: AudioRepresentation = { uid: uid_parts.join('-'), bitrate: format.bitrate, @@ -444,7 +456,7 @@ function getTrackRoles(format: Format, has_drc_streams: boolean) { if (format.is_descriptive) roles.push('description'); - if (format.is_drc) + if (format.is_drc || format.is_vb) roles.push('enhanced-audio-intelligibility'); return roles; @@ -458,6 +470,7 @@ async function getAudioSet( cpn?: string, shared_post_live_dvr_info?: SharedPostLiveDvrInfo, drc_labels?: DrcLabels, + vb_labels?: VbLabels, is_sabr?: boolean ) { const first_format = formats[0]; @@ -465,17 +478,27 @@ async function getAudioSet( const hoisted: string[] = []; const has_drc_streams = !!drc_labels; + const has_vb_streams = !!vb_labels; let track_name; if (audio_track) { if (has_drc_streams && first_format.is_drc) { track_name = drc_labels.label_drc_multiple(audio_track.display_name); + } else if (has_vb_streams && first_format.is_vb) { + track_name = vb_labels.label_vb_multiple(audio_track.display_name); } else { track_name = audio_track.display_name; } - } else if (has_drc_streams) { - track_name = first_format.is_drc ? drc_labels.label_drc : drc_labels.label_original; + } else if (has_drc_streams || has_vb_streams) { + if (has_drc_streams && first_format.is_drc) { + track_name = drc_labels.label_drc; + } else if (has_vb_streams && first_format.is_vb) { + track_name = vb_labels.label_vb; + } else { + // Both use the same param, so it doesn't matter which one is defined here. + track_name = (drc_labels || vb_labels)?.label_original; + } } const set: AudioSet = { @@ -868,8 +891,23 @@ export async function getStreamingInfo( }); let drc_labels: DrcLabels | undefined; + let vb_labels: VbLabels | undefined; - if (audio_groups.flat().some((format) => format.is_drc)) { + let hasDrc = false; + let hasVb = false; + + for (const ag of audio_groups.flat()) { + if (hasDrc === false && ag.is_drc) { + hasDrc = true; + } + + if (hasVb === false && ag.is_vb) { + hasVb = true; + } + } + + // TODO: Put these audio fields in a shared object to reduce dups. + if (hasDrc) { drc_labels = { label_original: options?.label_original || 'Original', label_drc: options?.label_drc || 'Stable Volume', @@ -877,7 +915,15 @@ export async function getStreamingInfo( }; } - const audio_sets = await Promise.all(audio_groups.map((formats) => getAudioSet(formats, url_transformer, actions, player, cpn, shared_post_live_dvr_info, drc_labels, options?.is_sabr))); + if (hasVb) { + vb_labels = { + label_original: options?.label_original || 'Original', + label_vb: options?.label_vb || 'Voice Boost', + label_vb_multiple: options?.label_vb_multiple || ((display_name) => `${display_name} (Voice Boost)`) + }; + } + + const audio_sets = await Promise.all(audio_groups.map((formats) => getAudioSet(formats, url_transformer, actions, player, cpn, shared_post_live_dvr_info, drc_labels, vb_labels, options?.is_sabr))); const video_sets = await Promise.all(video_groups.map((formats) => getVideoSet(formats, url_transformer, player, actions, cpn, shared_post_live_dvr_info, options?.is_sabr))); @@ -908,7 +954,7 @@ export async function getStreamingInfo( text_sets = getTextSets(caption_tracks, options.captions_format, url_transformer); } - const info : StreamingInfo = { + const info: StreamingInfo = { getDuration, audio_sets, video_sets,