Skip to content

Commit d80a29d

Browse files
committed
Showing error screen
1 parent 6223c5f commit d80a29d

File tree

7 files changed

+97
-16
lines changed

7 files changed

+97
-16
lines changed

special-pages/pages/duckplayer/app/components/DesktopApp.jsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,14 @@ export function DesktopApp({ embed }) {
3434
function DesktopLayout({ embed }) {
3535
const error = useYouTubeError();
3636

37-
if (error) {
38-
return <PlayerError layout={'desktop'} kind={'invalid-id'} />;
39-
}
37+
// TODO: Better conditionals for showing error or player
4038

4139
return (
4240
<div class={styles.desktop}>
4341
<PlayerContainer>
4442
{embed === null && <PlayerError layout={'desktop'} kind={'invalid-id'} />}
45-
{embed !== null && <Player src={embed.toEmbedUrl()} layout={'desktop'} />}
43+
{embed !== null && !error && <Player src={embed.toEmbedUrl()} layout={'desktop'} />}
44+
{embed !== null && error && <PlayerError layout={'desktop'} kind={error} />}
4645
<HideInFocusMode style={'slide'}>
4746
<InfoBarContainer>
4847
<InfoBar embed={embed} />

special-pages/pages/duckplayer/app/components/MobileApp.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { MobileButtons } from './MobileButtons.jsx';
1212
import { OrientationProvider } from '../providers/OrientationProvider.jsx';
1313
import { FocusMode } from './FocusMode.jsx';
1414
import { useTelemetry } from '../types.js';
15+
import { useYouTubeError } from '../providers/YouTubeErrorProvider';
1516

1617
const DISABLED_HEIGHT = 450;
1718

@@ -53,12 +54,17 @@ export function MobileApp({ embed }) {
5354
*/
5455
function MobileLayout({ embed }) {
5556
const platformName = usePlatformName();
57+
const error = useYouTubeError();
58+
59+
// TODO: Better conditionals for showing error or player
60+
5661
return (
5762
<main class={styles.main}>
5863
<div class={cn(styles.filler, styles.hideInFocus)} />
5964
<div class={styles.embed}>
6065
{embed === null && <PlayerError layout={'mobile'} kind={'invalid-id'} />}
61-
{embed !== null && <Player src={embed.toEmbedUrl()} layout={'mobile'} />}
66+
{embed !== null && !error && <Player src={embed.toEmbedUrl()} layout={'mobile'} />}
67+
{embed !== null && error && <PlayerError layout={'mobile'} kind={error} />}
6268
</div>
6369
<div class={cn(styles.logo, styles.hideInFocus)}>
6470
<MobileWordmark />

special-pages/pages/duckplayer/app/components/Player.jsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { h } from 'preact';
22
import cn from 'classnames';
33
import styles from './Player.module.css';
4-
import { useEffect, useRef, useState } from 'preact/hooks';
4+
import { useEffect, useRef } from 'preact/hooks';
55
import { useSettings } from '../providers/SettingsProvider.jsx';
66
import { createIframeFeatures } from '../features/iframe.js';
77
import { Settings } from '../settings';
88
import { useTypedTranslation } from '../types.js';
99

10+
export const PLAYER_ERRORS = {
11+
invalidId: 'invalid-id',
12+
botDetected: 'bot-detected',
13+
}
14+
1015
/**
11-
* @typedef {'invalid-id'|'bot-detected'} PlayerError
16+
* @typedef {typeof PLAYER_ERRORS[keyof typeof PLAYER_ERRORS]} PlayerError
1217
*/
1318

1419
/**
@@ -20,7 +25,6 @@ import { useTypedTranslation } from '../types.js';
2025
*/
2126
export function Player({ src, layout }) {
2227
const { ref, didLoad } = useIframeEffects(src);
23-
2428
const wrapperClasses = cn({
2529
[styles.root]: true,
2630
[styles.player]: true,
@@ -60,6 +64,7 @@ export function PlayerError({ kind, layout }) {
6064
['bot-detected']: <span dangerouslySetInnerHTML={{ __html: t('botDetectedError') }} />,
6165
};
6266
const text = errors[kind] || errors['invalid-id'];
67+
6368
return (
6469
<div
6570
class={cn(styles.root, {
@@ -108,6 +113,7 @@ function useIframeEffects(src) {
108113
features.clickCapture(),
109114
features.titleCapture(),
110115
features.mouseCapture(),
116+
features.errorDetection(),
111117
];
112118

113119
/**

special-pages/pages/duckplayer/app/features/error-detection.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ export class ErrorDetection {
1717
iframeDidLoad(iframe) {
1818
const documentBody = iframe.contentWindow?.document?.body;
1919
if (documentBody) {
20+
// Check if iframe already contains error
21+
const error = nodeContainsError(documentBody);
22+
if (error) {
23+
window.dispatchEvent(new CustomEvent(IFRAME_ERROR_EVENT, { detail: { error } }));
24+
return null;
25+
}
26+
2027
// Create a MutationObserver instance
2128
const observer = new MutationObserver(handleMutation);
2229

@@ -40,29 +47,48 @@ const handleMutation = (mutationsList) => {
4047
if (mutation.type === 'childList') {
4148
mutation.addedNodes.forEach((node) => {
4249
// Check if the added node is a div with the class ytp-error
43-
const error = errorForNode(node);
50+
const error = nodeIsError(node);
4451
if (error) {
4552
console.log('A node with an error has been added to the document:', node);
4653

47-
window.dispatchEvent(new CustomEvent(IFRAME_ERROR_EVENT, { detail: error }));
54+
window.dispatchEvent(new CustomEvent(IFRAME_ERROR_EVENT, { detail: { error } }));
4855
}
4956
});
5057
}
5158
}
5259
};
5360

61+
/**
62+
* Analyses children of a node to determine if it contains an error state
63+
*
64+
* @param {Node} [node]
65+
* @returns {PlayerError|null}
66+
*/
67+
const nodeContainsError = (node) => {
68+
if (node?.nodeType === Node.ELEMENT_NODE) {
69+
const element = /** @type {HTMLElement} */ (node);
70+
const errorElement = element.querySelector('ytp-error');
71+
72+
if (errorElement) {
73+
return 'bot-detected'; // TODO: More generic naming
74+
}
75+
}
76+
77+
return null;
78+
}
79+
5480
/**
5581
* Analyses attributes of a node to determine if it contains an error state
5682
*
5783
* @param {Node} [node]
5884
* @returns {PlayerError|null}
5985
*/
60-
const errorForNode = (node) => {
86+
const nodeIsError = (node) => {
6187
// if (node.nodeType === Node.ELEMENT_NODE && /** @type {HTMLElement} */(node).classList.contains('ytp-error')) {
6288
if (node?.nodeType === Node.ELEMENT_NODE) {
6389
const element = /** @type {HTMLElement} */ (node);
6490
if (element.classList.contains('ytp-error')) {
65-
return 'bot-detected';
91+
return 'bot-detected'; // TODO: More generic naming
6692
}
6793
// Add other error detection logic here
6894
}

special-pages/pages/duckplayer/app/providers/YouTubeErrorProvider.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useContext, useState } from 'preact/hooks';
22
import { h, createContext } from 'preact';
33
import { useEffect } from 'preact/hooks';
4+
import { PLAYER_ERRORS } from '../components/Player';
45

56
export const IFRAME_ERROR_EVENT = 'iframe-error';
67

@@ -26,8 +27,9 @@ export function YouTubeErrorProvider({ initial = null, children }) {
2627
useEffect(() => {
2728
/** @type {(event: CustomEvent) => void} */
2829
const errorEventHandler = (event) => {
29-
if (event.detail) {
30-
setError(event.detail.error || null);
30+
const error = event.detail?.error
31+
if (Object.values(PLAYER_ERRORS).includes(error) || error === null) {
32+
setError(error);
3133
}
3234
};
3335

special-pages/pages/duckplayer/integration-tests/duck-player.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@ const html = {
2222
</div>
2323
</div>
2424
</body>
25+
</html>`,
26+
signInRequired: `<html><head><title>${MOCK_VIDEO_TITLE}</title></head>
27+
<body>
28+
<div class="ytp-error" role="alert" data-layer="4">
29+
<div class="ytp-error-content" style="padding-top: 165px">
30+
<div class="ytp-error-content-wrap">
31+
<div class="ytp-error-content-wrap-reason"><span>Sign in to confirm you’re not a bot</span></div>
32+
<div class="ytp-error-content-wrap-subreason">
33+
<span
34+
><span>This helps protect our community. </span
35+
><a
36+
href="https://support.google.com/youtube/answer/3037019#zippy=%2Ccheck-that-youre-signed-into-youtube"
37+
>Learn more</a
38+
></span
39+
>
40+
</div>
41+
</div>
42+
</div>
43+
</div>
44+
</body>
2545
</html>`,
2646
};
2747

@@ -156,6 +176,14 @@ export class DuckPlayerPage {
156176
});
157177
}
158178

179+
if (urlParams.get('videoID') === 'SIGN_IN_REQUIRED') {
180+
return request.fulfill({
181+
status: 200,
182+
body: html.signInRequired,
183+
contentType: 'text/html',
184+
});
185+
}
186+
159187
const mp4VideoPlaceholderAsDataURI =
160188
'data:video/mp4;base64,AAAAHGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAwFtZGF0AAACogYF//+b3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE1MiByMjg1NCBlMjA5YTFjIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxNyAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzowMTMzIHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTEgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz02MyBsb29rYWhlYWRfdGhyZWFkcz0yIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTMgYl9weXJhbWlkPTIgYl9hZGFwdD1xLTIgYl9iaWFzPTAgZGlyZWN0PTEgd2VpZ2h0Yj0xIG9wZW5fZ29wPTAgd2VpZ2h0cD0yIGtleWludD0yNTAga2V5aW50X21pbj0yNSBzY2VuZWN1dD00MCBpbnRyYV9yZWZyZXNoPTAgcmM9bG9va2FoZWFkIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCB2YnY9MCBjbG9zZWRfZ29wPTAgY3V0X3Rocm91Z2g9MCAnbm8tZGlndHMuanBnLTFgcC1mbHWinS3SlB8AP0AAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABSAAAAAAAAAAAAAAAAAAABBZHJ0AAAAAAAAAA==';
161189
return request.fulfill({
@@ -306,8 +334,12 @@ export class DuckPlayerPage {
306334
await expect(this.page.locator('iframe')).toHaveAttribute('src', expected);
307335
}
308336

309-
async hasShownErrorMessage() {
310-
await expect(this.page.getByText('ERROR: Invalid video id')).toBeVisible();
337+
/**
338+
*
339+
* @param {string} text
340+
*/
341+
async hasShownErrorMessage(text = 'ERROR: Invalid video id') {
342+
await expect(this.page.getByText(text)).toBeVisible();
311343
}
312344

313345
async hasNotAddedIframe() {

special-pages/pages/duckplayer/integration-tests/duckplayer.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ test.describe('duckplayer iframe', () => {
3939
await duckplayer.openWithVideoID('UNSUPPORTED');
4040
await duckplayer.opensInYoutubeFromError({ videoID: 'UNSUPPORTED' });
4141
});
42+
test('supports "watch on youtube" for videos that require sign-in', async ({ page }, workerInfo) => {
43+
const duckplayer = DuckPlayerPage.create(page, workerInfo);
44+
await duckplayer.openWithVideoID('SIGN_IN_REQUIRED');
45+
await duckplayer.opensInYoutubeFromError({ videoID: 'SIGN_IN_REQUIRED' });
46+
});
47+
test('shows error screen for videos that require sign-in', async ({ page }, workerInfo) => {
48+
const duckplayer = DuckPlayerPage.create(page, workerInfo);
49+
await duckplayer.openWithVideoID('SIGN_IN_REQUIRED');
50+
await duckplayer.hasShownErrorMessage('ERROR: Bot detected');
51+
});
4252
test('clears storage', async ({ page }, workerInfo) => {
4353
const duckplayer = DuckPlayerPage.create(page, workerInfo);
4454
await duckplayer.openWithVideoID();

0 commit comments

Comments
 (0)