Compare commits

..

3 Commits

Author SHA1 Message Date
LuanRT
e147a01ba5 chore: v5.5.0 release 2023-07-16 20:58:15 +00:00
LuanRT
499937fe3a chore: v5.4.0 release 2023-07-14 03:01:47 +00:00
LuanRT
0582bcb677 chore: v5.3.0 release 2023-07-11 18:49:53 +00:00
19 changed files with 387 additions and 41 deletions

View File

@@ -542,6 +542,8 @@ Retrieves contents for a given channel.
- `<channel>#getVideos()`
- `<channel>#getShorts()`
- `<channel>#getLiveStreams()`
- `<channel>#getReleases()`
- `<channel>#getPodcasts()`
- `<channel>#getPlaylists()`
- `<channel>#getHome()`
- `<channel>#getCommunity()`

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "5.2.1",
"version": "5.5.0",
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
"type": "module",
"types": "./dist/src/platform/lib.d.ts",
@@ -84,7 +84,7 @@
},
"license": "MIT",
"dependencies": {
"jintr": "^1.0.0",
"jintr": "^1.1.0",
"linkedom": "^0.14.12",
"tslib": "^2.5.0",
"undici": "^5.19.1"

View File

@@ -8,6 +8,11 @@ export const PATH = '/player';
* @returns The payload.
*/
export function build(opts: PlayerEndpointOptions): IPlayerRequest {
const is_android =
opts.client === 'ANDROID' ||
opts.client === 'YTMUSIC_ANDROID' ||
opts.client === 'YTSTUDIO_ANDROID';
return {
playbackContext: {
contentPlaybackContext: {
@@ -37,8 +42,8 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest {
...{
client: opts.client,
playlistId: opts.playlist_id,
// Workaround streaming URLs returning 403 when using Android clients and throttling in web clients.
params: '2AMBCgIQBg'
// Workaround streaming URLs returning 403 or getting throttled when using Android based clients.
params: is_android ? '2AMBCgIQBg' : opts.params
}
};
}

View File

@@ -2,6 +2,7 @@ import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import ContinuationItem from './ContinuationItem.ts';
import EngagementPanelTitleHeader from './EngagementPanelTitleHeader.ts';
import MacroMarkersList from './MacroMarkersList.ts';
import SectionList from './SectionList.ts';
import StructuredDescriptionContent from './StructuredDescriptionContent.ts';
@@ -9,7 +10,7 @@ export default class EngagementPanelSectionList extends YTNode {
static type = 'EngagementPanelSectionList';
header: EngagementPanelTitleHeader | null;
content: SectionList | ContinuationItem | StructuredDescriptionContent | null;
content: SectionList | ContinuationItem | StructuredDescriptionContent | MacroMarkersList | null;
target_id?: string;
panel_identifier?: string;
visibility?: string;
@@ -17,7 +18,7 @@ export default class EngagementPanelSectionList extends YTNode {
constructor(data: RawNode) {
super();
this.header = Parser.parseItem(data.header, EngagementPanelTitleHeader);
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent ]);
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent, MacroMarkersList ]);
this.panel_identifier = data.panelIdentifier;
this.target_id = data.targetId;
this.visibility = data.visibility;

View File

@@ -0,0 +1,28 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import { Thumbnail } from '../misc.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Text from './misc/Text.ts';
export default class HashtagTile extends YTNode {
static type = 'HashtagTile';
hashtag: Text;
hashtag_info_text: Text;
hashtag_thumbnail: Thumbnail[];
endpoint: NavigationEndpoint;
hashtag_background_color: number;
hashtag_video_count: Text;
hashtag_channel_count: Text;
constructor(data: RawNode) {
super();
this.hashtag = new Text(data.hashtag);
this.hashtag_info_text = new Text(data.hashtagInfoText);
this.hashtag_thumbnail = Thumbnail.fromResponse(data.hashtagThumbnail);
this.endpoint = new NavigationEndpoint(data.onTapCommand);
this.hashtag_background_color = data.hashtagBackgroundColor;
this.hashtag_video_count = new Text(data.hashtagVideoCount);
this.hashtag_channel_count = new Text(data.hashtagChannelCount);
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../helpers.ts';
import { Parser, type RawNode } from '../index.ts';
import Menu from './menus/Menu.ts';
import Text from './misc/Text.ts';
export default class MacroMarkersInfoItem extends YTNode {
static type = 'MacroMarkersInfoItem';
info_text: Text;
menu: Menu | null;
constructor(data: RawNode) {
super();
this.info_text = new Text(data.infoText);
this.menu = Parser.parseItem(data.menu, Menu);
}
}

View File

@@ -0,0 +1,18 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import { Parser, type RawNode } from '../index.ts';
import { Text } from '../misc.ts';
import MacroMarkersInfoItem from './MacroMarkersInfoItem.ts';
import MacroMarkersListItem from './MacroMarkersListItem.ts';
export default class MacroMarkersList extends YTNode {
static type = 'MacroMarkersList';
contents: ObservedArray<MacroMarkersInfoItem | MacroMarkersListItem>;
sync_button_label: Text;
constructor(data: RawNode) {
super();
this.contents = Parser.parseArray(data.contents, [ MacroMarkersInfoItem, MacroMarkersListItem ]);
this.sync_button_label = new Text(data.syncButtonLabel);
}
}

View File

@@ -17,10 +17,10 @@ export default class PlayerCaptionsTracklist extends YTNode {
audio_tracks?: {
audio_track_id: string;
captions_initial_state: string;
default_caption_track_index: number;
default_caption_track_index?: number;
has_default_track: boolean;
visibility: string;
caption_track_indices: number;
caption_track_indices: number[];
}[];
default_audio_track_index?: number;

View File

@@ -1,6 +1,7 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import PlaylistCustomThumbnail from './PlaylistCustomThumbnail.ts';
import PlaylistVideoThumbnail from './PlaylistVideoThumbnail.ts';
import Author from './misc/Author.ts';
import Text from './misc/Text.ts';
@@ -13,7 +14,7 @@ export default class Playlist extends YTNode {
title: Text;
author: Text | Author;
thumbnails: Thumbnail[];
thumbnail_renderer?: PlaylistVideoThumbnail;
thumbnail_renderer?: PlaylistVideoThumbnail | PlaylistCustomThumbnail;
video_count: Text;
video_count_short: Text;
first_videos: ObservedArray<YTNode>;
@@ -44,7 +45,7 @@ export default class Playlist extends YTNode {
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
if (Reflect.has(data, 'thumbnailRenderer')) {
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, PlaylistVideoThumbnail) || undefined;
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, [ PlaylistVideoThumbnail, PlaylistCustomThumbnail ]) || undefined;
}
if (Reflect.has(data, 'viewPlaylistText')) {

View File

@@ -0,0 +1,25 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import Text from './misc/Text.ts';
export default class Quiz extends YTNode {
static type = 'Quiz';
choices: {
text: Text;
is_correct: boolean;
}[];
total_votes: Text;
constructor(data: RawNode) {
super();
this.choices = data.choices.map((choice: RawNode) => ({
text: new Text(choice.text),
is_correct: choice.isCorrect
}));
this.total_votes = new Text(data.totalVotes);
}
}

View File

@@ -1,6 +1,7 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import ExpandableVideoDescriptionBody from './ExpandableVideoDescriptionBody.ts';
import HorizontalCardList from './HorizontalCardList.ts';
import VideoDescriptionHeader from './VideoDescriptionHeader.ts';
import VideoDescriptionInfocardsSection from './VideoDescriptionInfocardsSection.ts';
import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.ts';
@@ -8,10 +9,10 @@ import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.ts';
export default class StructuredDescriptionContent extends YTNode {
static type = 'StructuredDescriptionContent';
items: ObservedArray<VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection | VideoDescriptionInfocardsSection>;
items: ObservedArray<VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection | VideoDescriptionInfocardsSection | HorizontalCardList>;
constructor(data: RawNode) {
super();
this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection ]);
this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection, HorizontalCardList ]);
}
}

View File

@@ -1,9 +1,10 @@
import Text from '../misc/Text.ts';
import Thumbnail from '../misc/Thumbnail.ts';
import CommentsSimplebox from './CommentsSimplebox.ts';
import CommentsEntryPointTeaser from './CommentsEntryPointTeaser.ts';
import { YTNode } from '../../helpers.ts';
import type { RawNode } from '../../index.ts';
import { Parser } from '../../index.ts';
import CommentsEntryPointTeaser from './CommentsEntryPointTeaser.ts';
export default class CommentsEntryPointHeader extends YTNode {
static type = 'CommentsEntryPointHeader';
@@ -12,7 +13,7 @@ export default class CommentsEntryPointHeader extends YTNode {
comment_count?: Text;
teaser_avatar?: Thumbnail[];
teaser_content?: Text;
content_renderer?: CommentsEntryPointTeaser | null;
content_renderer?: CommentsEntryPointTeaser | CommentsSimplebox | null;
simplebox_placeholder?: Text;
constructor(data: RawNode) {
@@ -35,7 +36,7 @@ export default class CommentsEntryPointHeader extends YTNode {
}
if (Reflect.has(data, 'contentRenderer')) {
this.content_renderer = Parser.parseItem(data.contentRenderer, CommentsEntryPointTeaser);
this.content_renderer = Parser.parseItem(data.contentRenderer, [ CommentsEntryPointTeaser, CommentsSimplebox ]);
}
if (Reflect.has(data, 'simpleboxPlaceholder')) {

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../../helpers.ts';
import Text from '../misc/Text.ts';
import Thumbnail from '../misc/Thumbnail.ts';
import type { RawNode } from '../../index.ts';
export default class CommentsSimplebox extends YTNode {
static type = 'CommentsSimplebox';
simplebox_avatar: Thumbnail[];
simplebox_placeholder: Text;
constructor(data: RawNode) {
super();
this.simplebox_avatar = Thumbnail.fromResponse(data.simpleboxAvatar);
this.simplebox_placeholder = new Text(data.simpleboxPlaceholder);
}
}

View File

@@ -46,6 +46,12 @@ export default class Format {
is_descriptive?: boolean;
is_original?: boolean;
color_info?: {
primaries?: string;
transfer_characteristics?: string;
matrix_coefficients?: string;
};
constructor(data: RawNode) {
this.itag = data.itag;
this.mime_type = data.mimeType;
@@ -81,14 +87,24 @@ export default class Format {
this.has_audio = !!data.audioBitrate || !!data.audioQuality;
this.has_video = !!data.qualityLabel;
this.color_info = data.colorInfo ? {
primaries: data.colorInfo.primaries?.replace('COLOR_PRIMARIES_', ''),
transfer_characteristics: data.colorInfo.transferCharacteristics?.replace('COLOR_TRANSFER_CHARACTERISTICS_', ''),
matrix_coefficients: data.colorInfo.matrixCoefficients?.replace('COLOR_MATRIX_COEFFICIENTS_', '')
} : undefined;
if (this.has_audio) {
const args = new URLSearchParams(this.cipher || this.signature_cipher);
const url_components = new URLSearchParams(args.get('url') || this.url);
this.language = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('lang='))?.split('=').at(1) || null;
this.is_dubbed = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'dubbed';
this.is_descriptive = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'descriptive';
this.is_original = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'original' || !this.is_dubbed;
const xtags = url_components.get('xtags')?.split(':');
const audio_content = xtags?.find((x) => x.startsWith('acont='))?.split('=')[1];
this.language = xtags?.find((x: string) => x.startsWith('lang='))?.split('=')[1] || null;
this.is_dubbed = audio_content === 'dubbed';
this.is_descriptive = audio_content === 'descriptive';
this.is_original = audio_content === 'original' || !this.is_dubbed || !this.is_descriptive;
if (Reflect.has(data, 'audioTrack')) {
this.audio_track = {

View File

@@ -58,6 +58,7 @@ export { default as CommentsEntryPointHeader } from './classes/comments/Comments
export { default as CommentsEntryPointTeaser } from './classes/comments/CommentsEntryPointTeaser.ts';
export { default as CommentsHeader } from './classes/comments/CommentsHeader.ts';
export { default as CommentSimplebox } from './classes/comments/CommentSimplebox.ts';
export { default as CommentsSimplebox } from './classes/comments/CommentsSimplebox.ts';
export { default as CommentThread } from './classes/comments/CommentThread.ts';
export { default as CreatorHeart } from './classes/comments/CreatorHeart.ts';
export { default as EmojiPicker } from './classes/comments/EmojiPicker.ts';
@@ -115,6 +116,7 @@ export { default as GuideEntry } from './classes/GuideEntry.ts';
export { default as GuideSection } from './classes/GuideSection.ts';
export { default as GuideSubscriptionsSection } from './classes/GuideSubscriptionsSection.ts';
export { default as HashtagHeader } from './classes/HashtagHeader.ts';
export { default as HashtagTile } from './classes/HashtagTile.ts';
export { default as Heatmap } from './classes/Heatmap.ts';
export { default as HeatMarker } from './classes/HeatMarker.ts';
export { default as HeroPlaylistThumbnail } from './classes/HeroPlaylistThumbnail.ts';
@@ -178,6 +180,8 @@ export { default as LiveChatItemList } from './classes/LiveChatItemList.ts';
export { default as LiveChatMessageInput } from './classes/LiveChatMessageInput.ts';
export { default as LiveChatParticipant } from './classes/LiveChatParticipant.ts';
export { default as LiveChatParticipantsList } from './classes/LiveChatParticipantsList.ts';
export { default as MacroMarkersInfoItem } from './classes/MacroMarkersInfoItem.ts';
export { default as MacroMarkersList } from './classes/MacroMarkersList.ts';
export { default as MacroMarkersListItem } from './classes/MacroMarkersListItem.ts';
export { default as Menu } from './classes/menus/Menu.ts';
export { default as MenuNavigationItem } from './classes/menus/MenuNavigationItem.ts';
@@ -265,6 +269,7 @@ export { default as ProfileColumn } from './classes/ProfileColumn.ts';
export { default as ProfileColumnStats } from './classes/ProfileColumnStats.ts';
export { default as ProfileColumnStatsEntry } from './classes/ProfileColumnStatsEntry.ts';
export { default as ProfileColumnUserInfo } from './classes/ProfileColumnUserInfo.ts';
export { default as Quiz } from './classes/Quiz.ts';
export { default as RecognitionShelf } from './classes/RecognitionShelf.ts';
export { default as ReelItem } from './classes/ReelItem.ts';
export { default as ReelShelf } from './classes/ReelShelf.ts';

View File

@@ -413,6 +413,8 @@ export default class Parser {
return observe(actions.map((action: any) => {
if (action.navigateAction)
return new NavigateAction(action.navigateAction);
if (action.showMiniplayerCommand)
return new ShowMiniplayerCommand(action.showMiniplayerCommand);
if (action.reloadContinuationItemsCommand)
return new ReloadContinuationItemsCommand(action.reloadContinuationItemsCommand);
if (action.appendContinuationItemsAction)
@@ -574,6 +576,19 @@ export class NavigateAction extends YTNode {
}
}
export class ShowMiniplayerCommand extends YTNode {
static readonly type = 'showMiniplayerCommand';
miniplayer_command: NavigationEndpoint;
show_premium_branding: boolean;
constructor(data: RawNode) {
super();
this.miniplayer_command = new NavigationEndpoint(data.miniplayerCommand);
this.show_premium_branding = data.showPremiumBranding;
}
}
export class AppendContinuationItemsAction extends YTNode {
static readonly type = 'appendContinuationItemsAction';

View File

@@ -163,6 +163,16 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
return new Channel(this.actions, tab.page, true);
}
async getReleases(): Promise<Channel> {
const tab = await this.getTabByURL('releases');
return new Channel(this.actions, tab.page, true);
}
async getPodcasts(): Promise<Channel> {
const tab = await this.getTabByURL('podcasts');
return new Channel(this.actions, tab.page, true);
}
async getPlaylists(): Promise<Channel> {
const tab = await this.getTabByURL('playlists');
return new Channel(this.actions, tab.page, true);
@@ -217,6 +227,14 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
return this.hasTabWithURL('streams');
}
get has_releases(): boolean {
return this.hasTabWithURL('releases');
}
get has_podcasts(): boolean {
return this.hasTabWithURL('podcasts');
}
get has_playlists(): boolean {
return this.hasTabWithURL('playlists');
}

View File

@@ -99,6 +99,28 @@ class VideoInfo extends MediaInfo {
this.captions = info.captions;
this.cards = info.cards;
if (this.streaming_data) {
const default_audio_track = this.streaming_data.adaptive_formats.find((format) => format.audio_track?.audio_is_default);
if (default_audio_track) {
// The combined formats only exist for the default language, even for videos with multiple audio tracks
// So we can copy the language from the default audio track to the combined formats
this.streaming_data.formats.forEach((format) => format.language = default_audio_track.language);
} else if (typeof this.captions?.default_audio_track_index !== 'undefined' && this.captions?.audio_tracks && this.captions.caption_tracks) {
// For videos with a single audio track and captions, we can use the captions to figure out the language of the audio and combined formats
const audioTrack = this.captions.audio_tracks[this.captions.default_audio_track_index];
const defaultCaptionTrackIndex = audioTrack.default_caption_track_index;
const index = audioTrack.caption_track_indices[defaultCaptionTrackIndex ? defaultCaptionTrackIndex : 0];
const language_code = this.captions.caption_tracks[index].language_code;
this.streaming_data.adaptive_formats.forEach((format) => {
if (format.has_audio) {
format.language = language_code;
}
});
this.streaming_data.formats.forEach((format) => format.language = language_code);
}
}
const two_col = next?.contents?.item().as(TwoColumnWatchNextResults);
const results = two_col?.results;

View File

@@ -146,7 +146,7 @@ class FormatUtils {
chunk_end += chunk_size;
resolve();
return;
} catch (e: any) {
reject(e);
}
@@ -373,10 +373,17 @@ class FormatUtils {
label: first_format.audio_track?.display_name as string
}, children);
const hoisted: string[] = [];
this.#hoistCodecsIfPossible(set, track_objects[j], hoisted);
this.#hoistNumberAttributeIfPossible(set, track_objects[j], 'audioSamplingRate', 'audio_sample_rate', hoisted);
this.#hoistAudioChannelsIfPossible(document, set, track_objects[j], hoisted);
period.appendChild(set);
for (const format of track_objects[j]) {
await this.#generateRepresentationAudio(document, set, format, url_transformer, cpn, player, actions);
await this.#generateRepresentationAudio(document, set, format, url_transformer, hoisted, cpn, player, actions);
}
}
} else {
@@ -387,43 +394,179 @@ class FormatUtils {
subsegmentAlignment: 'true'
});
const color_info = mime_objects[i][0].color_info;
if (typeof color_info !== 'undefined') {
// Section 5.5 Video source metadata signalling https://dashif.org/docs/IOP-Guidelines/DASH-IF-IOP-Part7-v5.0.0.pdf
// Section 8 Video code points https://www.itu.int/rec/T-REC-H.273-202107-I/en
// The player.js file was also helpful
if (color_info.primaries) {
let primaries = '';
switch (color_info.primaries) {
case 'BT709':
primaries = '1';
break;
case 'BT2020':
primaries = '9';
break;
}
if (primaries !== '') {
set.appendChild(this.#el(document, 'EssentialProperty', {
schemeIdUri: 'urn:mpeg:mpegB:cicp:ColourPrimaries',
value: primaries
}));
}
}
if (color_info.transfer_characteristics) {
let transfer_characteristics = '';
switch (color_info.transfer_characteristics) {
case 'BT709':
transfer_characteristics = '1';
break;
case 'BT2020_10':
transfer_characteristics = '14';
break;
case 'SMPTEST2084':
transfer_characteristics = '16';
break;
case 'ARIB_STD_B67':
transfer_characteristics = '18';
break;
}
if (transfer_characteristics !== '') {
set.appendChild(this.#el(document, 'EssentialProperty', {
schemeIdUri: 'urn:mpeg:mpegB:cicp:TransferCharacteristics',
value: transfer_characteristics
}));
}
}
if (color_info.matrix_coefficients) {
let matrix_coefficients = '';
// This list is incomplete, as the player.js doesn't currently have any code for matrix coefficients,
// So it doesn't have a list like with the other two, so this is just based on what we've seen in responses
switch (color_info.matrix_coefficients) {
case 'BT709':
matrix_coefficients = '1';
break;
case 'BT2020_NCL':
matrix_coefficients = '14';
break;
default: {
const format = mime_objects[i][0];
const url = new URL(format.url as string);
const anonymisedFormat = JSON.parse(JSON.stringify(format));
anonymisedFormat.url = 'REDACTED';
anonymisedFormat.signature_cipher = 'REDACTED';
anonymisedFormat.cipher = 'REDACTED';
console.warn(`YouTube.js toDash(): Unknown matrix coefficients "${color_info.matrix_coefficients}", the DASH manifest is still usuable without this.\n`
+ `Please report it at ${Platform.shim.info.bugs_url} so we can add support for it.\n`
+ `Innertube client: ${url.searchParams.get('c')}\nformat:`, anonymisedFormat);
}
}
if (matrix_coefficients !== '') {
set.appendChild(this.#el(document, 'EssentialProperty', {
schemeIdUri: 'urn:mpeg:mpegB:cicp:MatrixCoefficients',
value: matrix_coefficients
}));
}
}
}
const hoisted: string[] = [];
this.#hoistCodecsIfPossible(set, mime_objects[i], hoisted);
if (mime_objects[i][0].has_audio) {
this.#hoistNumberAttributeIfPossible(set, mime_objects[i], 'audioSamplingRate', 'audio_sample_rate', hoisted);
this.#hoistAudioChannelsIfPossible(document, set, mime_objects[i], hoisted);
const language = mime_objects[i][0].language;
if (language) {
set.setAttribute('lang', language);
}
} else {
set.setAttribute('maxPlayoutRate', '1');
this.#hoistNumberAttributeIfPossible(set, mime_objects[i], 'frameRate', 'fps', hoisted);
}
period.appendChild(set);
for (const format of mime_objects[i]) {
if (format.has_video) {
await this.#generateRepresentationVideo(document, set, format, url_transformer, cpn, player, actions);
await this.#generateRepresentationVideo(document, set, format, url_transformer, hoisted, cpn, player, actions);
} else {
await this.#generateRepresentationAudio(document, set, format, url_transformer, cpn, player, actions);
await this.#generateRepresentationAudio(document, set, format, url_transformer, hoisted, cpn, player, actions);
}
}
}
}
}
static async #generateRepresentationVideo(document: XMLDocument, set: Element, format: Format, url_transformer: URLTransformer, cpn?: string, player?: Player, actions?: Actions) {
const codecs = getStringBetweenStrings(format.mime_type, 'codecs="', '"');
static #hoistCodecsIfPossible(set: Element, formats: Format[], hoisted: string[]) {
if (formats.length > 1 && new Set(formats.map((format) => getStringBetweenStrings(format.mime_type, 'codecs="', '"'))).size === 1) {
set.setAttribute('codecs', getStringBetweenStrings(formats[0].mime_type, 'codecs="', '"') as string);
hoisted.push('codecs');
}
}
static #hoistNumberAttributeIfPossible(set: Element, formats: Format[], attribute: 'audioSamplingRate' | 'frameRate', property: 'audio_sample_rate' | 'fps', hoisted: string[]) {
if (formats.length > 1 && new Set(formats.map((format) => format.fps)).size === 1) {
set.setAttribute(attribute, formats[0][property]?.toString() as string);
hoisted.push(attribute);
}
}
static #hoistAudioChannelsIfPossible(document: XMLDocument, set: Element, formats: Format[], hoisted: string[]) {
if (formats.length > 1 && new Set(formats.map((format) => format.audio_channels?.toString() || '2')).size === 1) {
set.appendChild(
this.#el(document, 'AudioChannelConfiguration', {
schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
value: formats[0].audio_channels?.toString() || '2'
})
);
hoisted.push('AudioChannelConfiguration');
}
}
static async #generateRepresentationVideo(document: XMLDocument, set: Element, format: Format, url_transformer: URLTransformer, hoisted: string[], cpn?: string, player?: Player, actions?: Actions) {
const url = new URL(format.decipher(player));
url.searchParams.set('cpn', cpn || '');
const representation = this.#el(document, 'Representation', {
id: format.itag?.toString(),
codecs,
bandwidth: format.bitrate?.toString(),
width: format.width?.toString(),
height: format.height?.toString(),
maxPlayoutRate: '1',
frameRate: format.fps?.toString()
height: format.height?.toString()
});
if (!hoisted.includes('codecs')) {
const codecs = getStringBetweenStrings(format.mime_type, 'codecs="', '"');
representation.setAttribute('codecs', codecs as string);
}
if (!hoisted.includes('frameRate')) {
representation.setAttribute('frameRate', format.fps?.toString() as string);
}
set.appendChild(representation);
await this.#generateSegmentInformation(document, representation, format, url_transformer(url)?.toString(), actions);
}
static async #generateRepresentationAudio(document: XMLDocument, set: Element, format: Format, url_transformer: URLTransformer, cpn?: string, player?: Player, actions?: Actions) {
const codecs = getStringBetweenStrings(format.mime_type, 'codecs="', '"');
static async #generateRepresentationAudio(document: XMLDocument, set: Element, format: Format, url_transformer: URLTransformer, hoisted: string[], cpn?: string, player?: Player, actions?: Actions) {
const url = new URL(format.decipher(player));
url.searchParams.set('cpn', cpn || '');
@@ -436,15 +579,26 @@ class FormatUtils {
const representation = this.#el(document, 'Representation', {
id,
codecs,
bandwidth: format.bitrate?.toString(),
audioSamplingRate: format.audio_sample_rate?.toString()
}, [
this.#el(document, 'AudioChannelConfiguration', {
schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
value: format.audio_channels?.toString() || '2'
})
]);
bandwidth: format.bitrate?.toString()
});
if (!hoisted.includes('codecs')) {
const codecs = getStringBetweenStrings(format.mime_type, 'codecs="', '"');
representation.setAttribute('codecs', codecs as string);
}
if (!hoisted.includes('audioSamplingRate')) {
representation.setAttribute('audioSamplingRate', format.audio_sample_rate?.toString() as string);
}
if (!hoisted.includes('AudioChannelConfiguration')) {
representation.appendChild(
this.#el(document, 'AudioChannelConfiguration', {
schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
value: format.audio_channels?.toString() || '2'
})
);
}
set.appendChild(representation);