mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 09:32:12 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23c22a93c4 | ||
|
|
1ca20836bf | ||
|
|
5f058e69ae | ||
|
|
3500e92632 | ||
|
|
3f57c2fa5c | ||
|
|
7528ebdb60 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [5.3.0](https://github.com/LuanRT/YouTube.js/compare/v5.2.1...v5.3.0) (2023-07-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **toDash:** Add color information ([#430](https://github.com/LuanRT/YouTube.js/issues/430)) ([3500e92](https://github.com/LuanRT/YouTube.js/commit/3500e926327d560b1db036bfe503c276b91922ac))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **Format:** Cleanup the xtags parsing ([#434](https://github.com/LuanRT/YouTube.js/issues/434)) ([1ca2083](https://github.com/LuanRT/YouTube.js/commit/1ca20836bf343c78461fab7ad3b71db2b96e65c3))
|
||||
* **toDash:** Hoist duplicates from Representation to AdaptationSet ([#431](https://github.com/LuanRT/YouTube.js/issues/431)) ([5f058e6](https://github.com/LuanRT/YouTube.js/commit/5f058e69ae8594491133f7f96287bea4137f7822))
|
||||
|
||||
## [5.2.1](https://github.com/LuanRT/YouTube.js/compare/v5.2.0...v5.2.1) (2023-07-04)
|
||||
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "5.2.1",
|
||||
"version": "5.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "5.2.1",
|
||||
"version": "5.3.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "5.2.1",
|
||||
"version": "5.3.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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,174 @@ 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);
|
||||
} 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 +574,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);
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ describe('YouTube.js Tests', () => {
|
||||
});
|
||||
|
||||
test('Innertube#music.getAlbum', async () => {
|
||||
const album = await innertube.music.getAlbum('MPREb_aWzgoemOnfa');
|
||||
const album = await innertube.music.getAlbum('MPREb_YpQ7SWMPLvu');
|
||||
expect(album).toBeDefined();
|
||||
expect(album.header).toBeDefined();
|
||||
expect(album.sections).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user