diff options
Diffstat (limited to 'browser/extensions/webcompat/shims/vidible.js')
-rw-r--r-- | browser/extensions/webcompat/shims/vidible.js | 424 |
1 files changed, 424 insertions, 0 deletions
diff --git a/browser/extensions/webcompat/shims/vidible.js b/browser/extensions/webcompat/shims/vidible.js new file mode 100644 index 0000000000..1d45bc0f7e --- /dev/null +++ b/browser/extensions/webcompat/shims/vidible.js @@ -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); + } +} |