Skip to content

Commit d8d348e

Browse files
committed
Pause on Watch on YouTube
1 parent 1e57341 commit d8d348e

File tree

5 files changed

+112
-1
lines changed

5 files changed

+112
-1
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { SwitchContext, SwitchProvider } from '../providers/SwitchProvider.jsx';
1111
import { Tooltip } from './Tooltip.jsx';
1212
import { useSetFocusMode } from './FocusMode.jsx';
1313
import { useTypedTranslation } from '../types.js';
14+
import { usePlaybackControl } from './PlaybackControl';
1415

1516
/**
1617
* @param {object} props
@@ -99,6 +100,7 @@ export function InfoIcon({ debugStyles = false }) {
99100
*/
100101
function ControlBarDesktop({ embed }) {
101102
const settingsUrl = useSettingsUrl();
103+
const { pauseVideo } = usePlaybackControl();
102104
const openOnYoutube = useOpenOnYoutubeHandler();
103105
const { t } = useTypedTranslation();
104106
const { state } = useContext(SwitchContext);
@@ -120,7 +122,10 @@ function ControlBarDesktop({ embed }) {
120122
formfactor={'desktop'}
121123
buttonProps={{
122124
onClick: () => {
123-
if (embed) openOnYoutube(embed);
125+
if (embed) {
126+
pauseVideo();
127+
openOnYoutube(embed);
128+
}
124129
},
125130
}}
126131
>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const EVENT_PLAY = 'ddg-duckplayer-play';
2+
export const EVENT_PAUSE = 'ddg-duckplayer-pause';
3+
4+
export function usePlaybackControl() {
5+
return {
6+
playVideo: () => {
7+
window.dispatchEvent(new Event(EVENT_PLAY));
8+
},
9+
pauseVideo: () => {
10+
window.dispatchEvent(new Event(EVENT_PAUSE));
11+
},
12+
};
13+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ function useIframeEffects(src) {
102102
features.clickCapture(),
103103
features.titleCapture(),
104104
features.mouseCapture(),
105+
features.playbackEvents(),
105106
];
106107

107108
/**

special-pages/pages/duckplayer/app/features/iframe.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AutoFocus } from './autofocus.js';
33
import { ClickCapture } from './click-capture.js';
44
import { TitleCapture } from './title-capture.js';
55
import { MouseCapture } from './mouse-capture.js';
6+
import { PlaybackEvents } from './playback-events.js';
67

78
/**
89
* Represents an individual piece of functionality in the iframe.
@@ -74,5 +75,11 @@ export function createIframeFeatures(settings) {
7475
mouseCapture: () => {
7576
return new MouseCapture();
7677
},
78+
/**
79+
* @return {IframeFeature}
80+
*/
81+
playbackEvents: () => {
82+
return new PlaybackEvents();
83+
},
7784
};
7885
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { EVENT_PLAY, EVENT_PAUSE } from '../components/PlaybackControl.jsx';
2+
3+
/**
4+
* @typedef {import("./iframe").IframeFeature} IframeFeature
5+
*/
6+
7+
/**
8+
* Exposes video element in iframe.
9+
*
10+
* @implements IframeFeature
11+
*/
12+
export class PlaybackEvents {
13+
/** @type {MutationObserver} */
14+
observer;
15+
16+
/** @type {[EVENT_PLAY|EVENT_PAUSE, (() => void)][]} */
17+
handlers = [];
18+
19+
destroy() {
20+
if (this.observer) this.observer.disconnect();
21+
22+
this.handlers.forEach(([event, handler]) => {
23+
window.removeEventListener(event, handler);
24+
});
25+
}
26+
27+
/**
28+
* @param {HTMLIFrameElement} iframe
29+
*/
30+
iframeDidLoad(iframe) {
31+
const documentBody = iframe.contentWindow?.document?.body;
32+
33+
if (documentBody) {
34+
const videoElement = documentBody.querySelector('video');
35+
36+
// Check if iframe already contains video
37+
if (videoElement) {
38+
this.addHandlersToVideo(videoElement);
39+
} else {
40+
// No video found. Observe iframe's document for changes
41+
this.observer = new MutationObserver(this.handleMutation.bind(this));
42+
43+
this.observer.observe(documentBody, {
44+
childList: true,
45+
subtree: true, // Observe all descendants of the body
46+
});
47+
}
48+
}
49+
50+
return () => {
51+
this.destroy();
52+
};
53+
}
54+
55+
/**
56+
*
57+
* @param {HTMLVideoElement} videoElement
58+
*/
59+
addHandlersToVideo(videoElement) {
60+
const playHandler = () => videoElement.play();
61+
window.addEventListener(EVENT_PLAY, playHandler);
62+
this.handlers.push([EVENT_PLAY, playHandler]);
63+
64+
const pauseHandler = () => videoElement.pause();
65+
window.addEventListener(EVENT_PAUSE, pauseHandler);
66+
this.handlers.push([EVENT_PAUSE, pauseHandler]);
67+
}
68+
69+
/**
70+
* Mutation handler that checks for a new video element
71+
*
72+
* @type {MutationCallback}
73+
*/
74+
handleMutation(mutationsList) {
75+
for (const mutation of mutationsList) {
76+
if (mutation.type === 'childList') {
77+
mutation.addedNodes.forEach((node) => {
78+
if (node.nodeType === Node.ELEMENT_NODE && /** @type {HTMLElement} */ (node).tagName === 'VIDEO') {
79+
this.addHandlersToVideo(/** @type {HTMLVideoElement} */ (node));
80+
}
81+
});
82+
}
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)