Skip to content

Commit 87c0feb

Browse files
Add video playback observer (#1721)
* Add video playback observer * Enable webCompat on Windows * Call on document load * Move to telemetry * Fix comment that was stolen * Add PoC test case * Add video listener * Listen earlier, dedupe messages * Check for presence of node * Make observer use documentElement as a fallback for a listener * Use notify as a fire and forget * Call messaging in the correct scope * Simplify * Use once listeners instead * add args to constructor * Add play watch on start * Fix scope * Cleanup * Rename to web-telemetry * Account for video tag reuse by deduping against URL instead * Bail out on seen urls --------- Co-authored-by: Jonathan Kingston <[email protected]> Co-authored-by: Jonathan Kingston <[email protected]>
1 parent 4ed322e commit 87c0feb

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed

injected/src/features.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const otherFeatures = /** @type {const} */ ([
2929
'breakageReporting',
3030
'autofillPasswordImport',
3131
'favicon',
32+
'webTelemetry',
3233
'scriptlets',
3334
]);
3435

@@ -51,6 +52,7 @@ export const platformSupport = {
5152
windows: [
5253
'cookie',
5354
...baseFeatures,
55+
'webTelemetry',
5456
'windowsPermissionUsage',
5557
'duckPlayer',
5658
'brokerProtection',
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import ContentFeature from '../content-feature.js';
2+
3+
const MSG_VIDEO_PLAYBACK = 'video-playback';
4+
5+
export class WebTelemetry extends ContentFeature {
6+
constructor(featureName, importConfig, args) {
7+
super(featureName, importConfig, args);
8+
this.seenVideoElements = new WeakSet();
9+
this.seenVideoUrls = new Set();
10+
}
11+
12+
init() {
13+
if (this.getFeatureSettingEnabled('videoPlayback')) {
14+
this.videoPlaybackObserve();
15+
}
16+
}
17+
18+
getVideoUrl(video) {
19+
// Try to get the video URL from various sources
20+
if (video.src) {
21+
return video.src;
22+
}
23+
if (video.currentSrc) {
24+
return video.currentSrc;
25+
}
26+
// Check for source elements
27+
const source = video.querySelector('source');
28+
if (source && source.src) {
29+
return source.src;
30+
}
31+
return null;
32+
}
33+
34+
fireTelemetryForVideo(video) {
35+
const videoUrl = this.getVideoUrl(video);
36+
if (this.seenVideoUrls.has(videoUrl)) {
37+
return;
38+
}
39+
// If we have a URL, store it just to deduplicate
40+
// This will clear on page change and isn't sent to native/server.
41+
if (videoUrl) {
42+
this.seenVideoUrls.add(videoUrl);
43+
}
44+
const message = {
45+
userInteraction: navigator.userActivation.isActive,
46+
};
47+
this.messaging.notify(MSG_VIDEO_PLAYBACK, message);
48+
}
49+
50+
addPlayObserver(video) {
51+
if (this.seenVideoElements.has(video)) {
52+
return; // already observed
53+
}
54+
this.seenVideoElements.add(video);
55+
video.addEventListener('play', () => this.fireTelemetryForVideo(video));
56+
}
57+
58+
addListenersToAllVideos(node) {
59+
if (!node) {
60+
return;
61+
}
62+
const videos = node.querySelectorAll('video');
63+
videos.forEach((video) => {
64+
this.addPlayObserver(video);
65+
});
66+
}
67+
68+
videoPlaybackObserve() {
69+
if (document.body) {
70+
this.setup();
71+
} else {
72+
window.addEventListener(
73+
'DOMContentLoaded',
74+
() => {
75+
this.setup();
76+
},
77+
{ once: true },
78+
);
79+
}
80+
}
81+
82+
setup() {
83+
const documentBody = document.body;
84+
if (!documentBody) return;
85+
86+
this.addListenersToAllVideos(documentBody);
87+
88+
// Backfill: fire telemetry for already playing videos
89+
documentBody.querySelectorAll('video').forEach((video) => {
90+
if (!video.paused && !video.ended) {
91+
this.fireTelemetryForVideo(video);
92+
}
93+
});
94+
95+
const observerCallback = (mutationsList) => {
96+
for (const mutation of mutationsList) {
97+
if (mutation.type === 'childList') {
98+
mutation.addedNodes.forEach((node) => {
99+
if (node.nodeType === Node.ELEMENT_NODE) {
100+
if (node.tagName === 'VIDEO') {
101+
this.addPlayObserver(node);
102+
} else {
103+
this.addListenersToAllVideos(node);
104+
}
105+
}
106+
});
107+
}
108+
}
109+
};
110+
const observer = new MutationObserver(observerCallback);
111+
observer.observe(documentBody, {
112+
childList: true,
113+
subtree: true,
114+
});
115+
}
116+
}
117+
118+
export default WebTelemetry;

0 commit comments

Comments
 (0)