438 lines
12 KiB
JavaScript
438 lines
12 KiB
JavaScript
/* 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 = (() => {
|
|
const src = document.currentScript?.src;
|
|
try {
|
|
const { protocol, hostname, pathname, href } = new URL(src);
|
|
if (
|
|
(protocol === "http:" || protocol === "https:") &&
|
|
pathname.endsWith("/vidible-min.js") &&
|
|
(hostname.endsWith(".vidible.tv") ||
|
|
hostname === "vdb-cdn-files.s3.amazonaws.com")
|
|
) {
|
|
return href;
|
|
}
|
|
} catch (_) {}
|
|
return "https://cdn-ssl.vidible.tv/prod/player/js/21.1.1/vidible-min.js";
|
|
})();
|
|
|
|
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);
|
|
}
|
|
}
|