feat(StreamingInfoOptions)!: Add is_sabr option (#974)

Returns a manifest suitable for use in SABR player implementations.
This commit is contained in:
Luan
2025-06-08 07:02:10 -03:00
committed by GitHub
parent f202bcdeb7
commit 561e60b934
3 changed files with 50 additions and 32 deletions

View File

@@ -31,7 +31,7 @@ import type PlayerLiveStoryboardSpec from '../../parser/classes/PlayerLiveStoryb
import type PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.js';
export default class MediaInfo {
readonly #page: [IPlayerResponse, INextResponse?];
readonly #page: [ IPlayerResponse, INextResponse? ];
readonly #actions: Actions;
readonly #cpn: string;
readonly #playback_tracking?: IPlaybackTracking;
@@ -46,7 +46,7 @@ export default class MediaInfo {
public playability_status?: IPlayabilityStatus;
public player_config?: IPlayerConfig;
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
constructor(data: [ ApiResponse, ApiResponse? ], actions: Actions, cpn: string) {
this.#actions = actions;
const info = Parser.parseResponse<IPlayerResponse>(data[0].data.playerResponse ? data[0].data.playerResponse : data[0].data);
@@ -98,13 +98,18 @@ export default class MediaInfo {
/**
* Generates a DASH manifest from the streaming data.
* @param url_transformer - Function to transform the URLs.
* @param format_filter - Function to filter the formats.
* @param options - Additional options to customise the manifest generation
* @param options
* @returns DASH manifest
*/
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter, options: DashOptions = { include_thumbnails: false }): Promise<string> {
async toDash(options: {
url_transformer?: URLTransformer;
format_filter?: FormatFilter;
include_thumbnails?: boolean;
captions_format?: string;
manifest_options?: DashOptions;
} = {}): Promise<string> {
const player_response = this.#page[0];
const manifest_options = options.manifest_options || {};
if (player_response.video_details && (player_response.video_details.is_live)) {
throw new InnertubeError('Generating DASH manifests for live videos is not supported. Please use the DASH and HLS manifests provided by YouTube in `streaming_data.dash_manifest_url` and `streaming_data.hls_manifest_url` instead.');
@@ -113,28 +118,28 @@ export default class MediaInfo {
let storyboards;
let captions;
if (options.include_thumbnails && player_response.storyboards) {
if (manifest_options.include_thumbnails && player_response.storyboards) {
storyboards = player_response.storyboards;
}
if (typeof options.captions_format === 'string' && player_response.captions?.caption_tracks) {
if (typeof manifest_options.captions_format === 'string' && player_response.captions?.caption_tracks) {
captions = player_response.captions.caption_tracks;
}
return FormatUtils.toDash(
this.streaming_data,
this.page[0].video_details?.is_post_live_dvr,
url_transformer,
format_filter,
options.url_transformer,
options.format_filter,
this.#cpn,
this.#actions.session.player,
this.#actions,
storyboards,
captions,
options
manifest_options
);
}
/**
* Get a cleaned up representation of the adaptive_formats
*/
@@ -238,7 +243,7 @@ export default class MediaInfo {
/**
* Parsed InnerTube response.
*/
get page(): [IPlayerResponse, INextResponse?] {
get page(): [ IPlayerResponse, INextResponse? ] {
return this.#page;
}
}

View File

@@ -27,4 +27,9 @@ export interface StreamingInfoOptions {
* Defaults to `(audio_track_display_name) => audio_track_display_name + " (Stable Volume)"`
*/
label_drc_multiple?: (audio_track_display_name: string) => string;
/**
* If `true`, the generated manifest will contain URLs that are suitable for use with the SABR protocol.
*/
is_sabr?: boolean;
}

View File

@@ -315,13 +315,20 @@ function getSegmentInfo(
actions?: Actions,
player?: Player,
cpn?: string,
shared_post_live_dvr_info?: SharedPostLiveDvrInfo
shared_post_live_dvr_info?: SharedPostLiveDvrInfo,
is_sabr?: boolean
) {
const url = new URL(format.decipher(player));
url.searchParams.set('cpn', cpn || '');
const transformed_url = url_transformer(url).toString();
let transformed_url = '';
if (is_sabr) {
const formatKey = `${format.itag || ''}:${format.xtags || ''}`;
transformed_url = `sabr://${format.has_video ? 'video' : 'audio'}?key=${formatKey}`;
} else {
const url = new URL(format.decipher(player));
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 });
@@ -392,11 +399,9 @@ function getAudioRepresentation(
actions?: Actions,
player?: Player,
cpn?: string,
shared_post_live_dvr_info?: SharedPostLiveDvrInfo
shared_post_live_dvr_info?: SharedPostLiveDvrInfo,
is_sabr?: boolean
) {
const url = new URL(format.decipher(player));
url.searchParams.set('cpn', cpn || '');
const uid_parts = [ format.itag.toString() ];
if (format.audio_track) {
@@ -413,7 +418,7 @@ function getAudioRepresentation(
codecs: !hoisted.includes('codecs') ? getStringBetweenStrings(format.mime_type, 'codecs="', '"') : undefined,
audio_sample_rate: !hoisted.includes('audio_sample_rate') ? format.audio_sample_rate : undefined,
channels: !hoisted.includes('AudioChannelConfiguration') ? format.audio_channels || 2 : undefined,
segment_info: getSegmentInfo(format, url_transformer, actions, player, cpn, shared_post_live_dvr_info)
segment_info: getSegmentInfo(format, url_transformer, actions, player, cpn, shared_post_live_dvr_info, is_sabr)
};
return rep;
@@ -447,7 +452,8 @@ function getAudioSet(
player?: Player,
cpn?: string,
shared_post_live_dvr_info?: SharedPostLiveDvrInfo,
drc_labels?: DrcLabels
drc_labels?: DrcLabels,
is_sabr?: boolean
) {
const first_format = formats[0];
const { audio_track } = first_format;
@@ -475,7 +481,7 @@ function getAudioSet(
track_name,
track_roles: getTrackRoles(first_format, has_drc_streams),
channels: hoistAudioChannelsIfPossible(formats, hoisted),
representations: formats.map((format) => getAudioRepresentation(format, hoisted, url_transformer, actions, player, cpn, shared_post_live_dvr_info))
representations: formats.map((format) => getAudioRepresentation(format, hoisted, url_transformer, actions, player, cpn, shared_post_live_dvr_info, is_sabr))
};
return set;
@@ -556,7 +562,8 @@ function getVideoRepresentation(
player?: Player,
actions?: Actions,
cpn?: string,
shared_post_live_dvr_info?: SharedPostLiveDvrInfo
shared_post_live_dvr_info?: SharedPostLiveDvrInfo,
is_sabr?: boolean
) {
const rep: VideoRepresentation = {
uid: format.itag.toString(),
@@ -565,7 +572,7 @@ function getVideoRepresentation(
height: format.height,
codecs: !hoisted.includes('codecs') ? getStringBetweenStrings(format.mime_type, 'codecs="', '"') : undefined,
fps: !hoisted.includes('fps') ? format.fps : undefined,
segment_info: getSegmentInfo(format, url_transformer, actions, player, cpn, shared_post_live_dvr_info)
segment_info: getSegmentInfo(format, url_transformer, actions, player, cpn, shared_post_live_dvr_info, is_sabr)
};
return rep;
@@ -577,7 +584,8 @@ function getVideoSet(
player?: Player,
actions?: Actions,
cpn?: string,
shared_post_live_dvr_info?: SharedPostLiveDvrInfo
shared_post_live_dvr_info?: SharedPostLiveDvrInfo,
is_sabr?: boolean
) {
const first_format = formats[0];
const color_info = getColorInfo(first_format);
@@ -588,7 +596,7 @@ function getVideoSet(
color_info,
codecs: hoistCodecsIfPossible(formats, hoisted),
fps: hoistNumberAttributeIfPossible(formats, 'fps', hoisted),
representations: formats.map((format) => getVideoRepresentation(format, url_transformer, hoisted, player, actions, cpn, shared_post_live_dvr_info))
representations: formats.map((format) => getVideoRepresentation(format, url_transformer, hoisted, player, actions, cpn, shared_post_live_dvr_info, is_sabr))
};
return set;
@@ -860,9 +868,9 @@ export function getStreamingInfo(
};
}
const audio_sets = audio_groups.map((formats) => getAudioSet(formats, url_transformer, actions, player, cpn, shared_post_live_dvr_info, drc_labels));
const audio_sets = audio_groups.map((formats) => getAudioSet(formats, url_transformer, actions, player, cpn, shared_post_live_dvr_info, drc_labels, options?.is_sabr));
const video_sets = video_groups.map((formats) => getVideoSet(formats, url_transformer, player, actions, cpn, shared_post_live_dvr_info));
const video_sets = video_groups.map((formats) => getVideoSet(formats, url_transformer, player, actions, cpn, shared_post_live_dvr_info, options?.is_sabr));
let image_sets: ImageSet[] = [];