-
Notifications
You must be signed in to change notification settings - Fork 89
Fix YouTube live Picture-in-Picture Error 153 #1758
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { expect, test, type Request } from '@playwright/test'; | ||
| import { dismissCookieConsentIfVisible } from '@tests/e2e/helpers'; | ||
|
|
||
| // Native Document PiP needs the full browser rather than headless shell. | ||
| test.use({ channel: 'chromium' }); | ||
|
|
||
| for (const videoId of ['DtdP0JBawPI', null]) { | ||
| test(`native YouTube PiP sends a referrer for the ${videoId ? 'video' : 'channel fallback'}`, async ({ | ||
| page, | ||
| context, | ||
| isMobile, | ||
| }) => { | ||
| test.skip(isMobile, 'Native Document Picture-in-Picture requires a desktop browser.'); | ||
|
|
||
| const snapshot = { | ||
| isLive: true, | ||
| primarySource: 'twitch', | ||
| twitch: { live: true, channel: 'aspiredotdev' }, | ||
| youtube: { live: true, videoId }, | ||
| updatedAt: new Date().toISOString(), | ||
| }; | ||
| await page.route(/\/api\/live\/?$/, (route) => route.fulfill({ json: snapshot })); | ||
| await page.route(/\/api\/live\/stream\/?$/, (route) => | ||
| route.fulfill({ | ||
| contentType: 'text/event-stream', | ||
| body: `event: state\ndata: ${JSON.stringify(snapshot)}\n\n`, | ||
| }) | ||
| ); | ||
|
|
||
| // Stub only the provider response, not PiP: Chromium must generate the real request. | ||
| const embeds: Request[] = []; | ||
| await context.route( | ||
| /^https:\/\/(?:www\.youtube-nocookie\.com\/embed\/|player\.twitch\.tv\/)/, | ||
| async (route) => { | ||
| embeds.push(route.request()); | ||
| await route.fulfill({ | ||
| contentType: 'text/html', | ||
| body: '<!doctype html><title>Provider player</title><p>Provider player</p>', | ||
| }); | ||
| } | ||
| ); | ||
|
|
||
| await page.goto('/'); | ||
| await dismissCookieConsentIfVisible(page); | ||
| expect(await page.evaluate(() => 'documentPictureInPicture' in window)).toBe(true); | ||
| const expectedReferrer = new URL(page.url()).origin + '/'; | ||
| const liveButton = () => page.locator('.live-btn:visible').first(); | ||
| const menu = page.getByRole('dialog', { name: 'Choose how to watch' }); | ||
| const youtube = menu.getByRole('button', { name: /Open YouTube Picture-in-Picture/ }); | ||
| const twitch = menu.getByRole('button', { name: /Open Twitch Picture-in-Picture/ }); | ||
|
|
||
| await expect(liveButton()).toHaveAttribute('data-source', 'both'); | ||
| await liveButton().click(); | ||
| const opened = context.waitForEvent('page'); | ||
| await youtube.click(); | ||
| const pip = await opened; | ||
|
|
||
| await expect.poll(() => embeds.length).toBe(1); | ||
| expect(await embeds[0].headerValue('referer')).toBe(expectedReferrer); | ||
| const embedUrl = new URL(embeds[0].url()); | ||
| expect(embedUrl.origin).toBe('https://www.youtube-nocookie.com'); | ||
| expect(embedUrl.pathname).toBe(`/embed/${videoId ?? 'live_stream'}`); | ||
| if (!videoId) { | ||
| expect(embedUrl.searchParams.get('channel')).toBe('UCW_UJkc7RhM_NPcDXnOCfrQ'); | ||
| } | ||
| expect(embedUrl.searchParams.get('autoplay')).toBe('1'); | ||
| expect(embedUrl.searchParams.get('mute')).toBe('1'); | ||
| await expect(liveButton()).toHaveAttribute('data-pip-open', 'true'); | ||
|
|
||
| await page.locator('header a[href="/docs/"]:visible').click(); | ||
| await expect(page).toHaveURL(/\/docs\/$/); | ||
| await expect(liveButton()).toHaveAttribute('data-pip-open', 'true'); | ||
| expect(pip.isClosed()).toBe(false); | ||
| await liveButton().click(); | ||
| await youtube.click(); | ||
| expect(embeds).toHaveLength(1); | ||
|
|
||
| await liveButton().click(); | ||
| await twitch.click(); | ||
| await expect.poll(() => embeds.length).toBe(2); | ||
| const twitchUrl = new URL(embeds[1].url()); | ||
| expect(twitchUrl.origin).toBe('https://player.twitch.tv'); | ||
| expect(twitchUrl.searchParams.get('parent')).toBe(new URL(page.url()).hostname); | ||
|
|
||
| await liveButton().click(); | ||
| await youtube.click(); | ||
| await expect.poll(() => embeds.length).toBe(3); | ||
| expect(embeds[2].url()).toBe(embeds[0].url()); | ||
| expect(await embeds[2].headerValue('referer')).toBe(expectedReferrer); | ||
|
|
||
| await pip.close(); | ||
| await expect(liveButton()).toHaveAttribute('data-pip-open', 'false'); | ||
| await expect(liveButton()).toHaveAttribute('data-live', 'true'); | ||
| await expect(page).toHaveURL(/\/docs\/$/); | ||
|
|
||
| await liveButton().click(); | ||
| const reopened = context.waitForEvent('page'); | ||
| await youtube.click(); | ||
| const nextPip = await reopened; | ||
| await expect.poll(() => embeds.length).toBe(4); | ||
| expect(await embeds[3].headerValue('referer')).toBe(expectedReferrer); | ||
| await nextPip.close(); | ||
| await expect(liveButton()).toHaveAttribute('data-pip-open', 'false'); | ||
| }); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a hack. Is this the real way to handle the problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair question. This is a standards-defined workaround for the missing referrer, not a YouTube- or Chrome-documented PiP recipe. I should distinguish those rather than imply this is the official PiP solution.
The relevant behavior is explicit in the HTML specification's document open steps:
entryDocumentfrom the calling environment and requires the target document to be same-origin.document.close()closes that input stream; we subsequently create the iframe through the DOM API, withoutdocument.write()or HTML string injection.That URL change is the part we need: PiP initially contains an
about:blankdocument, and the direct YouTube embed request had no HTTPReferer. Callingopen()from the opener gives the PiP document the opener's URL, allowing Chromium to generate the header normally. The explicitstrict-origin-when-cross-originpolicy sends only our origin to YouTube, not the documentation path or query. This satisfies YouTube's embedded-player identification requirement.The destructive effects are why these calls run only on the newly created, empty PiP document, before registering the
pagehidelistener or adding the iframe—never during provider switches or reuse of an existing PiP window.I compared alternatives in a real native PiP window against the livestream. Referrer policy alone, adding
origin, asrcdocwrapper, and a blob wrapper all still produced Error 153. A hosted same-origin wrapper also worked, but adds a page/request and nested iframe. The open/close initialization restoredReferer: https://aspire.dev/and actual playback without that extra structure.The new tests use a real native PiP window and inspect the browser-generated request, stubbing only the provider response. Both video-ID and channel-fallback cases failed with a null referrer before the fix and pass after it; they also cover provider switching, client navigation, and close/reopen cleanup. On that evidence, I prefer this smaller, standards-defined workaround to the hosted wrapper.
Auto-replied by the GitHub Copilot app