Bug 1713710 - Add a SmartBlock shim for Vidible-served video playback on user opt-in; r=denschub,webcompat-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D122756
This commit is contained in:
Thomas Wisniewski 2021-08-16 19:46:19 +00:00
Родитель d4f3c0c592
Коммит e427ce2e32
4 изменённых файлов: 453 добавлений и 1 удалений

Просмотреть файл

@ -417,6 +417,32 @@ const AVAILABLE_SHIMS = [
matches: ["*://media.richrelevance.com/rrserver/js/1.2/p13n.js"],
onlyIfBlockedByETP: true,
},
{
id: "Vidible",
branch: ["nightly"],
platform: "all",
name: "Vidible",
bug: "1713710",
file: "vidible.js",
logos: ["play.svg"],
matches: [
"*://*.vidible.tv/*/vidible-min.js*",
"*://vdb-cdn-files.s3.amazonaws.com/*/vidible-min.js*",
],
needsShimHelpers: ["optIn"],
onlyIfBlockedByETP: true,
unblocksOnOptIn: [
"*://delivery.vidible.tv/jsonp/pid=*/vid=*/*.js*",
"*://delivery.vidible.tv/placement/*",
"*://img.vidible.tv/prod/*",
"*://cdn-ssl.vidible.tv/prod/player/js/*.js",
"*://hlsrv.vidible.tv/prod/*.m3u8*",
"*://videos.vidible.tv/prod/*.key*",
"*://videos.vidible.tv/prod/*.mp4*",
"*://videos.vidible.tv/prod/*.webm*",
"*://videos.vidible.tv/prod/*.ts*",
],
},
];
module.exports = AVAILABLE_SHIMS;

Просмотреть файл

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "Web Compatibility Interventions",
"description": "Urgent post-release fixes for web compatibility.",
"version": "25.4.0",
"version": "25.5.0",
"applications": {
"gecko": {
@ -124,6 +124,7 @@
"shims/tracking-pixel.png",
"shims/vast2.xml",
"shims/vast3.xml",
"shims/vidible.js",
"shims/vmad.xml"
]
}

Просмотреть файл

@ -113,6 +113,7 @@ FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["shims"] += [
"shims/tracking-pixel.png",
"shims/vast2.xml",
"shims/vast3.xml",
"shims/vidible.js",
"shims/vmad.xml",
]

Просмотреть файл

