Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
},
"dependencies": {
"quickjs-emscripten": "^0.31.0",
"youtubei.js": "github:alexeichhorn/YouTube.js#f4a66ec3e26dafcafdb81f4e4a0091c67238b605"
"youtubei.js": "github:alexeichhorn/YouTube.js#d3adc7d03259f899cc1c767ad2a09b23d6185573"
}
}
1 change: 1 addition & 0 deletions src/youtube/models/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type AvailableInnertubeClient =
| 'MWEB'
| 'ANDROID'
| 'ANDROID_VR'
| 'VISIONOS'
| 'YTMUSIC'
| 'YTMUSIC_ANDROID'
| 'YTSTUDIO_ANDROID'
Expand Down
39 changes: 33 additions & 6 deletions src/youtube/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class YouTubeService {
fetch: wsFetch,
...(YouTubeService.PLAYER_ID_OVERRIDE ? { player_id: YouTubeService.PLAYER_ID_OVERRIDE } : {}),
});
const streams = await this.getStreams(innertube);
const streams = await this.getStreams(innertube, wsFetch);

this.send({ type: ServerMessageType.result, content: streams });
} catch (error: any) {
Expand Down Expand Up @@ -246,8 +246,8 @@ export class YouTubeService {

// - InnerTube Methods -

private async getStreams(innertube: Innertube): Promise<RemoteStream[]> {
const clients: AvailableInnertubeClient[] = ['ANDROID_VR', 'WEB'];
private async getStreams(innertube: Innertube, fetcher: typeof fetch): Promise<RemoteStream[]> {
const clients: AvailableInnertubeClient[] = ['VISIONOS', 'WEB'];
const fallbackClient: AvailableInnertubeClient = 'WEB_EMBEDDED';

let allStreams: RemoteStream[] = [];
Expand All @@ -265,7 +265,8 @@ export class YouTubeService {

if (allStreams.length === 0) {
try {
return this.deduplicateStreams(await this.getStreamsForClient(innertube, fallbackClient));
const encryptedHostFlags = await this.getEncryptedHostFlags(fetcher);
return this.deduplicateStreams(await this.getStreamsForClient(innertube, fallbackClient, encryptedHostFlags));
} catch (error) {
console.error(`Failed to get streams for fallback client ${fallbackClient}:`, error);
}
Expand All @@ -286,8 +287,34 @@ export class YouTubeService {
return Array.from(streamsByItag.values());
}

private async getStreamsForClient(innertube: Innertube, client: AvailableInnertubeClient): Promise<RemoteStream[]> {
const info = await innertube.getInfo(this.videoID, { client });
private extractEncryptedHostFlags(html: string): string {
const encryptedHostFlags = html.match(/"encryptedHostFlags":"([^"]+)"/)?.[1];
if (!encryptedHostFlags) {
throw new Error('Embedded player config is missing encrypted host flags');
}
return encryptedHostFlags;
}

private async getEncryptedHostFlags(fetcher: typeof fetch): Promise<string> {
const response = await fetcher(`https://www.youtube.com/embed/${this.videoID}`, {
headers: {
'Accept-Language': 'en-US,en',
'Referer': 'https://www.reddit.com/',
'User-Agent': 'Mozilla/5.0',
},
});
if (!response.ok) {
throw new Error(`Failed to load embedded player config: ${response.status}`);
}
return this.extractEncryptedHostFlags(await response.text());
}

private async getStreamsForClient(
innertube: Innertube,
client: AvailableInnertubeClient,
encryptedHostFlags?: string
): Promise<RemoteStream[]> {
const info = await innertube.getInfo(this.videoID, { client, encrypted_host_flags: encryptedHostFlags });
const f = info.streaming_data || { formats: [], adaptive_formats: [] };
const formats = [...(f.formats ?? []), ...(f.adaptive_formats ?? [])];

Expand Down
Loading