mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 01:22:11 +00:00
feat(StreamingInfoOptions)!: Add is_sabr option (#974)
Returns a manifest suitable for use in SABR player implementations.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user