/* 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); } }