Implement initial media overlay

This commit is contained in:
hensm
2020-01-12 12:08:29 +00:00
parent 270d62e6f8
commit d65c607479
18 changed files with 552 additions and 11 deletions

View File

@@ -0,0 +1,391 @@
"use strict";
import options from "../../lib/options";
import cast, { ensureInit } from "../../shim/export";
import { Message, Receiver } from "../../types";
function getLocalAddress () {
const pc = new RTCPeerConnection();
pc.createDataChannel(null);
pc.createOffer().then(pc.setLocalDescription.bind(pc));
return new Promise((resolve, reject) => {
pc.addEventListener("icecandidate", ev => {
if (ev.candidate) {
resolve(ev.candidate.candidate.split(" ")[4]);
}
});
pc.addEventListener("error", () => {
reject();
});
});
}
function startMediaServer (filePath: string, port: number) {
return new Promise((resolve, reject) => {
backgroundPort.postMessage({
subject: "bridge:/mediaServer/start"
, data: {
filePath: decodeURI(filePath)
, port
}
});
backgroundPort.addEventListener("message", function onMessage (ev) {
const message = ev.data as Message;
if (message.subject.startsWith("mediaCast:/mediaServer/")) {
backgroundPort.removeEventListener("message", onMessage);
}
switch (message.subject) {
case "mediaCast:/mediaServer/started": {
resolve();
break;
}
case "mediaCast:/mediaServer/error": {
reject();
break;
}
}
});
backgroundPort.start();
});
}
let backgroundPort: MessagePort;
let currentSession: cast.Session;
let currentMedia: cast.media.Media;
let mediaElement: HTMLMediaElement;
function getSession (opts: InitOptions): Promise<cast.Session> {
return new Promise(async (resolve, reject) => {
/**
* If a receiver is available, call requestSession. If a
* specific receiver was specified, bypass receiver selector
* and create session directly.
*/
function receiverListener (availability: string) {
if (availability === cast.ReceiverAvailability.AVAILABLE) {
if (opts.receiver) {
cast._requestSession(
opts.receiver
, onRequestSessionSuccess
, onRequestSessionError);
} else {
cast.requestSession(
onRequestSessionSuccess
, onRequestSessionError);
}
}
}
function onRequestSessionSuccess (session: cast.Session) {
resolve(session);
}
function onRequestSessionError (err: cast.Error) {
reject(err.description);
}
const sessionRequest = new cast.SessionRequest(
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
const apiConfig = new cast.ApiConfig(
sessionRequest
, null // sessionListener
, receiverListener); // receiverListener
cast.initialize(apiConfig);
});
}
function getMedia (opts: InitOptions): Promise<cast.media.Media> {
return new Promise(async resolve => {
let mediaUrlObject = new URL(opts.mediaUrl);
const mediaTitle = mediaUrlObject.pathname;
/**
* If the media is a local file, start an HTTP media server
* and change the media URL to point to it.
*/
if (opts.mediaUrl.startsWith("file://")) {
const host = await getLocalAddress();
const port = await options.get("localMediaServerPort");
try {
// Wait until media server is listening
await startMediaServer(mediaUrlObject.pathname, port);
} catch (err) {
console.error("Failed to start media server");
return;
}
mediaUrlObject = new URL(`http://${host}:${port}/`);
}
const activeTrackIds: number[] = [];
const mediaInfo = new cast.media.MediaInfo(mediaUrlObject.href, null);
mediaInfo.metadata = new cast.media.GenericMediaMetadata();
mediaInfo.metadata.metadataType = cast.media.MetadataType.GENERIC;
mediaInfo.metadata.title = mediaTitle;
mediaInfo.tracks = [];
if (mediaElement) {
if (mediaElement instanceof HTMLVideoElement) {
if (mediaElement.poster) {
mediaInfo.metadata.images = [
new cast.Image(mediaElement.poster)
];
}
}
if (mediaElement.textTracks.length) {
const tracks = Array.from(mediaElement.textTracks);
const trackElements = mediaElement.querySelectorAll("track");
tracks.forEach((track, index) => {
const trackElement = trackElements[index];
/**
* Create media.Track object with the index as the track ID
* and type as TrackType.TEXT.
*/
const castTrack = new cast.media.Track(
index, cast.media.TrackType.TEXT);
// Copy TextTrack properties
castTrack.name = track.label;
castTrack.language = track.language;
castTrack.trackContentId = trackElement.src;
castTrack.trackContentType = "text/vtt";
switch (track.kind) {
case "subtitles":
castTrack.subtype =
cast.media.TextTrackType.SUBTITLES;
break;
case "captions":
castTrack.subtype =
cast.media.TextTrackType.CAPTIONS;
break;
case "descriptions":
castTrack.subtype =
cast.media.TextTrackType.DESCRIPTIONS;
break;
case "chapters":
castTrack.subtype =
cast.media.TextTrackType.CHAPTERS;
break;
case "metadata":
castTrack.subtype =
cast.media.TextTrackType.METADATA;
break;
// Default to subtitles
default:
castTrack.subtype =
cast.media.TextTrackType.SUBTITLES;
}
// Add track to mediaInfo
mediaInfo.tracks.push(castTrack);
// If enabled, mark as active track for load request
if (track.mode === "showing" || trackElement.default) {
activeTrackIds.push(index);
}
});
}
}
const loadRequest = new cast.media.LoadRequest(mediaInfo);
loadRequest.autoplay = false;
loadRequest.activeTrackIds = activeTrackIds;
currentSession.loadMedia(loadRequest
, (media) => resolve(media)
, null);
});
}
let ignoreMediaEvents = false;
async function registerMediaElementListeners () {
if (await options.get("mediaSyncElement")) {
function checkIgnore (ev: Event) {
if (ignoreMediaEvents) {
ignoreMediaEvents = false;
ev.stopImmediatePropagation();
}
}
mediaElement.addEventListener("play", checkIgnore, true);
mediaElement.addEventListener("pause", checkIgnore, true);
mediaElement.addEventListener("suspend", checkIgnore, true);
mediaElement.addEventListener("seeking", checkIgnore, true);
mediaElement.addEventListener("ratechange", checkIgnore, true);
mediaElement.addEventListener("volumechange", checkIgnore, true);
mediaElement.addEventListener("play", () => {
currentMedia.play(null, null, null);
});
mediaElement.addEventListener("pause", () => {
currentMedia.pause(null, null, null);
});
mediaElement.addEventListener("suspend", () => {
// currentMedia.stop(null, null, null);
});
mediaElement.addEventListener("seeked", () => {
const seekRequest = new cast.media.SeekRequest();
seekRequest.currentTime = mediaElement.currentTime;
currentMedia.seek(seekRequest, null, null);
});
mediaElement.addEventListener("ratechange", () => {
currentMedia._sendMediaMessage({
type: "SET_PLAYBACK_RATE"
, playbackRate: mediaElement.playbackRate
});
});
mediaElement.addEventListener("volumechange", () => {
const newVolume = new cast.Volume(
currentMedia.volume.level
, currentMedia.volume.muted);
const volumeRequest = new cast.media.VolumeRequest(newVolume);
currentMedia.setVolume(volumeRequest);
});
currentMedia.addUpdateListener(isAlive => {
if (!isAlive) {
return;
}
const localPlayerState = mediaElement.paused
? cast.media.PlayerState.PAUSED
: cast.media.PlayerState.PLAYING;
if (localPlayerState !== currentMedia.playerState) {
ignoreMediaEvents = true;
switch (currentMedia.playerState) {
case cast.media.PlayerState.PLAYING: {
mediaElement.play();
break;
}
case cast.media.PlayerState.PAUSED: {
mediaElement.pause();
break;
}
}
}
const localRepeatMode = mediaElement.loop
? cast.media.RepeatMode.SINGLE
: cast.media.RepeatMode.OFF;
if (localRepeatMode !== currentMedia.repeatMode) {
ignoreMediaEvents = true;
switch (currentMedia.repeatMode) {
case cast.media.RepeatMode.SINGLE: {
mediaElement.loop = true;
break;
}
case cast.media.RepeatMode.OFF: {
mediaElement.loop = false;
break;
}
}
}
if (currentMedia.currentTime !== mediaElement.currentTime) {
ignoreMediaEvents = true;
mediaElement.currentTime = currentMedia.currentTime;
}
});
}
}
interface InitOptions {
mediaUrl: string;
receiver: Receiver;
targetElementId?: number;
}
export async function init (opts: InitOptions) {
backgroundPort = await ensureInit();
const isLocalMedia = opts.mediaUrl.startsWith("file://");
const isLocalMediaEnabled = await options.get("localMediaEnabled");
if (isLocalMedia && !isLocalMediaEnabled) {
cast.logMessage("Local media casting not enabled");
return;
}
if (opts.targetElementId) {
mediaElement = browser.menus.getTargetElement(
opts.targetElementId) as HTMLMediaElement;
}
currentSession = await getSession(opts);
currentMedia = await getMedia(opts);
if (opts.targetElementId) {
registerMediaElementListeners();
if (options.get("mediaOverlayEnabled")) {
// TODO: Un-hide overlay here
}
window.addEventListener("beforeunload", async () => {
backgroundPort.postMessage({
subject: "bridge:/mediaServer/stop"
});
if (await options.get("mediaStopOnUnload")) {
currentSession.stop(null, null);
}
});
}
}
/**
* If loaded as a content script, the init values are
* provided on the window object.
*/
if (window.location.protocol !== "moz-extension:") {
const _window = (window as any);
init({
mediaUrl: _window.mediaUrl
, receiver: _window.receiver
, targetElementId: _window.targetElementId
});
}

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 125 125" fill="white">
<path d="M43.5 84.1l1.3-1.5c.3-.3.3-.8 0-1.1-10.5-9.7-11.2-26.2-1.4-36.7s26.2-11.2 36.7-1.4 11.2 26.2 1.4 36.7c-.5.5-.9 1-1.4 1.4-.3.3-.3.8 0 1.1l1.3 1.5c.3.3.8.3 1.1.1 12-11.1 12.7-29.7 1.7-41.7-11.1-12-29.7-12.7-41.7-1.7s-12.7 29.7-1.7 41.7c.5.6 1.1 1.1 1.7 1.7.3.2.7.2 1-.1z"/>
<path d="M44.8 62.5c0-9.7 7.9-17.6 17.6-17.6S80 52.9 80 62.6c0 4.8-2 9.5-5.5 12.8-.3.3-.3.8 0 1.1l1.3 1.5c.3.3.8.4 1.1.1 8.5-8 8.9-21.3 1-29.8s-21.3-8.9-29.8-1-9 21.2-1.1 29.7c.3.3.6.7 1 1 .3.3.8.3 1.1 0l1.3-1.5c.3-.3.3-.8 0-1.1-3.5-3.3-5.6-8-5.6-12.9z"/>
<path d="M53.2 62.5c0-5.1 4.1-9.2 9.2-9.2s9.2 4.1 9.2 9.2c0 2.5-1 4.8-2.8 6.6-.3.3-.3.8 0 1.1l1.3 1.5c.3.3.8.3 1.1 0 5-4.9 5.2-12.9.3-18s-12.9-5.2-18-.3-5.2 12.9-.3 18l.3.3c.3.3.8.3 1.1 0l1.3-1.5c.3-.3.3-.8 0-1.1-1.7-1.7-2.7-4.1-2.7-6.6z"/>
<path d="M80.9 89.1L63.5 69.3c-.5-.6-1.3-.6-1.9-.1l-.1.1-17.6 19.8c-.4.5-.4 1.2.1 1.7.2.2.5.3.7.3H80c.6 0 1.2-.5 1.2-1.2 0-.3-.1-.6-.3-.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 1016 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 125 125" fill="white">
<path d="M81 88.8c.4.5.4 1.3-.1 1.7-.2.2-.5.3-.8.3H44.9c-.7 0-1.2-.5-1.2-1.2 0-.3.1-.6.3-.8l17.5-20.1c.5-.6 1.3-.6 1.9-.1l.1.1L81 88.8zm-4.1-11.1l-2.8-3.3h10.5c.9.1 1.7-.1 2.5-.4.5-.3 1-.7 1.2-1.2.4-.8.5-1.7.4-2.5V45.8c.1-.9-.1-1.7-.4-2.5-.3-.5-.7-1-1.2-1.2-.8-.4-1.7-.5-2.5-.4h-44c-.9-.1-1.7.1-2.5.4-.5.3-1 .7-1.2 1.2-.4.8-.5 1.7-.4 2.5v24.4c-.1.9.1 1.7.4 2.5.3.5.7 1 1.2 1.2.8.4 1.7.5 2.5.4h10.5l-2.8 3.3h-6.7c-3 0-4-.3-5-.9-1.1-.6-1.9-1.4-2.5-2.5-.6-1.1-.9-2.1-.9-5V46.7c0-3 .3-4 .9-5.1.6-1.1 1.4-1.9 2.5-2.5 1.1-.6 2.1-.9 5-.9h42.2c3 0 4 .3 5.1.9 1.1.6 1.9 1.4 2.5 2.5.6 1.1.9 2.1.9 5.1v22.5c0 3-.3 4-.9 5-.6 1.1-1.4 1.9-2.5 2.5-1.1.6-2.1.9-5.1.9l-6.9.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -0,0 +1,75 @@
"use strict";
/**
* Walk up the prototype chain until the specified property
* descriptor is found, otherwise return undefined.
*/
export function getPropertyDescriptor (
target: any, prop: string | number | symbol)
: PropertyDescriptor | undefined {
let desc: PropertyDescriptor | undefined;
while (!desc && target !== null) {
desc = Object.getOwnPropertyDescriptor(target, prop);
if (!desc) {
target = Object.getPrototypeOf(target);
}
}
return desc;
}
/**
* Bind either the getter/setter functions or the value function
* to a target object.
*/
export function bindPropertyDescriptor (
desc: PropertyDescriptor, target: any)
: PropertyDescriptor {
if (typeof desc.value === "function") {
desc.value = desc.value.bind(target);
} else {
if (desc.get) { desc.get = desc.get.bind(target); }
if (desc.set) { desc.set = desc.set.bind(target); }
}
return desc;
}
/**
* For each attribute handler, fetch the property descriptor (which may
* be further up in the prototype chain), re-bind it to the target
* element and collect them into a property descriptor map.
*/
export function clonePropsDescriptor<T> (
target: T, props: Array<any/*keyof typeof T*/>)
: PropertyDescriptorMap {
return props.reduce<PropertyDescriptorMap>((descriptorMap, prop) => {
const desc = getPropertyDescriptor(target, prop);
if (desc) {
bindPropertyDescriptor(desc, target);
descriptorMap[prop as any] = desc;
}
return descriptorMap;
}, {});
}
export function makeGetterDescriptor (val: any): PropertyDescriptor {
return {
enumerable: true
, configurable: true
, get () { return val; }
};
}
export function makeValueDescriptor (val: any): PropertyDescriptor {
return {
enumerable: true
, configurable: true
, writable: true
, value: val
};
}

View File

@@ -0,0 +1,366 @@
"use strict";
import { bindPropertyDescriptor
, clonePropsDescriptor
, getPropertyDescriptor
, makeGetterDescriptor
, makeValueDescriptor } from "./descriptorUtils";
// Injected by content loader
declare const iconAirPlayAudio: string;
declare const iconAirPlayVideo: string;
declare const mediaOverlayTitle: string;
/**
* Intercept and store references to shadow root nodes created by
* calls to `attachShadow`. Used to reference shadow roots, even when
* created in closed mode without exposing them to other page scripts.
*/
const internalShadowRoots = new WeakMap<Element, ShadowRoot>();
const _attachShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function (init) {
const shadowRoot = _attachShadow.call(this, init);
internalShadowRoots.set(this, shadowRoot);
return shadowRoot;
};
function getShadowRootFromNode (node: Node): ShadowRoot | undefined {
// Don't touch our custom element
if (node instanceof PlayerElement) {
return;
}
return internalShadowRoots.get(node as Element);
}
const DQS_XPATH_EXPRESSION = `//*[contains(name(), "-")]`;
/**
* Return the first matching querySelector result on any ShadowRoot
* nodes present in the document.
*/
function deepQuerySelector (selector: string): Element | null {
const result = document.evaluate(
DQS_XPATH_EXPRESSION, document, null
, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
let node: Node | null;
while (node = result.iterateNext()) {
const shadowRoot = getShadowRootFromNode(node);
if (!shadowRoot) {
continue;
}
const queryResult = shadowRoot.querySelector(selector);
if (queryResult) {
return queryResult;
}
}
return null;
}
/**
* Collect and return the results of querySelectorAll on any
* ShadowRoot nodes present in the document.
*/
function deepQuerySelectorAll (selector: string): Node[] {
const result = document.evaluate(
DQS_XPATH_EXPRESSION, document, null
, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
const nodes: Node[] = [];
let node: Node | null;
while (node = result.iterateNext()) {
const shadowRoot = getShadowRootFromNode(node);
if (shadowRoot) {
nodes.push(...shadowRoot.querySelectorAll(selector));
}
}
return nodes;
}
const mediaElementTypes = [
HTMLMediaElement
, HTMLVideoElement
, HTMLAudioElement
];
const mediaElementEvents = [
"abort", "canplay", "canplaythrough", "durationchange", "emptied"
, "encrypted", "ended", "error", "interruptbegin", "interruptend"
, "loadeddata", "loadedmetadata", "loadstart", "mozaudioavailable", "pause"
, "play", "playing", "progress", "ratechange", "seeked", "seeking", "stalled"
, "suspend", "timeupdate", "volumechange", "waiting"
];
const mediaElementAttributes = mediaElementTypes
.flatMap(type => Object.getOwnPropertyNames(type.prototype))
.concat(mediaElementEvents.map(ev => `on${ev}`));
/**
* Opaque wrapper around the media element to provide an overlay without
* author interference. Relevant properties, attributes, events and
* functions are proxied to the internal media element.
*/
class PlayerElement extends HTMLElement {
constructor () {
super();
const shadowRoot = this.attachShadow({ mode: "closed" });
const { host } = shadowRoot;
let iconUrl;
switch (this.constructor) {
// URL variables injected ahead of current script
case AudioPlayerElement: { iconUrl = iconAirPlayAudio; break; }
case VideoPlayerElement: { iconUrl = iconAirPlayVideo; break; }
}
shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
font: menu;
position: relative;
}
:host[hidden],
.overlay[hidden] {
display: none;
}
video {
width: 100%;
}
.overlay {
align-items: center;
background-color: rgba(0, 0, 0, 0.85);
color: white;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
left: 0;
pointer-events: none;
position: absolute;
top: 0;
width: 100%;
}
.overlay__icon {
background-image: url("${iconUrl}");
height: 125px;
width: 125px;
}
.overlay__text {
font-size: 18px;
}
</style>
<div class="overlay" hidden>
<div class="overlay__icon"></div>
<div class="overlay__text">
${mediaOverlayTitle}
</div>
</div>
`;
const videoElement = _createElement.call(document, "video");
for (const attr of mediaElementAttributes) {
if (host.hasOwnProperty(attr)) {
// @ts-ignore
videoElement[attr] = host[attr];
}
}
/**
* Page scripts need to be able to read/write attributes, event
* listeners, etc... on the media element, but since it's hidden
* within the shadow DOM, these properties must be proxied.
*/
Object.defineProperties(host, clonePropsDescriptor(videoElement, [
"attributes", "setAttribute", "removeAttribute", "setAttribute"
, "addEventListener", "removeEventListener", "hasEventListener"
, ...mediaElementAttributes as any ]));
shadowRoot.prepend(videoElement);
}
}
class AudioPlayerElement extends PlayerElement {}
class VideoPlayerElement extends PlayerElement {
set overlayHidden (val: boolean) {
const shadowRoot = internalShadowRoots.get(this);
(shadowRoot.querySelector(".overlay") as HTMLDivElement).hidden = val;
}
get overlayHidden () {
const shadowRoot = internalShadowRoots.get(this);
return (shadowRoot.querySelector(".overlay") as HTMLDivElement).hidden;
}
}
try {
customElements.define("audio-player-element", AudioPlayerElement);
customElements.define("video-player-element", VideoPlayerElement);
} catch (err) {
if (err instanceof DOMException
&& err.code === DOMException.NOT_SUPPORTED_ERR) {
// Script already injected
}
}
// Original functions
const _createElement = document.createElement;
const _createElementNS = document.createElementNS;
/**
* Intercepts `<audio>`/`<video>` element creation and returns a wrapped
* custom element version that imitates the original. Otherwise, returns
* the result of the original.
*/
function createElement (
tagName: string
, options?: ElementCreationOptions) {
// Normalize formatting
const lowerTagName = tagName.toLowerCase();
const upperTagName = tagName.toUpperCase();
if (lowerTagName === "audio" || lowerTagName === "video") {
const fakeElement = _createElement.call(document
, `${lowerTagName}-player-element`) as HTMLMediaElement;
// Ensure all references to the element name match tagName
Object.defineProperties(fakeElement, {
tagName: makeGetterDescriptor(upperTagName)
, nodeName: makeGetterDescriptor(upperTagName)
, localName: makeGetterDescriptor(lowerTagName)
});
return fakeElement;
}
return _createElement.call(document, tagName, options);
}
/**
* If the namespace matches the current document, redirect to the
* patched `createElement` function, otherwise return the result of the
* original.
*/
function createElementNS (
namespaceURI: string
, qualifiedName: string
, options?: ElementCreationOptions) {
if (namespaceURI === document.namespaceURI) {
return createElement(qualifiedName, options);
}
return _createElementNS.call(document
, namespaceURI, qualifiedName, options);
}
/**
* Attempt to hide function source from page scripts by returning the
* toString/toSource values of the native function.
*/
Object.defineProperties(createElement, clonePropsDescriptor(
_createElement, ["toString", "toSource"]));
Object.defineProperties(createElement, clonePropsDescriptor(
_createElementNS, ["toString", "toSource"]));
// Re-define element creation functions
Object.defineProperties(document, {
createElement: makeValueDescriptor(createElement)
, createElementNS: makeValueDescriptor(createElementNS)
});
/**
* Takes a media element, creates a `PlayerElement` via the patched
* `createElement` function, fetches the shadow root and copies any
* attributes before swapping with the original element in-place.
*/
function wrapMediaElement (mediaElement: HTMLMediaElement) {
const wrappedMedia = document.createElement(mediaElement.tagName);
const shadowRoot = internalShadowRoots.get(wrappedMedia);
if (!shadowRoot) {
console.error("err: Failed to fetch shadow root!");
return;
}
/**
* Copy attributes, any non-media specific attributes are set to the
* wrapper element for identification (id, class, etc...) or styling.
*/
for (const attr of mediaElement.attributes) {
if (mediaElementAttributes.includes(attr.name)) {
wrappedMedia.setAttribute(attr.name, attr.value);
} else {
/**
* Since the wrapped element has a patched `setAttribute`
* method, need to call the original from the `HTMLElement`
* prototype, otherwise attributes will be set on the
* internal media element instead.
*/
HTMLElement.prototype.setAttribute.call(
wrappedMedia, attr.name, attr.value);
}
}
/**
* Clone and append any HTMLSourceElement children to the
* internal media element within the wrapped media shadow root.
*/
for (const source of mediaElement.getElementsByTagName("source")) {
const internalMedia = shadowRoot.querySelector("audio,video");
if (!internalMedia) {
console.error("err: Failed to fetch internal video element!");
return;
}
internalMedia.appendChild(source.cloneNode());
}
// Replace media element on page with wrapped media
mediaElement.replaceWith(wrappedMedia);
}
/*function* joinIterables (...iterables: Array<Iterable<any>>) {
for (const iterable of iterables) {
for (const item of iterable) {
yield item;
}
}
}*/
/**
* Find all media elements (both in the main DOM and any shadow DOMs)
* and wrap them.
*/
document.addEventListener("DOMContentLoaded", () => {
const mediaSelector = "audio,video";
setTimeout(() => {
const mediaElements = document.querySelectorAll(mediaSelector);
const deepMediaElements = deepQuerySelectorAll(mediaSelector);
for (const mediaElement of [...Array.from(mediaElements), ...deepMediaElements]) {
wrapMediaElement(mediaElement as HTMLMediaElement);
}
});
});

View File

@@ -0,0 +1,31 @@
"use strict";
const _ = browser.i18n.getMessage;
/**
* Make synchronous request for another script to keep other page
* scripts from loading before its execution.
*/
const req = new XMLHttpRequest();
req.open("GET", browser.runtime.getURL(
"senders/media/overlay/overlayContent.js"), false);
req.send();
if (req.status === 200) {
// TODO: Replace with cast icons until AirPlay support is ready
const iconAirPlayAudio = browser.runtime.getURL(
"senders/media/overlay/AirPlay_Audio.svg");
const iconAirPlayVideo = browser.runtime.getURL(
"senders/media/overlay/AirPlay_Audio.svg");
const scriptElement = document.createElement("script");
scriptElement.textContent = `(function(){
const iconAirPlayAudio = "${iconAirPlayAudio}";
const iconAirPlayVideo = "${iconAirPlayVideo}";
const mediaOverlayTitle = "${_("mediaOverlayTitle", "X")}";
${req.responseText}
})();`;
// <head> probably doesn't exist yet
(document.head || document.documentElement).append(scriptElement);
}