@ -0,0 +1,424 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* Bug 1713710 - Shim Vidible video player
*
* Sites relying on Vidible's video player may experience broken videos if that
* script is blocked. This shim allows users to opt into viewing those videos
* regardless of any tracking consequences, by providing placeholders for each.
*/
if (!window.vidible?.version) {
const PlayIconURL = "https://smartblock.firefox.etp/play.svg";
const originalScript = document.currentScript.src;
const getGUID = () => {
const v = crypto.getRandomValues(new Uint8Array(20));
return Array.from(v, c => c.toString(16)).join("");
};
const sendMessageToAddon = (function() {
const shimId = "Vidible";
const pendingMessages = new Map();
const channel = new MessageChannel();
channel.port1.onerror = console.error;
channel.port1.onmessage = event => {
const { messageId, response } = event.data;
const resolve = pendingMessages.get(messageId);
if (resolve) {
pendingMessages.delete(messageId);
resolve(response);
}
};
function reconnect() {
const detail = {
pendingMessages: [...pendingMessages.values()],
port: channel.port2,
shimId,
};
window.dispatchEvent(new CustomEvent("ShimConnects", { detail }));
}
window.addEventListener("ShimHelperReady", reconnect);
reconnect();
return function(message) {
const messageId = getGUID();
return new Promise(resolve => {
const payload = { message, messageId, shimId };
pendingMessages.set(messageId, resolve);
channel.port1.postMessage(payload);
});
};
})();
const Shimmer = (function() {
// If a page might store references to an object before we replace it,
// ensure that it only receives proxies to that object created by
// `Shimmer.proxy(obj)`. Later when the unshimmed object is created,
// call `Shimmer.unshim(proxy, unshimmed)`. This way the references
// will automatically "become" the unshimmed object when appropriate.
const shimmedObjects = new WeakMap();
const unshimmedObjects = new Map();
function proxy(shim) {
if (shimmedObjects.has(shim)) {
return shimmedObjects.get(shim);
}
const prox = new Proxy(shim, {
get: (target, k) => {
if (unshimmedObjects.has(prox)) {
return unshimmedObjects.get(prox)[k];
}
return target[k];
},
apply: (target, thisArg, args) => {
if (unshimmedObjects.has(prox)) {
return unshimmedObjects.get(prox)(...args);
}
return target.apply(thisArg, args);
},
construct: (target, args) => {
if (unshimmedObjects.has(prox)) {
return new unshimmedObjects.get(prox)(...args);
}
return new target(...args);
},
});
shimmedObjects.set(shim, prox);
shimmedObjects.set(prox, prox);
for (const key in shim) {
const value = shim[key];
if (typeof value === "function") {
shim[key] = function() {
const unshimmed = unshimmedObjects.get(prox);
if (unshimmed) {
return unshimmed[key].apply(unshimmed, arguments);
}
return value.apply(this, arguments);
};
} else if (typeof value !== "object" || value === null) {
shim[key] = value;
} else {
shim[key] = Shimmer.proxy(value);
}
}
return prox;
}
function unshim(shim, unshimmed) {
unshimmedObjects.set(shim, unshimmed);
for (const prop in shim) {
if (prop in unshimmed) {
const un = unshimmed[prop];
if (typeof un === "object" && un !== null) {
unshim(shim[prop], un);
}
} else {
unshimmedObjects.set(shim[prop], undefined);
}
}
}
return { proxy, unshim };
})();
const extras = [];
const playersByNode = new WeakMap();
const playerData = new Map();
const getJSONPVideoPlacements = () => {
return document.querySelectorAll(
`script[src*="delivery.vidible.tv/jsonp"]`
);
};
const allowVidible = () => {
if (allowVidible.promise) {
return allowVidible.promise;
}
const shim = window.vidible;
window.vidible = undefined;
allowVidible.promise = sendMessageToAddon("optIn")
.then(() => {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = originalScript;
script.addEventListener("load", () => {
Shimmer.unshim(shim, window.vidible);
for (const args of extras) {
window.visible.registerExtra(...args);
}
for (const jsonp of getJSONPVideoPlacements()) {
const { src } = jsonp;
const jscript = document.createElement("script");
jscript.onload = resolve;
jscript.src = src;
jsonp.replaceWith(jscript);
}
for (const [playerShim, data] of playerData.entries()) {
const { loadCalled, on, parent, placeholder, setup } = data;
placeholder?.remove();
const player = window.vidible.player(parent);
Shimmer.unshim(playerShim, player);
for (const [type, fns] of on.entries()) {
for (const fn of fns) {
try {
player.on(type, fn);
} catch (e) {
console.error(e);
}
}
}
if (setup) {
player.setup(setup);
}
if (loadCalled) {
player.load();
}
}
resolve();
});
script.addEventListener("error", () => {
script.remove();
reject();
});
document.head.appendChild(script);
});
})
.catch(() => {
window.vidible = shim;
delete allowVidible.promise;
});
return allowVidible.promise;
};
const createVideoPlaceholder = (service, callback) => {
const placeholder = document.createElement("div");
placeholder.style = `
position: absolute;
width: 100%;
height: 100%;
min-width: 160px;
min-height: 100px;
top: 0px;
left: 0px;
background: #000;
color: #fff;
text-align: center;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background-image: url(${PlayIconURL});
background-position: 50% 47.5%;
background-repeat: no-repeat;
background-size: 25% 25%;
-moz-text-size-adjust: none;
-moz-user-select: none;
color: #fff;
align-items: center;
padding-top: 200px;
font-size: 14pt;
`;
placeholder.textContent = `Click to allow blocked ${service} content`;
placeholder.addEventListener("click", evt => {
evt.isTrusted && callback();
});
return placeholder;
};
const Player = function(parent) {
const existing = playersByNode.get(parent);
if (existing) {
return existing;
}
const player = Shimmer.proxy(this);
playersByNode.set(parent, player);
const placeholder = createVideoPlaceholder("Vidible", allowVidible);
parent.parentNode.insertBefore(placeholder, parent);
playerData.set(player, {
on: new Map(),
parent,
placeholder,
});
return player;
};
const changeData = function(fn) {
const data = playerData.get(this);
if (data) {
fn(data);
playerData.set(this, data);
}
};
Player.prototype = {
addEventListener() {},
destroy() {
const { placeholder } = playerData.get(this);
placeholder?.remove();
playerData.delete(this);
},
dispatchEvent() {},
getAdsPassedTime() {},
getAllMacros() {},
getCurrentTime() {},
getDuration() {},
getHeight() {},
getPixelsLog() {},
getPlayerContainer() {},
getPlayerInfo() {},
getPlayerStatus() {},
getRequestsLog() {},
getStripUrl() {},
getVolume() {},
getWidth() {},
hidePlayReplayControls() {},
isMuted() {},
isPlaying() {},
load() {
changeData(data => (data.loadCalled = true));
},
mute() {},
on(type, fn) {
changeData(({ on }) => {
if (!on.has(type)) {
on.set(type, new Set());
}
on.get(type).add(fn);
});
},
off(type, fn) {
changeData(({ on }) => {
on.get(type)?.delete(fn);
});
},
overrideMacro() {},
pause() {},
play() {},
playVideoByIndex() {},
removeEventListener() {},
seekTo() {},
sendBirthDate() {},
sendKey() {},
setup(s) {
changeData(data => (data.setup = s));
return this;
},
setVideosToPlay() {},
setVolume() {},
showPlayReplayControls() {},
toggleFullscreen() {},
toggleMute() {},
togglePlay() {},
updateBid() {},
version() {},
volume() {},
};
const vidible = {
ADVERT_CLOSED: "advertClosed",
AD_END: "adend",
AD_META: "admeta",
AD_PAUSED: "adpaused",
AD_PLAY: "adplay",
AD_START: "adstart",
AD_TIMEUPDATE: "adtimeupdate",
AD_WAITING: "adwaiting",
AGE_GATE_DISPLAYED: "agegatedisplayed",
BID_UPDATED: "BidUpdated",
CAROUSEL_CLICK: "CarouselClick",
CONTEXT_ENDED: "contextended",
CONTEXT_STARTED: "contextstarted",
ENTER_FULLSCREEN: "playerenterfullscreen",
EXIT_FULLSCREEN: "playerexitfullscreen",
FALLBACK: "fallback",
FLOAT_END_ACTION: "floatended",
FLOAT_START_ACTION: "floatstarted",
HIDE_PLAY_REPLAY_BUTTON: "hideplayreplaybutton",
LIGHTBOX_ACTIVATED: "lightboxactivated",
LIGHTBOX_DEACTIVATED: "lightboxdeactivated",
MUTE: "Mute",
PLAYER_CONTROLS_STATE_CHANGE: "playercontrolsstatechaned",
PLAYER_DOCKED: "playerDocked",
PLAYER_ERROR: "playererror",
PLAYER_FLOATING: "playerFloating",
PLAYER_READY: "playerready",
PLAYER_RESIZE: "playerresize",
PLAYLIST_END: "playlistend",
SEEK_END: "SeekEnd",
SEEK_START: "SeekStart",
SHARE_SCREEN_CLOSED: "sharescreenclosed",
SHARE_SCREEN_OPENED: "sharescreenopened",
SHOW_PLAY_REPLAY_BUTTON: "showplayreplaybutton",
SUBTITLES_DISABLED: "subtitlesdisabled",
SUBTITLES_ENABLED: "subtitlesenabled",
SUBTITLES_READY: "subtitlesready",
UNMUTE: "Unmute",
VIDEO_DATA_LOADED: "videodataloaded",
VIDEO_END: "videoend",
VIDEO_META: "videometadata",
VIDEO_MODULE_CREATED: "videomodulecreated",
VIDEO_PAUSE: "videopause",
VIDEO_PLAY: "videoplay",
VIDEO_SEEKEND: "videoseekend",
VIDEO_SELECTED: "videoselected",
VIDEO_START: "videostart",
VIDEO_TIMEUPDATE: "videotimeupdate",
VIDEO_VOLUME_CHANGED: "videovolumechanged",
VOLUME: "Volume",
_getContexts: () => [],
"content.CLICK": "content.click",
"content.IMPRESSION": "content.impression",
"content.QUARTILE": "content.quartile",
"content.VIEW": "content.view",
createPlayer: parent => new Player(parent),
createPlayerAsync: parent => new Player(parent),
createVPAIDPlayer: parent => new Player(parent),
destroyAll() {},
extension() {},
getContext() {},
player: parent => new Player(parent),
playerInceptionTime() {
return { undefined: 1620149827713 };
},
registerExtra(a, b, c) {
extras.push([a, b, c]);
},
version: () => "21.1.313",
};
window.vidible = Shimmer.proxy(vidible);
for (const jsonp of getJSONPVideoPlacements()) {
const player = new Player(jsonp);
const { placeholder } = playerData.get(player);
jsonp.parentNode.insertBefore(placeholder, jsonp);
}
}