Allow mediaCast sender to run in background context

This commit is contained in:
hensm
2019-07-27 07:56:43 +01:00
parent 79fd72022b
commit 9e196465e5
12 changed files with 527 additions and 523 deletions

View File

@@ -276,11 +276,9 @@ function handleMediaServerMessage (message: Message) {
sendMessage("mediaCast:/mediaServer/started");
});
mediaServer.on("close", () => {
console.error("mediaServer close");
sendMessage("mediaCast:/mediaServer/stopped");
});
mediaServer.on("error", (a) => {
console.error("mediaServer error", a);
sendMessage("mediaCast:/mediaServer/error");
});

View File

@@ -41,8 +41,8 @@ async function getSelection (
ReceiverSelectorMediaType.Tab
, availableMediaTypes =
ReceiverSelectorMediaType.Tab
| ReceiverSelectorMediaType.Screen)
// | ReceiverSelectorMediaType.File)
| ReceiverSelectorMediaType.Screen
| ReceiverSelectorMediaType.File)
: Promise<ReceiverSelection> {
return new Promise(async (resolve, reject) => {

View File

@@ -13,151 +13,218 @@ import SelectorManager from "./SelectorManager";
import StatusManager from "./StatusManager";
type Port = browser.runtime.Port | MessagePort;
export interface Shim {
bridgePort: browser.runtime.Port;
contentPort?: browser.runtime.Port;
contentPort: Port;
contentTabId?: number;
contentFrameId?: number;
}
export default async function createShim (
port: browser.runtime.Port): Promise<Shim> {
const contentPort = port;
const contentTabId = port.sender.tab.id;
const contentFrameId = port.sender.frameId;
const activeShims = new Set<Shim>();
const bridgePort = await bridge.connect();
/**
* If either the bridge port or the content port disconnects,
* just teardown all communication.
*/
function onDisconnect () {
bridgePort.onMessage.removeListener(onBridgePortMessage);
contentPort.onMessage.removeListener(onContentPortMessage);
// Ensure all ports are disconnected
contentPort.disconnect();
bridgePort.disconnect();
StatusManager.addEventListener("serviceUp", ev => {
for (const shim of activeShims) {
shim.contentPort.postMessage({
subject: "shim:/serviceUp"
, data: { id: ev.detail.id }
});
}
});
bridgePort.onDisconnect.addListener(onDisconnect);
contentPort.onDisconnect.addListener(onDisconnect);
// Add listeners
bridgePort.onMessage.addListener(onBridgePortMessage);
contentPort.onMessage.addListener(onContentPortMessage);
function onBridgePortMessage (message: Message) {
contentPort.postMessage(message);
}
async function onContentPortMessage (message: Message) {
const [ destination ] = message.subject.split(":/");
if (destination === "bridge") {
bridgePort.postMessage(message);
}
switch (message.subject) {
case "main:/shimInitialized": {
for (const receiver of StatusManager.getReceivers()) {
contentPort.postMessage({
subject: "shim:/serviceUp"
, data: { id: receiver.id }
});
}
break;
}
case "main:/selectReceiverBegin": {
const allMediaTypes =
ReceiverSelectorMediaType.App
| ReceiverSelectorMediaType.Tab
| ReceiverSelectorMediaType.Screen
| ReceiverSelectorMediaType.File;
try {
const selection = await SelectorManager.getSelection(
ReceiverSelectorMediaType.App
, allMediaTypes);
// Handle cancellation
if (!selection) {
contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
break;
}
/**
* If the media type returned from the selector has been
* changed, we need to cancel the current sender and switch
* it out for the right one.
*/
if (selection.mediaType !== ReceiverSelectorMediaType.App) {
contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
loadSender({
tabId: contentTabId
, frameId: contentFrameId
, selection
});
break;
}
// Pass selection back to shim
contentPort.postMessage({
subject: "shim:/selectReceiverEnd"
, data: selection
});
} catch (err) {
// TODO: Report errors properly
contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
}
break;
}
/**
* TODO: If we're closing a selector, make sure it's the
* same one that caused the session creation.
*/
case "main:/sessionCreated": {
const selector = await SelectorManager.getSharedSelector();
const shouldClose = await options.get(
"receiverSelectorWaitForConnection");
if (selector.isOpen && shouldClose) {
selector.close();
}
break;
}
}
StatusManager.addEventListener("serviceDown", ev => {
for (const shim of activeShims) {
shim.contentPort.postMessage({
subject: "shim:/serviceDown"
, data: { id: ev.detail.id }
});
}
});
contentPort.postMessage({
async function createShim (port: Port): Promise<void> {
const shim = await (port instanceof MessagePort
? createShimFromBackground(port)
: createShimFromContent(port));
shim.contentPort.postMessage({
subject: "shim:/initialized"
, data: await bridge.getInfo()
});
return {
bridgePort
, contentPort
, contentTabId
, contentFrameId
};
activeShims.add(shim);
}
async function createShimFromBackground (
contentPort: MessagePort): Promise<Shim> {
const shim: Shim = {
bridgePort: await bridge.connect()
, contentPort
};
shim.bridgePort.onDisconnect.addListener(() => {
contentPort.close();
activeShims.delete(shim);
});
shim.bridgePort.onMessage.addListener((message: Message) => {
contentPort.postMessage(message);
});
contentPort.onmessage = ev => {
const message = ev.data as Message;
handleContentMessage(shim, message);
};
return shim;
}
async function createShimFromContent (
contentPort: browser.runtime.Port): Promise<Shim> {
/**
* If there's already an active shim for the sender
* tab/frame ID, disconnect it.
*/
for (const activeShim of activeShims) {
if (activeShim.contentTabId === contentPort.sender.tab.id
&& activeShim.contentFrameId === contentPort.sender.frameId) {
activeShim.bridgePort.disconnect();
}
}
const shim: Shim = {
bridgePort: await bridge.connect()
, contentPort
, contentTabId: contentPort.sender.tab.id
, contentFrameId: contentPort.sender.frameId
};
function onContentPortMessage (message: Message) {
handleContentMessage(shim, message);
}
function onBridgePortMessage (message: Message) {
contentPort.postMessage(message);
}
function onDisconnect () {
shim.bridgePort.onMessage.removeListener(onBridgePortMessage);
contentPort.onMessage.removeListener(onContentPortMessage);
shim.bridgePort.disconnect();
contentPort.disconnect();
activeShims.delete(shim);
}
shim.bridgePort.onDisconnect.addListener(onDisconnect);
shim.bridgePort.onMessage.addListener(onBridgePortMessage);
contentPort.onDisconnect.addListener(onDisconnect);
contentPort.onMessage.addListener(onContentPortMessage);
return shim;
}
async function handleContentMessage (shim: Shim, message: Message) {
const [ destination ] = message.subject.split(":/");
if (destination === "bridge") {
shim.bridgePort.postMessage(message);
}
switch (message.subject) {
case "main:/shimInitialized": {
for (const receiver of StatusManager.getReceivers()) {
shim.contentPort.postMessage({
subject: "shim:/serviceUp"
, data: { id: receiver.id }
});
}
break;
}
case "main:/selectReceiverBegin": {
const allMediaTypes =
ReceiverSelectorMediaType.App
| ReceiverSelectorMediaType.Tab
| ReceiverSelectorMediaType.Screen
| ReceiverSelectorMediaType.File;
try {
const selection = await SelectorManager.getSelection(
ReceiverSelectorMediaType.App
, allMediaTypes);
// Handle cancellation
if (!selection) {
shim.contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
break;
}
/**
* If the media type returned from the selector has been
* changed, we need to cancel the current sender and switch
* it out for the right one.
*/
if (selection.mediaType !== ReceiverSelectorMediaType.App) {
shim.contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
loadSender({
tabId: shim.contentTabId
, frameId: shim.contentFrameId
, selection
});
break;
}
// Pass selection back to shim
shim.contentPort.postMessage({
subject: "shim:/selectReceiverEnd"
, data: selection
});
} catch (err) {
// TODO: Report errors properly
shim.contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
}
break;
}
/**
* TODO: If we're closing a selector, make sure it's the
* same one that caused the session creation.
*/
case "main:/sessionCreated": {
const selector = await SelectorManager.getSharedSelector();
const shouldClose = await options.get(
"receiverSelectorWaitForConnection");
if (selector.isOpen && shouldClose) {
selector.close();
}
break;
}
}
}
export default createShim;

View File

@@ -15,7 +15,7 @@ export default {
, mirroringAppId: MIRRORING_APP_ID
, receiverSelectorType: ReceiverSelectorType.Popup
, receiverSelectorCloseIfFocusLost: true
, receiverSelectorWaitForConnection: false
, receiverSelectorWaitForConnection: true
, userAgentWhitelistEnabled: true
, userAgentWhitelist: [
"https://www.netflix.com/*"

View File

@@ -1,7 +1,5 @@
"use strict";
import mediaCasting from "./mediaCasting";
import { stringify } from "./utils";
import { ReceiverSelection
@@ -45,8 +43,12 @@ export default async function loadSender (opts: LoadSenderOptions) {
case ReceiverSelectorMediaType.File: {
const fileUrl = new URL(`file://${opts.selection.filePath}`);
const mediaSession = await mediaCasting.loadMediaUrl(
fileUrl.href, opts.selection.receiver);
const { init } = await import("../senders/mediaCast");
init({
mediaUrl: fileUrl.href
, receiver: opts.selection.receiver
});
break;
}

View File

@@ -1,81 +0,0 @@
"use strict";
import cast, { ensureInit } from "../shim/export";
import options from "./options";
import { Receiver } from "../types";
function getMediaSession (
receiver?: Receiver): Promise<cast.Session> {
return new Promise(async (resolve, reject) => {
await ensureInit();
/**
* 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 (receiver) {
cast._requestSession(receiver, resolve, reject);
} else {
cast.requestSession(resolve, reject);
}
}
}
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 loadMediaUrl (
mediaUrl: string
, receiver: Receiver): Promise<cast.Session> {
return new Promise(async (resolve, reject) => {
const isLocalMedia = mediaUrl.startsWith("file://");
const isLocalMediaEnabled = await options.get("localMediaEnabled");
if (isLocalMedia && !isLocalMediaEnabled) {
console.error("fx_cast (Debug): Local media casting not enabled");
return;
}
const mediaUrlObject = new URL(mediaUrl);
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 = mediaUrlObject.pathname;
const mediaSession = await getMediaSession(receiver);
const loadRequest = new cast.media.LoadRequest(mediaInfo);
loadRequest.autoplay = false;
mediaSession.loadMedia(loadRequest
, null // successCallback
, () => { reject(); }); // errorCallback
});
}
export default {
getMediaSession
, loadMediaUrl
};

View File

@@ -3,7 +3,6 @@
import defaultOptions from "./defaultOptions";
import bridge from "./lib/bridge";
import loadSender from "./lib/loadSender";
import mediaCasting from "./lib/mediaCasting";
import options, { Options } from "./lib/options";
import { getChromeUserAgent } from "./lib/userAgents";
@@ -49,6 +48,17 @@ browser.runtime.onInstalled.addListener(async details => {
});
/**
* When a message port connection with the name "shim" is
* established, pass it to createShim to handle the setup
* and maintenance.
*/
browser.runtime.onConnect.addListener(async port => {
if (port.name === "shim") {
createShim(port);
}
});
/**
* When the browser action is clicked, open a receiver
* selector and load a sender for the response. The
@@ -66,61 +76,6 @@ browser.browserAction.onClicked.addListener(async tab => {
});
const activeShims = new Set<Shim>();
browser.runtime.onConnect.addListener(async port => {
if (port.name === "shim") {
/**
* If there's already an active shim for the sender
* tab/frame ID, disconnect it.
*/
for (const activeShim of activeShims) {
if (activeShim.contentTabId === port.sender.tab.id
&& activeShim.contentFrameId === port.sender.frameId) {
activeShim.contentPort.disconnect();
activeShim.bridgePort.disconnect();
}
}
const shim = await createShim(port);
shim.bridgePort.onDisconnect.addListener(() => {
activeShims.delete(shim);
});
shim.contentPort.onDisconnect.addListener(() => {
activeShims.delete(shim);
});
activeShims.add(shim);
}
});
StatusManager.addEventListener("serviceUp", ev => {
for (const shim of activeShims) {
shim.contentPort.postMessage({
subject: "shim:/serviceUp"
, data: { id: ev.detail.id }
});
}
});
StatusManager.addEventListener("serviceDown", ev => {
for (const shim of activeShims) {
shim.contentPort.postMessage({
subject: "shim:/serviceDown"
, data: { id: ev.detail.id }
});
}
});
let mediaCastTabId: number;
let mediaCastFrameId: number;
async function initMenus () {
console.info("fx_cast (Debug): init (menus)");
@@ -135,8 +90,8 @@ async function initMenus () {
const allMediaTypes =
ReceiverSelectorMediaType.App
| ReceiverSelectorMediaType.Tab
| ReceiverSelectorMediaType.Screen;
// | ReceiverSelectorMediaType.File;
| ReceiverSelectorMediaType.Screen
| ReceiverSelectorMediaType.File;
const selection = await SelectorManager.getSelection(
ReceiverSelectorMediaType.App
@@ -154,8 +109,8 @@ async function initMenus () {
if (selection.mediaType === ReceiverSelectorMediaType.App) {
await browser.tabs.executeScript(tab.id, {
code: stringify`
window.selectedReceiver = ${selection.receiver};
window.srcUrl = ${info.srcUrl};
window.receiver = ${selection.receiver};
window.mediaUrl = ${info.srcUrl};
window.targetElementId = ${info.targetElementId};
`
, frameId: info.frameId
@@ -165,10 +120,6 @@ async function initMenus () {
file: "senders/mediaCast.js"
, frameId: info.frameId
});
// Store for later
mediaCastTabId = tab.id;
mediaCastFrameId = info.frameId;
} else {
// Handle other responses

View File

@@ -1,44 +1,11 @@
"use strict";
import mediaCasting from "../lib/mediaCasting";
import options from "../lib/options";
import cast, { ensureInit } from "../shim/export";
import { Message, Receiver } from "../types";
// Variables passed from background
const { selectedReceiver
, srcUrl
, targetElementId }
: { selectedReceiver: Receiver
, srcUrl: string
, targetElementId: number } = (window as any);
let backgroundPort: browser.runtime.Port;
let session: cast.Session;
let currentMedia: cast.media.Media;
let ignoreMediaEvents = false;
const isLocalFile = srcUrl.startsWith("file:");
const mediaElement = browser.menus.getTargetElement(
targetElementId) as HTMLMediaElement;
window.addEventListener("beforeunload", async () => {
backgroundPort.postMessage({
subject: "bridge:/mediaServer/stop"
});
if (await options.get("mediaStopOnUnload")) {
session.stop(null, null);
}
});
function getLocalAddress () {
const pc = new RTCPeerConnection();
pc.createDataChannel(null);
@@ -56,7 +23,6 @@ function getLocalAddress () {
});
}
function startMediaServer (filePath: string, port: number) {
return new Promise((resolve, reject) => {
backgroundPort.postMessage({
@@ -67,177 +33,235 @@ function startMediaServer (filePath: string, port: number) {
}
});
backgroundPort.onMessage.addListener(
function onMessage (message: Message) {
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": {
backgroundPort.onMessage.removeListener(onMessage);
resolve();
break;
}
case "mediaCast:/mediaServer/error": {
backgroundPort.onMessage.removeListener(onMessage);
reject();
break;
}
}
});
backgroundPort.start();
});
}
async function loadMedia () {
let mediaUrl = new URL(srcUrl);
const mediaTitle = mediaUrl.pathname;
/**
* If the media is a local file, start an HTTP media server
* and change the media URL to point to it.
*/
if (isLocalFile) {
const host = await getLocalAddress();
const port = await options.get("localMediaServerPort");
let backgroundPort: MessagePort;
try {
// Wait until media server is listening
await startMediaServer(mediaUrl.pathname, port);
} catch (err) {
console.error("Failed to start media server");
return;
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);
}
}
}
mediaUrl = new URL(`http://${host}:${port}/`);
}
const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, null);
// Media metadata (title/poster)
mediaInfo.metadata = new cast.media.GenericMediaMetadata();
mediaInfo.metadata.metadataType = cast.media.MetadataType.GENERIC;
mediaInfo.metadata.title = mediaTitle;
if (mediaElement instanceof HTMLVideoElement && mediaElement.poster) {
mediaInfo.metadata.images = [
new cast.Image(mediaElement.poster)
];
}
const activeTrackIds = [];
if (mediaElement.textTracks.length) {
const trackElements = mediaElement.querySelectorAll("track");
let index = 0;
for (const textTrack of Array.from(mediaElement.textTracks)) {
const trackElement = trackElements[index];
// Create Track object
const track = new cast.media.Track(
index // trackId
, cast.media.TrackType.TEXT); // trackType
// Copy TextTrack properties to Track
track.name = textTrack.label;
track.language = textTrack.language;
track.trackContentId = trackElement.src;
track.trackContentType = "text/vtt";
const { TextTrackType } = cast.media;
switch (textTrack.kind) {
case "subtitles":
track.subtype = TextTrackType.SUBTITLES;
break;
case "captions":
track.subtype = TextTrackType.CAPTIONS;
break;
case "descriptions":
track.subtype = TextTrackType.DESCRIPTIONS;
break;
case "chapters":
track.subtype = TextTrackType.CHAPTERS;
break;
case "metadata":
track.subtype = TextTrackType.METADATA;
break;
// Default to subtitles
default:
track.subtype = TextTrackType.SUBTITLES;
}
// Add track to mediaInfo
mediaInfo.tracks.push(track);
// If enabled, set as active track for load request
if (textTrack.mode === "showing" || trackElement.default) {
activeTrackIds.push(index);
}
index++;
function onRequestSessionSuccess (session: cast.Session) {
resolve(session);
}
function onRequestSessionError (err: cast.Error) {
reject(err.description);
}
}
const loadRequest = new cast.media.LoadRequest(mediaInfo);
loadRequest.autoplay = false;
loadRequest.activeTrackIds = activeTrackIds;
session.loadMedia(loadRequest
, onLoadMediaSuccess
, onLoadMediaError);
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, reject) => {
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);
});
}
async function onLoadMediaSuccess (media: cast.media.Media) {
cast.logMessage("onLoadMediaSuccess");
currentMedia = media;
let ignoreMediaEvents = false;
async function registerMediaElementListeners () {
if (await options.get("mediaSyncElement")) {
mediaElement.addEventListener("play", () => {
function checkIgnore (ev: Event) {
if (ignoreMediaEvents) {
ignoreMediaEvents = false;
return;
ev.stopImmediatePropagation();
}
}
currentMedia.play(null
, onMediaPlaySuccess
, onMediaPlayError);
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", () => {
if (ignoreMediaEvents) {
ignoreMediaEvents = false;
return;
}
currentMedia.pause(null
, onMediaPauseSuccess
, onMediaPauseError);
currentMedia.pause(null, null, null);
});
mediaElement.addEventListener("suspend", () => {
/*currentMedia.stop(null
, onMediaStopSuccess
, onMediaStopError);*/
// currentMedia.stop(null, null, null);
});
mediaElement.addEventListener("seeking", () => {
if (ignoreMediaEvents) {
ignoreMediaEvents = false;
return;
}
mediaElement.addEventListener("seeked", () => {
const seekRequest = new cast.media.SeekRequest();
seekRequest.currentTime = mediaElement.currentTime;
currentMedia.seek(seekRequest
, onMediaSeekSuccess
, onMediaSeekError);
currentMedia.seek(seekRequest, null, null);
});
mediaElement.addEventListener("ratechange", () => {
(currentMedia as any)._sendMediaMessage({
currentMedia._sendMediaMessage({
type: "SET_PLAYBACK_RATE"
, playbackRate: mediaElement.playbackRate
});
@@ -248,10 +272,8 @@ async function onLoadMediaSuccess (media: cast.media.Media) {
currentMedia.volume.level
, currentMedia.volume.muted);
const volumeRequest =
new cast.media.VolumeRequest(newVolume);
const volumeRequest = new cast.media.VolumeRequest(newVolume);
cast.logMessage("Volume change");
currentMedia.setVolume(volumeRequest);
});
@@ -261,44 +283,46 @@ async function onLoadMediaSuccess (media: cast.media.Media) {
return;
}
// PlayerState
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:
case cast.media.PlayerState.PLAYING: {
mediaElement.play();
break;
case cast.media.PlayerState.PAUSED:
}
case cast.media.PlayerState.PAUSED: {
mediaElement.pause();
break;
}
}
}
// RepeatMode
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:
case cast.media.RepeatMode.SINGLE: {
mediaElement.loop = true;
break;
case cast.media.RepeatMode.OFF:
}
case cast.media.RepeatMode.OFF: {
mediaElement.loop = false;
break;
}
}
}
// currentTime
if (currentMedia.currentTime !== mediaElement.currentTime) {
ignoreMediaEvents = true;
mediaElement.currentTime = currentMedia.currentTime;
@@ -307,57 +331,57 @@ async function onLoadMediaSuccess (media: cast.media.Media) {
}
}
function onRequestSessionError () {
cast.logMessage("onRequestSessionError");
}
function sessionListener (newSession: cast.Session) {
cast.logMessage("sessionListener");
}
function onInitializeSuccess () {
cast.logMessage("onInitializeSuccess");
}
function onInitializeError () {
cast.logMessage("onInitializeError");
}
function onLoadMediaError () {
cast.logMessage("onLoadMediaError");
}
function onMediaPlaySuccess () {
cast.logMessage("onMediaPlaySuccess");
}
function onMediaPlayError (err: cast.Error) {
cast.logMessage("onMediaPlayError");
}
function onMediaPauseSuccess () {
cast.logMessage("onMediaPauseSuccess");
}
function onMediaPauseError (err: cast.Error) {
cast.logMessage("onMediaPauseError");
}
function onMediaStopSuccess () {
cast.logMessage("onMediaStopSuccess");
}
function onMediaStopError (err: cast.Error) {
cast.logMessage("onMediaStopError");
}
function onMediaSeekSuccess () {
cast.logMessage("onMediaSeekSuccess");
}
function onMediaSeekError (err: cast.Error) {
cast.logMessage("onMediaSeekError");
interface InitOptions {
mediaUrl: string;
receiver: Receiver;
targetElementId?: number;
}
export async function init (opts: InitOptions) {
backgroundPort = await ensureInit();
ensureInit().then(async (port) => {
backgroundPort = port;
const isLocalMedia = opts.mediaUrl.startsWith("file://");
const isLocalMediaEnabled = await options.get("localMediaEnabled");
if (isLocalFile && !isLocalMediaEnabled) {
if (isLocalMedia && !isLocalMediaEnabled) {
cast.logMessage("Local media casting not enabled");
return;
}
session = await mediaCasting.getMediaSession(selectedReceiver);
if (opts.targetElementId) {
mediaElement = browser.menus.getTargetElement(
opts.targetElementId) as HTMLMediaElement;
}
loadMedia();
});
currentSession = await getSession(opts);
currentMedia = await getMedia(opts);
if (opts.targetElementId) {
registerMediaElementListeners();
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

@@ -16,6 +16,8 @@ import { ErrorCode
, SessionStatus
, VolumeControlType } from "../enums";
import { RepeatMode } from "../media/enums";
import { ListenerObject
, onMessage
, sendMessageResponse } from "../../eventMessageChannel";
@@ -305,9 +307,10 @@ export default class Session {
, autoplay: loadRequest.autoplay || false
, currentTime: loadRequest.currentTime || 0
, customData: loadRequest.customData || {}
, repeatMode: "REPEAT_OFF"
, repeatMode: RepeatMode.OFF
});
let hasResponded = false;
this.addMessageListener(
@@ -318,23 +321,28 @@ export default class Session {
return;
}
const mediaObject = JSON.parse(data);
const message = JSON.parse(data);
if (mediaObject.status && mediaObject.status.length > 0) {
if (message.status && message.status.length > 0) {
hasResponded = true;
const media = new Media(
this.sessionId
, mediaObject.status[0].mediaSessionId
, message.status[0].mediaSessionId
, _id.get(this));
media.media = loadRequest.media;
this.media = [ media ];
media.play();
successCallback(media);
if (successCallback) {
successCallback(media);
}
} else {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
if (errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
}
}
});
}

View File

@@ -210,9 +210,6 @@ export function _requestSession (
sessionRequestInProgress = true;
sessionSuccessCallback = successCallback;
sessionErrorCallback = errorCallback;
const selectedReceiver = new Receiver_(
_receiver.id
@@ -235,8 +232,8 @@ export function _requestSession (
sessionRequestInProgress = false;
if (sessionSuccessCallback) {
sessionSuccessCallback(session);
if (successCallback) {
successCallback(session);
}
}));
}

View File

@@ -293,7 +293,7 @@ export default class Media {
}
private _sendMediaMessage (
public _sendMediaMessage (
message: any
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {

View File

@@ -4,11 +4,12 @@ import * as cast from "./cast";
import { BridgeInfo } from "../lib/bridge";
import { Message } from "../types";
import { onMessage } from "./eventMessageChannel";
import { onMessage, onMessageResponse, sendMessage } from "./eventMessageChannel";
let initializedBridgeInfo: BridgeInfo;
let initializedBackgroundPort: browser.runtime.Port;
let initializedBackgroundPort: MessagePort;
/**
* To support exporting an API from a module, we need to
@@ -17,7 +18,7 @@ let initializedBackgroundPort: browser.runtime.Port;
* for and emits these messages, and changing that behavior
* is too messy.
*/
export function ensureInit (): Promise<browser.runtime.Port> {
export function ensureInit (): Promise<MessagePort> {
return new Promise(async (resolve, reject) => {
// If already initialized, just return existing bridge info
@@ -31,6 +32,9 @@ export function ensureInit (): Promise<browser.runtime.Port> {
return;
}
const channel = new MessageChannel();
initializedBackgroundPort = channel.port1;
/**
* If the module is imported into a background script
* context, the location will be the internal extension URL,
@@ -38,14 +42,48 @@ export function ensureInit (): Promise<browser.runtime.Port> {
* URL.
*/
if (window.location.protocol === "moz-extension:") {
//
const { default: createShim } = await import("../createShim");
// port2 will post bridge messages to port 1
await createShim(channel.port2);
// bridge -> shim
channel.port1.onmessage = ev => {
const message = ev.data as Message;
// Send message to shim
sendMessage(message);
handleIncomingMessageToShim(message);
};
// shim -> bridge
onMessageResponse(message => {
channel.port1.postMessage(message);
});
} else {
// Trigger message port setup side-effects
/**
* Import reference to message port created by contentBridge.
* Creation of the port triggers side-effects in the
* background script.
*/
const { backgroundPort } = await import("./contentBridge");
initializedBackgroundPort = backgroundPort;
// backgroundPort -> channel.port2
backgroundPort.onMessage.addListener((message: Message) => {
channel.port2.postMessage(message);
});
// channel.port2 -> backgroundPort
channel.port2.onmessage = ev => {
const message = ev.data as Message;
backgroundPort.postMessage(message);
};
// Handle shim messages
onMessage(handleIncomingMessageToShim);
}
onMessage(message => {
function handleIncomingMessageToShim (message: Message) {
switch (message.subject) {
case "shim:/initialized": {
initializedBridgeInfo = message.data;
@@ -57,7 +95,7 @@ export function ensureInit (): Promise<browser.runtime.Port> {
}
}
}
});
}
});
}