From 1eaa3136d8e101f4aaa0a65e9c3a1c1b301f0729 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Tue, 18 Aug 2026 15:38:07 +0200 Subject: [PATCH 1/2] use visionOS stream client --- src/youtube/models/internal.ts | 1 + src/youtube/service.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/youtube/models/internal.ts b/src/youtube/models/internal.ts index 320922b..27b4101 100644 --- a/src/youtube/models/internal.ts +++ b/src/youtube/models/internal.ts @@ -5,6 +5,7 @@ export type AvailableInnertubeClient = | 'MWEB' | 'ANDROID' | 'ANDROID_VR' + | 'VISIONOS' | 'YTMUSIC' | 'YTMUSIC_ANDROID' | 'YTSTUDIO_ANDROID' diff --git a/src/youtube/service.ts b/src/youtube/service.ts index b81c5d0..7ed0404 100644 --- a/src/youtube/service.ts +++ b/src/youtube/service.ts @@ -247,7 +247,7 @@ export class YouTubeService { // - InnerTube Methods - private async getStreams(innertube: Innertube): Promise { - const clients: AvailableInnertubeClient[] = ['ANDROID_VR', 'WEB']; + const clients: AvailableInnertubeClient[] = ['VISIONOS', 'WEB']; const fallbackClient: AvailableInnertubeClient = 'WEB_EMBEDDED'; let allStreams: RemoteStream[] = []; From 95ee041dcb7240245bab77c70980df1cc4117754 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Tue, 18 Aug 2026 16:19:40 +0200 Subject: [PATCH 2/2] fix embedded stream fallback --- package-lock.json | 6 +++--- package.json | 2 +- src/youtube/service.ts | 37 ++++++++++++++++++++++++++++++++----- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 502df30..001c476 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "quickjs-emscripten": "^0.31.0", - "youtubei.js": "github:alexeichhorn/YouTube.js#f4a66ec3e26dafcafdb81f4e4a0091c67238b605" + "youtubei.js": "github:alexeichhorn/YouTube.js#d3adc7d03259f899cc1c767ad2a09b23d6185573" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.16.11", @@ -2825,8 +2825,8 @@ }, "node_modules/youtubei.js": { "version": "18.0.0", - "resolved": "git+ssh://git@github.com/alexeichhorn/YouTube.js.git#f4a66ec3e26dafcafdb81f4e4a0091c67238b605", - "integrity": "sha512-6Tak6+j+Gew2KyvIU0ePsBo+fQF7BwJzE2siU0tLQRQoY/7EindDzbffFx3sgZS3PCx08GbNv2sHruJ8gDvxhg==", + "resolved": "git+ssh://git@github.com/alexeichhorn/YouTube.js.git#d3adc7d03259f899cc1c767ad2a09b23d6185573", + "integrity": "sha512-K8MPsxTTk2sH0lSc9cFH/fd0N8m7Q5CM6yylQyl6n8oMZeBLb1PSzUzhK4S1O35S6Ji2vpSYbPWkjyiWChuZig==", "funding": [ "https://github.com/sponsors/LuanRT" ], diff --git a/package.json b/package.json index f320bed..2f5fc7b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,6 @@ }, "dependencies": { "quickjs-emscripten": "^0.31.0", - "youtubei.js": "github:alexeichhorn/YouTube.js#f4a66ec3e26dafcafdb81f4e4a0091c67238b605" + "youtubei.js": "github:alexeichhorn/YouTube.js#d3adc7d03259f899cc1c767ad2a09b23d6185573" } } diff --git a/src/youtube/service.ts b/src/youtube/service.ts index 7ed0404..411ce31 100644 --- a/src/youtube/service.ts +++ b/src/youtube/service.ts @@ -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) { @@ -246,7 +246,7 @@ export class YouTubeService { // - InnerTube Methods - - private async getStreams(innertube: Innertube): Promise { + private async getStreams(innertube: Innertube, fetcher: typeof fetch): Promise { const clients: AvailableInnertubeClient[] = ['VISIONOS', 'WEB']; const fallbackClient: AvailableInnertubeClient = 'WEB_EMBEDDED'; @@ -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); } @@ -286,8 +287,34 @@ export class YouTubeService { return Array.from(streamsByItag.values()); } - private async getStreamsForClient(innertube: Innertube, client: AvailableInnertubeClient): Promise { - 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 { + 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 { + 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 ?? [])];