mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
Remove experimental media overlay
This commit is contained in:
@@ -74,8 +74,6 @@ const buildOpts = {
|
||||
`${srcPath}/background/background.ts`,
|
||||
// Media sender
|
||||
`${srcPath}/senders/media/index.ts`,
|
||||
`${srcPath}/senders/media/overlay/overlayContent.ts`,
|
||||
`${srcPath}/senders/media/overlay/overlayContentLoader.ts`,
|
||||
// Mirroring sender
|
||||
`${srcPath}/senders/mirroring.ts`,
|
||||
// Cast
|
||||
|
||||
@@ -92,18 +92,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
, "mediaOverlayTitle": {
|
||||
"message": "Wiedergabe auf $receiverName$"
|
||||
, "description": "Main title for overlay displayed on media elements whilst casting."
|
||||
, "placeholders": {
|
||||
"receiverName": {
|
||||
"content": "$1"
|
||||
, "example": "Living Room TV"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, "optionsBridgeLoading": {
|
||||
"message": "Lade Bridge-Informationen..."
|
||||
, "description": "Loading placeholder text for bridge section on options page."
|
||||
@@ -226,18 +214,6 @@
|
||||
"message": "Streamen von Medien aktivieren"
|
||||
, "description": "Media casting enabled checkbox label."
|
||||
}
|
||||
, "optionsMediaOverlayEnabled": {
|
||||
"message": "Aktiviere Medien-Overlay"
|
||||
, "description": "Media element overlay checkbox label."
|
||||
}
|
||||
, "optionsMediaOverlayEnabledTemp": {
|
||||
"message": "Aktiviere Medien-Overlay (experimentell)"
|
||||
, "description": "Experimental-labelled version of above."
|
||||
}
|
||||
, "optionsMediaOverlayEnabledDescription": {
|
||||
"message": "Overlay auf Medien, das, wenn verbunden, Informationen über die aktuelle Sitzung anzeigt."
|
||||
, "description": "Media element overlay option description."
|
||||
}
|
||||
, "optionsMediaSyncElement": {
|
||||
"message": "Empfängerstatus mit Media-Element synchronisieren"
|
||||
, "description": "Media casting sync checkbox label."
|
||||
|
||||
@@ -97,18 +97,6 @@
|
||||
}
|
||||
|
||||
|
||||
, "mediaOverlayTitle": {
|
||||
"message": "Playing on $receiverName$"
|
||||
, "description": "Main title for overlay displayed on media elements whilst casting."
|
||||
, "placeholders": {
|
||||
"receiverName": {
|
||||
"content": "$1"
|
||||
, "example": "Living Room TV"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
, "optionsBridgeLoading": {
|
||||
"message": "Loading bridge info..."
|
||||
, "description": "Loading placeholder text for bridge section on options page."
|
||||
@@ -231,18 +219,6 @@
|
||||
"message": "Enable media casting"
|
||||
, "description": "Media casting enabled checkbox label."
|
||||
}
|
||||
, "optionsMediaOverlayEnabled": {
|
||||
"message": "Enable media element overlay"
|
||||
, "description": "Media element overlay checkbox label."
|
||||
}
|
||||
, "optionsMediaOverlayEnabledTemp": {
|
||||
"message": "Enable media element overlay (experimental)"
|
||||
, "description": "Experimental-labelled version of above."
|
||||
}
|
||||
, "optionsMediaOverlayEnabledDescription": {
|
||||
"message": "Overlay on media elements displaying information about the current session if connected."
|
||||
, "description": "Media element overlay option description."
|
||||
}
|
||||
, "optionsMediaSyncElement": {
|
||||
"message": "Sync receiver state with media element"
|
||||
, "description": "Media casting sync checkbox label."
|
||||
|
||||
@@ -97,18 +97,6 @@
|
||||
}
|
||||
|
||||
|
||||
, "mediaOverlayTitle": {
|
||||
"message": "Reproduciendo en $receiverName$"
|
||||
, "description": "Main title for overlay displayed on media elements whilst casting."
|
||||
, "placeholders": {
|
||||
"receiverName": {
|
||||
"content": "$1"
|
||||
, "example": "Living Room TV"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
, "optionsBridgeLoading": {
|
||||
"message": "Cargando información de la aplicación puente..."
|
||||
, "description": "Loading placeholder text for bridge section on options page."
|
||||
@@ -231,18 +219,6 @@
|
||||
"message": "Activar transmisión de contenidos"
|
||||
, "description": "Media casting enabled checkbox label."
|
||||
}
|
||||
, "optionsMediaOverlayEnabled": {
|
||||
"message": "Activar sobreposición en el elemento de medios"
|
||||
, "description": "Media element overlay checkbox label."
|
||||
}
|
||||
, "optionsMediaOverlayEnabledTemp": {
|
||||
"message": "Activar sobreposición en el elemento de medios (experimental)"
|
||||
, "description": "Experimental-labelled version of above."
|
||||
}
|
||||
, "optionsMediaOverlayEnabledDescription": {
|
||||
"message": "Sobreposición en elementos de medios mostrando información acerca de la sesión actual si está conectado."
|
||||
, "description": "Media element overlay option description."
|
||||
}
|
||||
, "optionsMediaSyncElement": {
|
||||
"message": "Sincronizar estado del receptor con el contenido"
|
||||
, "description": "Media casting sync checkbox label."
|
||||
|
||||
@@ -85,16 +85,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mediaOverlayTitle": {
|
||||
"message": "Speelt af op $receiverName$",
|
||||
"description": "Main title for overlay displayed on media elements whilst casting.",
|
||||
"placeholders": {
|
||||
"receiverName": {
|
||||
"content": "$1",
|
||||
"example": "Living Room TV"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsBridgeLoading": {
|
||||
"message": "Bezig met laden van bridge-informatie...",
|
||||
"description": "Loading placeholder text for bridge section on options page."
|
||||
@@ -209,18 +199,6 @@
|
||||
"message": "Mediacasten ingeschakeld",
|
||||
"description": "Media casting enabled checkbox label."
|
||||
},
|
||||
"optionsMediaOverlayEnabled": {
|
||||
"message": "Inschakelen overlay media element",
|
||||
"description": "Media element overlay checkbox label."
|
||||
},
|
||||
"optionsMediaOverlayEnabledTemp": {
|
||||
"message": "Inschakelen overlay media element (Experimenteel)",
|
||||
"description": "Experimental-labelled version of above."
|
||||
},
|
||||
"optionsMediaOverlayEnabledDescription": {
|
||||
"message": "Een overlay over media elementen die informatie weergeeft over de huidige sessie indien verbonden.",
|
||||
"description": "Media element overlay option description."
|
||||
},
|
||||
"optionsMediaSyncElement": {
|
||||
"message": "Ontvangerstatus synchroniseren met media-element",
|
||||
"description": "Media casting sync checkbox label."
|
||||
|
||||
@@ -97,17 +97,6 @@
|
||||
}
|
||||
|
||||
|
||||
, "mediaOverlayTitle": {
|
||||
"message": "Spiller på $receiverName$"
|
||||
, "description": "Main title for overlay displayed on media elements whilst casting."
|
||||
, "placeholders": {
|
||||
"receiverName": {
|
||||
"content": "$1"
|
||||
, "example": "Living Room TV"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, "optionsBridgeLoading": {
|
||||
"message": "Laster bro-info"
|
||||
, "description": "Loading placeholder text for bridge section on options page."
|
||||
@@ -230,18 +219,6 @@
|
||||
"message": "Skru på media-casting"
|
||||
, "description": "Media casting enabled checkbox label."
|
||||
}
|
||||
, "optionsMediaOverlayEnabled": {
|
||||
"message": "Skru på element-overlegg (eksperimentell)"
|
||||
, "description": "Media element overlay checkbox label."
|
||||
}
|
||||
, "optionsMediaOverlayEnabledTemp": {
|
||||
"message": "Skru på element-overlegg (eksperimentell)"
|
||||
, "description": "Experimental-labelled version of above."
|
||||
}
|
||||
, "optionsMediaOverlayEnabledDescription": {
|
||||
"message": "mediaelementer "
|
||||
, "description": "Media element overlay option description."
|
||||
}
|
||||
, "optionsMediaSyncElement": {
|
||||
"message": "Synkroniser mottager med mediaelement"
|
||||
, "description": "Media casting sync checkbox label."
|
||||
|
||||
@@ -41,48 +41,6 @@ browser.runtime.onInstalled.addListener(async details => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets up media overlay content script and handles toggling
|
||||
* on options change.
|
||||
*/
|
||||
async function initMediaOverlay() {
|
||||
logger.info("init (media overlay)");
|
||||
|
||||
let contentScript: browser.contentScripts.RegisteredContentScript;
|
||||
|
||||
async function registerMediaOverlayContentScript() {
|
||||
if (!(await options.get("mediaOverlayEnabled"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
contentScript = await browser.contentScripts.register({
|
||||
allFrames: true,
|
||||
js: [{ file: "senders/media/overlay/overlayContentLoader.js" }],
|
||||
matches: ["<all_urls>"],
|
||||
runAt: "document_start"
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Failed to register media overlay");
|
||||
}
|
||||
}
|
||||
|
||||
async function unregisterMediaOverlayContentScript() {
|
||||
await contentScript?.unregister();
|
||||
}
|
||||
|
||||
registerMediaOverlayContentScript();
|
||||
|
||||
// Update if toggled
|
||||
options.addEventListener("changed", async ev => {
|
||||
const alteredOpts = ev.detail;
|
||||
|
||||
if (alteredOpts.includes("mediaOverlayEnabled")) {
|
||||
await unregisterMediaOverlayContentScript();
|
||||
await registerMediaOverlayContentScript();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bridge can be reached and is compatible
|
||||
@@ -153,7 +111,6 @@ async function init() {
|
||||
|
||||
await initMenus();
|
||||
await initWhitelist();
|
||||
await initMediaOverlay();
|
||||
|
||||
/**
|
||||
* When the browser action is clicked, open a receiver
|
||||
|
||||
@@ -8,7 +8,6 @@ export default {
|
||||
bridgeBackupHost: "localhost",
|
||||
bridgeBackupPort: 9556,
|
||||
mediaEnabled: true,
|
||||
mediaOverlayEnabled: false,
|
||||
mediaSyncElement: false,
|
||||
mediaStopOnUnload: false,
|
||||
localMediaEnabled: true,
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface Options {
|
||||
bridgeBackupHost: string;
|
||||
bridgeBackupPort: number;
|
||||
mediaEnabled: boolean;
|
||||
mediaOverlayEnabled: boolean;
|
||||
mediaSyncElement: boolean;
|
||||
mediaStopOnUnload: boolean;
|
||||
localMediaEnabled: boolean;
|
||||
|
||||
@@ -66,8 +66,5 @@
|
||||
]
|
||||
, "web_accessible_resources": [
|
||||
"cast/index.js"
|
||||
, "senders/media/overlay/overlayContent.js"
|
||||
, "senders/media/overlay/AirPlay_Audio.svg"
|
||||
, "senders/media/overlay/AirPlay_Video.svg"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -362,10 +362,6 @@ export async function init(opts: InitOptions) {
|
||||
|
||||
if (targetElement instanceof HTMLMediaElement) {
|
||||
registerMediaElementListeners(targetElement);
|
||||
|
||||
if (await options.get("mediaOverlayEnabled")) {
|
||||
// TODO: Un-hide overlay here
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", async () => {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1016 B |
@@ -1,3 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 749 B |
@@ -1,75 +0,0 @@
|
||||
"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: any[]
|
||||
): 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
|
||||
};
|
||||
}
|
||||
@@ -1,418 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import logger from "../../../lib/logger";
|
||||
|
||||
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;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
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;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
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(
|
||||
createElementNS,
|
||||
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) {
|
||||
logger.error("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) {
|
||||
logger.error("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 [...mediaElements, ...deepMediaElements]) {
|
||||
wrapMediaElement(mediaElement as HTMLMediaElement);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
"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);
|
||||
}
|
||||
@@ -189,21 +189,6 @@ class OptionsApp extends Component<
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="option option--inline">
|
||||
<div className="option__control">
|
||||
<input name="mediaOverlayEnabled"
|
||||
type="checkbox"
|
||||
checked={ this.state.options?.mediaOverlayEnabled }
|
||||
onChange={ this.handleInputChange } />
|
||||
</div>
|
||||
<div className="option__label">
|
||||
{ _("optionsMediaOverlayEnabledTemp") }
|
||||
</div>
|
||||
<div className="option__description">
|
||||
{ _("optionsMediaOverlayEnabledDescription") }
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<hr />
|
||||
|
||||
<label className="option option--inline">
|
||||
|
||||
Reference in New Issue
Block a user