Skip to content

Commit 4f8c118

Browse files
committed
Duck Player Native feature
1 parent 4ac5fd9 commit 4f8c118

File tree

10 files changed

+248
-85
lines changed

10 files changed

+248
-85
lines changed

injected/src/features.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const otherFeatures = /** @type {const} */ ([
1919
'cookie',
2020
'messageBridge',
2121
'duckPlayer',
22+
'duckPlayerNative',
2223
'harmfulApis',
2324
'webCompat',
2425
'windowsPermissionUsage',
@@ -32,8 +33,16 @@ const otherFeatures = /** @type {const} */ ([
3233
/** @typedef {baseFeatures[number]|otherFeatures[number]} FeatureName */
3334
/** @type {Record<string, FeatureName[]>} */
3435
export const platformSupport = {
35-
apple: ['webCompat', ...baseFeatures],
36-
'apple-isolated': ['duckPlayer', 'brokerProtection', 'performanceMetrics', 'clickToLoad', 'messageBridge', 'favicon'],
36+
apple: ['webCompat', 'duckPlayerNative', ...baseFeatures],
37+
'apple-isolated': [
38+
'duckPlayer',
39+
'duckPlayerNative',
40+
'brokerProtection',
41+
'performanceMetrics',
42+
'clickToLoad',
43+
'messageBridge',
44+
'favicon',
45+
],
3746
android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer', 'messageBridge'],
3847
'android-broker-protection': ['brokerProtection'],
3948
'android-autofill-password-import': ['autofillPasswordImport'],
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import ContentFeature from '../content-feature.js';
2+
import { isBeingFramed } from '../utils.js';
3+
import { DuckPlayerNativeMessages } from './duckplayer-native/native-messages.js';
4+
import { initDuckPlayerNative } from './duckplayer-native/duckplayer-native.js';
5+
6+
/**
7+
* @typedef InitialSettings - The initial payload used to communicate render-blocking information
8+
* @property {string} version - TODO: this is only here to test config. Replace with actual settings.
9+
*/
10+
11+
export class DuckPlayerNative extends ContentFeature {
12+
init() {
13+
if (this.platform.name !== 'ios') return;
14+
15+
/**
16+
* This feature never operates in a frame
17+
*/
18+
if (isBeingFramed()) return;
19+
20+
const comms = new DuckPlayerNativeMessages(this.messaging);
21+
initDuckPlayerNative(comms);
22+
}
23+
}
24+
25+
export default DuckPlayerNative;

injected/src/features/duckplayer-native.js

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const MSG_NAME_INITIAL_SETUP = 'initialSetup';
2+
export const MSG_NAME_SET_VALUES = 'setUserValues';
3+
export const MSG_NAME_READ_VALUES = 'getUserValues';
4+
export const MSG_NAME_READ_VALUES_SERP = 'readUserValues';
5+
export const MSG_NAME_OPEN_PLAYER = 'openDuckPlayer';
6+
export const MSG_NAME_OPEN_INFO = 'openInfo';
7+
export const MSG_NAME_PUSH_DATA = 'onUserValuesChanged';
8+
export const MSG_NAME_PIXEL = 'sendDuckPlayerPixel';
9+
export const MSG_NAME_PROXY_INCOMING = 'ddg-serp-yt';
10+
export const MSG_NAME_PROXY_RESPONSE = 'ddg-serp-yt-response';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { getCurrentTimestamp } from './getCurrentTimestamp.js';
2+
import { mediaControl } from './mediaControl.js';
3+
import { muteAudio } from './muteAudio.js';
4+
import { serpNotify } from './serpNotify.js';
5+
6+
/**
7+
*
8+
* @param {import('./native-messages.js').DuckPlayerNativeMessages} messages
9+
* @returns
10+
*/
11+
export async function initDuckPlayerNative(messages) {
12+
/** @type {import("../duck-player-native.js").InitialSettings} */
13+
let initialSetup;
14+
try {
15+
initialSetup = await messages.initialSetup();
16+
} catch (e) {
17+
console.error(e);
18+
return;
19+
}
20+
21+
console.log('INITIAL SETUP', initialSetup);
22+
23+
/**
24+
* Set up subscription listeners
25+
*/
26+
messages.onGetCurrentTimestamp(() => {
27+
console.log('GET CURRENT TIMESTAMP');
28+
getCurrentTimestamp();
29+
});
30+
31+
messages.onMediaControl(() => {
32+
console.log('MEDIA CONTROL');
33+
mediaControl();
34+
});
35+
36+
messages.onMuteAudio((mute) => {
37+
console.log('MUTE AUDIO', mute);
38+
muteAudio(mute);
39+
});
40+
41+
messages.onSerpNotify(() => {
42+
console.log('SERP PROXY');
43+
serpNotify();
44+
});
45+
}
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
function getCurrentTime() {
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-nocheck - Typing will be fixed in the future
3+
4+
export function getCurrentTimestamp() {
25
const video = document.querySelector('video');
36
return video ? video.currentTime : 0;
4-
}
7+
}
Lines changed: 57 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
(function() {
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-nocheck - Typing will be fixed in the future
3+
4+
export function mediaControl() {
25
// Initialize state if not exists
36
if (!window._mediaControlState) {
47
window._mediaControlState = {
58
observer: null,
69
userInitiated: false,
710
originalPlay: HTMLMediaElement.prototype.play,
811
originalLoad: HTMLMediaElement.prototype.load,
9-
isPaused: false
12+
isPaused: false,
1013
};
1114
}
1215
const state = window._mediaControlState;
1316

1417
// Block playback handler
15-
const blockPlayback = function(event) {
18+
const blockPlayback = function (event) {
1619
event.preventDefault();
1720
event.stopPropagation();
1821
return false;
@@ -21,59 +24,63 @@
2124
// The actual media control function
2225
function mediaControl(pause) {
2326
state.isPaused = pause;
24-
27+
2528
if (pause) {
2629
// Capture play events at the earliest possible moment
2730
document.addEventListener('play', blockPlayback, true);
2831
document.addEventListener('playing', blockPlayback, true);
29-
32+
3033
// Block HTML5 video/audio playback methods
31-
HTMLMediaElement.prototype.play = function() {
34+
HTMLMediaElement.prototype.play = function () {
3235
this.pause();
3336
return Promise.reject(new Error('Playback blocked'));
3437
};
35-
38+
3639
// Override load to ensure media starts paused
37-
HTMLMediaElement.prototype.load = function() {
40+
HTMLMediaElement.prototype.load = function () {
3841
this.autoplay = false;
3942
this.pause();
4043
return state.originalLoad.apply(this, arguments);
4144
};
4245

4346
// Listen for user interactions that may lead to playback
44-
document.addEventListener('touchstart', () => {
45-
state.userInitiated = true;
46-
47-
// Remove the early blocking listeners
48-
document.removeEventListener('play', blockPlayback, true);
49-
document.removeEventListener('playing', blockPlayback, true);
50-
51-
// Reset HTMLMediaElement.prototype.play
52-
HTMLMediaElement.prototype.play = state.originalPlay;
53-
54-
// Unmute all media elements when user interacts
55-
document.querySelectorAll('audio, video').forEach(media => {
56-
media.muted = false;
57-
});
47+
document.addEventListener(
48+
'touchstart',
49+
() => {
50+
state.userInitiated = true;
51+
52+
// Remove the early blocking listeners
53+
document.removeEventListener('play', blockPlayback, true);
54+
document.removeEventListener('playing', blockPlayback, true);
55+
56+
// Reset HTMLMediaElement.prototype.play
57+
HTMLMediaElement.prototype.play = state.originalPlay;
5858

59-
// Reset after a short delay
60-
setTimeout(() => {
61-
state.userInitiated = false;
62-
63-
// Re-add blocking if still in paused state
64-
if (state.isPaused) {
65-
document.addEventListener('play', blockPlayback, true);
66-
document.addEventListener('playing', blockPlayback, true);
67-
HTMLMediaElement.prototype.play = function() {
68-
this.pause();
69-
return Promise.reject(new Error('Playback blocked'));
70-
};
71-
}
72-
}, 500);
73-
}, true);
59+
// Unmute all media elements when user interacts
60+
document.querySelectorAll('audio, video').forEach((media) => {
61+
media.muted = false;
62+
});
63+
64+
// Reset after a short delay
65+
setTimeout(() => {
66+
state.userInitiated = false;
67+
68+
// Re-add blocking if still in paused state
69+
if (state.isPaused) {
70+
document.addEventListener('play', blockPlayback, true);
71+
document.addEventListener('playing', blockPlayback, true);
72+
HTMLMediaElement.prototype.play = function () {
73+
this.pause();
74+
return Promise.reject(new Error('Playback blocked'));
75+
};
76+
}
77+
}, 500);
78+
},
79+
true,
80+
);
7481

7582
// Initial pause of all media
76-
document.querySelectorAll('audio, video').forEach(media => {
83+
document.querySelectorAll('audio, video').forEach((media) => {
7784
media.pause();
7885
media.muted = true;
7986
media.autoplay = false;
@@ -83,19 +90,19 @@
8390
if (state.observer) {
8491
state.observer.disconnect();
8592
}
86-
87-
state.observer = new MutationObserver(mutations => {
88-
mutations.forEach(mutation => {
93+
94+
state.observer = new MutationObserver((mutations) => {
95+
mutations.forEach((mutation) => {
8996
// Check for added nodes
90-
mutation.addedNodes.forEach(node => {
97+
mutation.addedNodes.forEach((node) => {
9198
if (node.tagName === 'VIDEO' || node.tagName === 'AUDIO') {
9299
if (!state.userInitiated) {
93100
node.pause();
94101
node.muted = true;
95102
node.autoplay = false;
96103
}
97104
} else if (node.querySelectorAll) {
98-
node.querySelectorAll('audio, video').forEach(media => {
105+
node.querySelectorAll('audio, video').forEach((media) => {
99106
if (!state.userInitiated) {
100107
media.pause();
101108
media.muted = true;
@@ -107,34 +114,34 @@
107114
});
108115
});
109116

110-
state.observer.observe(document.documentElement || document.body, {
111-
childList: true,
117+
state.observer.observe(document.documentElement || document.body, {
118+
childList: true,
112119
subtree: true,
113120
attributes: true,
114-
attributeFilter: ['autoplay', 'src', 'playing']
121+
attributeFilter: ['autoplay', 'src', 'playing'],
115122
});
116123
} else {
117124
// Restore original methods
118125
HTMLMediaElement.prototype.play = state.originalPlay;
119126
HTMLMediaElement.prototype.load = state.originalLoad;
120-
127+
121128
// Remove listeners
122129
document.removeEventListener('play', blockPlayback, true);
123130
document.removeEventListener('playing', blockPlayback, true);
124-
131+
125132
// Clean up observer
126133
if (state.observer) {
127134
state.observer.disconnect();
128135
state.observer = null;
129136
}
130137

131138
// Unmute all media
132-
document.querySelectorAll('audio, video').forEach(media => {
139+
document.querySelectorAll('audio, video').forEach((media) => {
133140
media.muted = false;
134141
});
135142
}
136143
}
137144

138145
// Export function
139146
window.mediaControl = mediaControl;
140-
})();
147+
}
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
function muteAudio(mute) {
2-
document.querySelectorAll('audio, video').forEach(media => {
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-nocheck - Typing will be fixed in the future
3+
4+
export function muteAudio(mute) {
5+
document.querySelectorAll('audio, video').forEach((media) => {
36
media.muted = mute;
47
});
5-
}
8+
}

0 commit comments

Comments
 (0)