Initial commit

This commit is contained in:
hensm
2018-06-08 04:56:02 +01:00
commit d815fb7af0
70 changed files with 8370 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
{
"extension_name": {
"message": "Caster"
}
, "extension_description": {
"message": ""
}
, "popup_cast_button_label": {
"message": "Cast"
}
, "popup_casting_button_label": {
"message": "Casting"
}
, "context_media_cast": {
"message": "Cast..."
}
}

19
ext/src/compat/youtube.js Normal file
View File

@@ -0,0 +1,19 @@
"use strict";
function injectScript (url) {
const script = document.createElement("script");
script.src = url;
script.addEventListener("load", ev => {
script.remove();
});
document.documentElement.appendChild(script);
}
injectScript(browser.runtime.getURL("shim/bundle.js"));
//injectScript("https://s.ytimg.com/yts/jsbin/www-tampering-vflyYlECh/www-tampering.js");
//injectScript("https://s.ytimg.com/yts/jsbin/www-prepopulator-vfl8hLntF/www-prepopulator.js");
//injectScript("https://s.ytimg.com/yts/jsbin/webcomponents-lite.min-vfl2VqBkx/webcomponents-lite.min.js");
console.log(script);

12
ext/src/content.js Normal file
View File

@@ -0,0 +1,12 @@
"use strict";
document.addEventListener("__castMessageResponse", ev => {
browser.runtime.sendMessage(ev.detail);
})
browser.runtime.onMessage.addListener(message => {
const event = new CustomEvent("__castMessage", {
detail: JSON.stringify(message)
});
document.dispatchEvent(event);
});

3
ext/src/contentSetup.js Normal file
View File

@@ -0,0 +1,3 @@
"use strict";
window.wrappedJSObject.chrome = cloneInto({}, window);

11
ext/src/lib/utils.js Normal file
View File

@@ -0,0 +1,11 @@
"use strict";
export function cloneIntoWithProto (obj, destination) {
const ret = cloneInto(obj, destination);
for (const key of Object.getOwnPropertyNames(obj.__proto__)) {
exportFunction(obj.__proto__[key].bind(obj), ret, { defineAs: key });
}
return ret;
}

211
ext/src/main.js Executable file
View File

@@ -0,0 +1,211 @@
"use strict";
const _ = browser.i18n.getMessage;
// Google-hosted API loader script
const SENDER_SCRIPT_URL =
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js";
const SENDER_SCRIPT_FRAMEWORK_URL =
`${SENDER_SCRIPT_URL}?loadCastFramework=1`;
/**
* Sender applications load a cast_sender.js script that
* functions as a loader for the internal chrome-extension:
* hosted script.
*
* We can redirect this and inject our own script to setup
* the API shim.
*/
browser.webRequest.onBeforeRequest.addListener(
async details => {
console.log(details);
switch (details.url) {
case SENDER_SCRIPT_URL:
// Content/Page script bridge
await browser.tabs.executeScript(details.tabId, {
file: "content.js"
, frameId: details.frameId
, runAt: "document_start"
});
return {
redirectUrl: browser.runtime.getURL("shim/bundle.js")
};
case SENDER_SCRIPT_FRAMEWORK_URL:
// TODO: implement cast.framework
return {
cancel: true
};
}
}
, { urls: [
SENDER_SCRIPT_URL
, SENDER_SCRIPT_FRAMEWORK_URL
]}
, [ "blocking" ]);
// Defines window.chrome for site compatibility
browser.contentScripts.register({
allFrames: true
, js: [{ file: "contentSetup.js" }]
, matches: [ "<all_urls>" ]
, runAt: "document_start"
});
// YouTube compat shim
browser.contentScripts.register({
allFrames: true
, js: [{ file: "compat/youtube.js" }]
, matches: [ "*://www.youtube.com/*" ]
, runAt: "document_start"
});
// <video>/<audio> "Cast..." context menu item
browser.menus.create({
contexts: [ "audio", "video" ]
, id: "contextCastMedia"
, targetUrlPatterns: [
"http://*/*"
, "https://*/*"
]
, title: _("context_media_cast")
});
browser.menus.onClicked.addListener(async (info, tab) => {
const { frameId } = info;
// Pass media URL to media sender app
await browser.tabs.executeScript(tab.id, {
code: `const srcUrl = "${info.srcUrl}";`
, frameId
});
// Load app and sender API shim
await browser.tabs.executeScript(tab.id, { file: "content.js" , frameId })
await browser.tabs.executeScript(tab.id, { file: "mediaCast.js" , frameId });
await browser.tabs.executeScript(tab.id, { file: "shim/bundle.js" , frameId });
});
const bridgeMap = new Map();
/**
* Initializes native application and handles message
* forwarding.
*/
function initBridge (tabId, frameId) {
const port = browser.runtime.connectNative("caster_bridge");
bridgeMap.set(tabId, port);
port.onMessage.addListener(message => {
// Forward shim: messages
if (message.subject.startsWith("shim:")) {
browser.tabs.sendMessage(tabId, message, {
frameId
});
return;
}
});
}
let popupTabId;
let popupOpenerTabId;
/**
* Creates popup window for cast destination selection.
* Refocusing other browser windows causes the popup window
* to close and returns an API error (TODO).
*/
async function openPopup (tabId) {
const width = 350;
const height = 200;
// Current window to base centered position on
const win = await browser.windows.getCurrent();
// Top(mid)-center position
const centerX = win.left + (win.width / 2);
const centerY = win.top + (win.height / 3);
const left = Math.floor(centerX - (width / 2));
const top = Math.floor(centerY - (height / 2));
const popup = await browser.windows.create({
url: "popup/index.html"
, type: "popup"
, width
, height
, left
, top
});
// Store popup details for message forwarding
popupTabId = popup.tabs[0].id;
popupOpenerTabId = tabId;
// Size/position not set correctly on creation (bug?)
await browser.windows.update(popup.id, {
width
, height
, left
, top
});
// Close popup on other browser window focus
browser.windows.onFocusChanged.addListener(function listener (id) {
if (id !== browser.windows.WINDOW_ID_NONE
&& id === win.id) {
browser.windows.onFocusChanged.removeListener(listener);
browser.windows.remove(popup.id);
}
});
}
// Extension messages
browser.runtime.onMessage.addListener(async (message, sender, respond) => {
const tabId = sender.tab.id;
const { frameId } = sender.tab;
// Forward bridge: messages
if (message.subject.startsWith("bridge:")) {
bridgeMap.get(tabId).postMessage(message);
return;
}
// Forward shim: messages
if (message.subject.startsWith("shim:")) {
browser.tabs.sendMessage(popupOpenerTabId, message, { frameId });
return;
}
// Forward popup messages
if (message.subject.startsWith("popup:")) {
if (popupTabId) {
try {
browser.tabs.sendMessage(popupTabId, message);
} catch (err) {
// Popup is closed
}
}
return;
}
switch (message.subject) {
case "main:initialize":
initBridge(tabId);
break;
case "main:openPopup": {
await openPopup(tabId);
break;
}
}
});

34
ext/src/manifest.json Executable file
View File

@@ -0,0 +1,34 @@
{
"name": "__MSG_extension_name__"
, "description": "__MSG_extension_description__"
, "version": "0.0.1"
, "applications": {
"gecko": {
"id": "caster@matt.tf"
, "strict_min_version": "57.0"
}
}
, "browser_action": {
"default_popup": "popup/index.html"
}
, "background": {
"scripts": [ "main.js" ]
}
, "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
, "default_locale": "en"
, "manifest_version": 2
, "permissions": [
"menus"
, "nativeMessaging"
, "webNavigation"
, "webRequest"
, "webRequestBlocking"
, "<all_urls>"
]
, "web_accessible_resources": [
"shim/bundle.js"
, "dm.js"
]
}

253
ext/src/mediaCast.js Normal file
View File

@@ -0,0 +1,253 @@
"use strict";
let chrome;
let logMessage;
let session;
let currentMedia;
let mediaElement = document.querySelector(`[src="${srcUrl}"]`);
// TODO: Fix this broken mess
let ignoreMediaEvents = false;
function silent (fn) {
ignoreMediaEvents = true;
fn();
}
mediaElement.addEventListener("play", () => {
if (ignoreMediaEvents) {
ignoreMediaEvents = false;
return;
}
currentMedia.play(null
, onMediaPlaySuccess
, onMediaPlayError);
});
mediaElement.addEventListener("pause", () => {
if (ignoreMediaEvents) {
ignoreMediaEvents = false;
return;
}
currentMedia.pause(null
, onMediaPauseSuccess
, onMediaPauseError);
});
mediaElement.addEventListener("suspend", () => {
if (ignoreMediaEvents) return;
/*currentMedia.stop(null
, onMediaStopSuccess
, onMediaStopError);*/
});
mediaElement.addEventListener("seeking", () => {
if (ignoreMediaEvents) {
ignoreMediaEvents = false;
return;
}
const seekRequest = new chrome.cast.media.SeekRequest();
seekRequest.currentTime = mediaElement.currentTime;
currentMedia.seek(seekRequest
, onMediaSeekSuccess
, onMediaSeekError);
});
mediaElement.addEventListener("ratechange", () => {
if (ignoreMediaEvents) {
ignoreMediaEvents = false;
return;
}
currentMedia._sendMediaMessage({
type: "SET_PLAYBACK_RATE"
, playbackRate: mediaElement.playbackRate
});
});
mediaElement.addEventListener("volumechange", () => {
if (ignoreMediaEvents) {
ignoreMediaEvents = false;
return;
}
const newVolume = new chrome.cast.Volume(
currentMedia.volume
, currentMedia.muted);
const volumeRequest = new chrome.cast.media.VolumeRequest(newVolume);
logMessage("Volume change");
currentMedia.setVolume(volumeRequest);
});
function onRequestSessionSuccess (session_) {
logMessage("onRequestSessionSuccess");
session = session_;
const mediaUrl = new URL(srcUrl);
const mediaInfo = new chrome.cast.media.MediaInfo(mediaUrl.href);
// Media metadata (title/poster)
mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata();
mediaInfo.metadata.metadataType = chrome.cast.media.MetadataType.GENERIC;
mediaInfo.metadata.title = mediaUrl.pathname;
if (mediaElement.poster) {
mediaInfo.metadata.images = [
new chrome.cast.Image(mediaElement.poster)
];
}
const loadRequest = new chrome.cast.media.LoadRequest(mediaInfo);
loadRequest.autoplay = false;
session.loadMedia(loadRequest
, onLoadMediaSuccess
, onLoadMediaError);
}
function onRequestSessionError () {
logMessage("onRequestSessionError");
}
function sessionListener (session) {
logMessage("sessionListener");
}
function receiverListener (availability) {
logMessage("receiverListener");
if (availability === chrome.cast.ReceiverAvailability.AVAILABLE) {
chrome.cast.requestSession(
onRequestSessionSuccess
, onRequestSessionError);
}
}
function onInitializeSuccess () {
logMessage("onInitializeSuccess");
}
function onInitializeError () {
logMessage("onInitializeError");
}
function onLoadMediaSuccess (media) {
logMessage("onLoadMediaSuccess");
currentMedia = media;
currentMedia.addUpdateListener(() => {
console.log(currentMedia);
// PlayerState
const localPlayerState = mediaElement.paused
? chrome.cast.media.PlayerState.PAUSED
: chrome.cast.media.PlayerState.PLAYING;
if (localPlayerState !== currentMedia.playerState) {
switch (currentMedia.playerState) {
case chrome.cast.media.PlayerState.PLAYING:
silent(() => {
mediaElement.play();
});
break;
case chrome.cast.media.PlayerState.PAUSED:
silent(() => {
mediaElement.pause();
});
break;
}
}
// RepeatMode
const localRepeatMode = mediaElement.loop
? chrome.cast.media.RepeatMode.SINGLE
: chrome.cast.media.RepeatMode.OFF;
if (localRepeatMode !== currentMedia.repeatMode) {
switch (currentMedia.repeatMode) {
case chrome.cast.media.RepeatMode.SINGLE:
mediaElement.loop = true;
break;
case chrome.cast.media.RepeatMode.OFF:
mediaElement.loop = false;
break;
}
}
// currentTime
if (currentMedia.currentTime !== mediaElement.currentTime) {
silent(() => {
mediaElement.currentTime = currentMedia.currentTime;
});
}
});
}
function onLoadMediaError () {
logMessage("onLoadMediaError");
}
/* play */
function onMediaPlaySuccess () {
logMessage("onMediaPlaySuccess");
}
function onMediaPlayError (err) {
logMessage("onMediaPlayError");
}
/* pause */
function onMediaPauseSuccess () {
logMessage("onMediaPauseSuccess");
}
function onMediaPauseError (err) {
logMessage("onMediaPauseError");
}
/* stop */
function onMediaStopSuccess () {
logMessage("onMediaStopSuccess");
}
function onMediaStopError (err) {
logMessage("onMediaStopError");
}
/* seek */
function onMediaSeekSuccess () {
logMessage("onMediaSeekSuccess");
}
function onMediaSeekError (err) {
logMessage("onMediaSeekError");
}
window.__onGCastApiAvailable = function (loaded, errorInfo) {
if (!loaded) {
logMessage("__onGCastApiAvailable error");
return;
}
chrome = window.chrome;
logMessage = chrome.cast.logMessage;
logMessage("__onGCastApiAvailable success");
const sessionRequest = new chrome.cast.SessionRequest(
chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
const apiConfig = new chrome.cast.ApiConfig(sessionRequest
, sessionListener
, receiverListener);
chrome.cast.initialize(apiConfig
, onInitializeSuccess
, onInitializeError);
};

52
ext/src/popup/index.css Executable file
View File

@@ -0,0 +1,52 @@
body {
background: -moz-dialog;
color: -moz-dialogtext;
margin: initial;
font: message-box;
}
.receivers {
list-style: none;
margin: initial;
padding: initial;
}
.receiver {
column-gap: 0.75em;
display: grid;
flex-direction: column;
grid-template-columns: 1fr min-content;
grid-template-rows: min-content min-content 1fr;
grid-template-areas:
"name connect"
"address connect"
". connect";
justify-content: center;
padding: 0.75em 1em;
position: relative;
}
.receiver:not(:last-child) {
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
}
.receiver-name,
.receiver-address {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.receiver-name {
font-size: 1.1em;
grid-area: name;
}
.receiver-address {
color: GrayText;
grid-area: address;
}
.receiver-connect {
grid-area: connect;
justify-self: end;
min-width: 100px;
}

11
ext/src/popup/index.html Executable file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="index.css">
<script src="bundle.js" defer></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

146
ext/src/popup/index.js Executable file
View File

@@ -0,0 +1,146 @@
"use strict";
import React, { Component } from "react";
import ReactDOM from "react-dom";
const _ = browser.i18n.getMessage;
let winWidth = 350;
let winHeight = 200;
let frameHeight;
let frameWidth;
class App extends Component {
constructor () {
super();
this.state = {
receivers: []
, isLoading: false
};
// Store window ref
browser.windows.getCurrent().then(win => {
this.win = win;
frameHeight = win.height - window.innerHeight;
frameWidth = win.width - window.innerWidth;
});
}
componentDidMount () {
browser.runtime.sendMessage({
subject: "shim:popupReady"
});
browser.runtime.onMessage.addListener(message => {
switch (message.subject) {
case "popup:populate":
this.setState({
receivers: message.data
});
winHeight = document.body.clientHeight + frameHeight;
browser.windows.update(this.win.id, {
height: winHeight
});
break;
case "popup:close":
window.close();
break;
}
});
}
onCast (receiver) {
this.setState({
isLoading: true
});
browser.runtime.sendMessage({
subject: "shim:selectReceiver"
, data: receiver
});
}
render () {
return (
<ul className="receivers">
{ this.state.receivers.map(receiver => {
return (
<Receiver receiver={receiver}
onCast={this.onCast.bind(this)}
isLoading={this.state.isLoading} />
);
})}
</ul>
);
}
}
class Receiver extends Component {
constructor () {
super();
this.state = {
isLoading: false
, ellipsis: ""
};
}
onClick () {
this.props.onCast(this.props.receiver);
this.setState({
isLoading: true
});
setInterval(() => {
this.setState({
ellipsis: do {
if (this.state.ellipsis === "") ".";
else if (this.state.ellipsis === ".") "..";
else if (this.state.ellipsis === "..") "...";
else if (this.state.ellipsis === "...") "";
}
});
}, 500);
}
render () {
return (
<li className="receiver">
<div className="receiver-name">
{ this.props.receiver.friendlyName }
</div>
<div className="receiver-address">
{ `${this.props.receiver._address}:${this.props.receiver._port}` }
</div>
<button className="receiver-connect"
onClick={this.onClick.bind(this)}
disabled={this.props.isLoading}>
{ do {
if (this.state.isLoading) {
_("popup_casting_button_label") +
(this.state.isLoading
? this.state.ellipsis
: "" )
} else {
_("popup_cast_button_label")
}
}}
</button>
</li>
);
}
}
ReactDOM.render(
<App />
, document.querySelector("#root"));

View File

@@ -0,0 +1,16 @@
"use strict";
export default class ApiConfig {
constructor (
sessionRequest
, sessionListener
, receiverListener
, opt_autoJoinPolicy
, opt_defaultActionPolicy) {
this.autoJoinPolicy
this.receiverListener = receiverListener;
this.sessionListener = sessionListener;
this.sessionRequest = sessionRequest;
}
};

View File

@@ -0,0 +1,12 @@
"use strict";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.DialRequest
export default class DialRequest {
constructor (
appName
, opt_launchParameter = null) {
this.appName = appName;
this.launchParameter = opt_launchParameter;
}
};

View File

@@ -0,0 +1,14 @@
"use strict";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Error
export default class Error {
constructor (
code
, opt_description = null
, opt_details = null) {
this.code = code;
this.description = opt_description;
this.details = opt_details;
}
};

View File

@@ -0,0 +1,11 @@
"use strict";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Image
export default class Image {
width = null;
height = null;
constructor (url) {
this.url = url;
}
};

View File

@@ -0,0 +1,24 @@
"use strict";
import { Capability
, ReceiverType } from "../enums";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Receiver
export default class Receiver {
constructor (
label
, friendlyName
, opt_capabilities = [
Capability.VIDEO_OUT
, Capability.AUDIO_OUT ]
, opt_volume = null) {
this.capabilities = opt_capabilities;
this.displayStatus = null;
this.friendlyName = friendlyName;
this.isActiveInput = null;
this.label = label;
this.receiverType = ReceiverType.CAST;
this.volume = opt_volume;
}
};

View File

@@ -0,0 +1,10 @@
"use strict";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.ReceiverDisplayStatus
export default class ReceiverDisplayStatus {
constructor (statusText, appImages) {
this.appImages = appImages;
this.showStop = null;
this.statusText = statusText;
}
};

View File

@@ -0,0 +1,10 @@
"use strict";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.SenderApplication
export default class SenderApplication {
constructor (platform) {
this.packageId = null;
this.platform = platform;
this.url = null;
}
};

View File

@@ -0,0 +1,319 @@
"use strict";
import _Error from "./Error";
import Volume from "./Volume";
import Media from "../../media/classes/Media";
import { SessionStatus
, ErrorCode
, VolumeControlType } from "../enums";
import { onMessage, sendMessage } from "../../messageBridge";
import uuid from "uuid/v1";
export default class Session {
constructor (
sessionId
, appId
, displayName
, appImages
, receiver
, successCallback) {
this._id = uuid();
this._messageListeners = new Map();
this._updateListeners = new Set();
this._sendMessageCallbacks = new Map();
this._setReceiverMutedCallbacks = new Map();
this._setReceiverVolumeLevelCallbacks = new Map();
this._stopCallbacks = new Map();
this.sessionId = sessionId;
this.appId = appId;
this.appImages = appImages;
this.displayName = displayName;
this.receiver = receiver;
this.media = [];
this.namespaces = [];
this.senderApps = [];
this.status = SessionStatus.DISCONNECTED;
this.statusText = null;
this._sendMessage("bridge:bridgesession/initialize", {
address: receiver._address
, port: receiver._port
, appId
, sessionId
});
onMessage(message => {
// Filter other session messages
if (message._id && message._id !== this._id) {
return;
}
switch (message.subject) {
case "shim:session/stopped":
this.status = SessionStatus.STOPPED;
this._updateListeners.forEach(listener => listener());
break;
case "shim:session/connected":
this.status = SessionStatus.CONNECTED;
this.sessionId = message.data.sessionId;
this.namespaces = message.data.namespaces;
this.displayName = message.data.displayName;
this.statusText = message.data.statusText;
if (successCallback) {
successCallback(this);
}
break;
case "shim:session/updateStatus":
if (message.data.volume) {
if (!this.receiver.volume) {
const receiverVolume = new Volume(
message.data.volume.level
, message.data.volume.muted);
receiverVolume.controlType = message.data.volume.controlType;
receiverVolume.stepInterval = message.data.volume.stepInterval;
this.receiver.volume = receiverVolume;
} else {
this.receiver.volume.level = message.data.volume.level;
this.receiver.volume.muted = message.data.volume.muted;
}
}
break;
case "shim:session/impl_addMessageListener": {
const { namespace, data } = message.data;
this._messageListeners.get(namespace).forEach(
listener => listener(namespace, data));
break;
}
case "shim:session/impl_sendMessage": {
const { messageId, error } = message.data;
const [ successCallback, errorCallback ]
= this._sendMessageCallbacks.get(messageId)
if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else if (successCallback) {
successCallback();
}
this._sendMessageCallbacks.delete(messageId);
break;
}
case "shim:session/impl_setReceiverMuted": {
const { volumeId, error } = message.data;
const [ successCallback, errorCallback ]
= this._setReceiverMutedCallbacks.get(volumeId);
if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else if (successCallback) {
successCallback();
}
break;
this._setReceiverMutedCallbacks.delete(volumeId);
}
case "shim:session/impl_setReceiverVolumeLevel": {
const { volumeId, error } = message.data;
const [ successCallback, errorCallback ]
= this._setReceiverVolumeLevelCallbacks.get(volumeId);
if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else if (successCallback) {
successCallback();
}
this._setReceiverVolumeLevelCallbacks.delete(volumeId);
break;
}
case "shim:session/impl_stop": {
const { stopId, error } = message.data;
const [ successCallback, errorCallback ]
= this._stopCallbacks.get(stopId);
if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else {
this.status = SessionStatus.STOPPED;
this._updateListeners.forEach(listener => listener());
if (successCallback) {
successCallback();
}
}
this._stopCallbacks.delete(stopId);
break;
}
}
});
}
_sendMessage (subject, data = {}) {
sendMessage({
subject
, data
, _id: this._id
});
}
addMediaListener (listener) {
console.info("STUB :: Session#addMediaListener")
}
addMessageListener (namespace, listener) {
if (!this._messageListeners.has(namespace)) {
this._messageListeners.set(namespace, new Set());
}
this._messageListeners.get(namespace).add(listener);
this._sendMessage("bridge:bridgesession/impl_addMessageListener", {
namespace
});
}
addUpdateListener (listener) {
this._updateListeners.add(listener);
}
leave (successCallback, errorCallback) {
const id = uuid();
this._sendMessage("bridge:bridgesession/impl_leave", { id });
this._leaveCallbacks.set(id, [
successCallback
, errorCallback
]);
}
loadMedia (loadRequest, successCallback, errorCallback) {
this.sendMediaMessage({
type: "LOAD"
, requestId: 0
, media: loadRequest.media
, activeTrackIds: loadRequest.activeTrackIds || []
, autoplay: loadRequest.autoplay || false
, currentTime: loadRequest.currentTime || 0
, customData: loadRequest.customData || {}
, repeatMode: "REPEAT_OFF"
});
let hasResponded = false;
this.addMessageListener("urn:x-cast:com.google.cast.media"
, (namespace, data) => {
if (hasResponded) return;
const mediaObject = JSON.parse(data);
if (mediaObject.status && mediaObject.status.length > 0) {
hasResponded = true;
const media = new Media(
this.sessionId
, mediaObject.status[0].mediaSessionId
, this._id);
media.media = loadRequest.media;
this.media = [ media ];
media.play();
successCallback(media);
} else {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
}
})
}
queueLoad () {
console.info("STUB :: Session#queueLoad");
}
removeMediaListener (listener) {
console.info("STUB :: Session#removeMediaListener");
}
removeMessageListener (namespace, listener) {
this._messageListeners.get(namespace).delete(listener);
}
removeUpdateListener (namespace, listener) {
this._updateListeners.delete(listener);
}
sendMediaMessage (message) {
this.sendMessage(
"urn:x-cast:com.google.cast.media"
, message
, () => {}
, () => {});
}
sendMessage (namespace, message, successCallback, errorCallback) {
const messageId = uuid();
this._sendMessage("bridge:bridgesession/impl_sendMessage", {
namespace
, message
, messageId
});
this._sendMessageCallbacks.set(messageId, [
successCallback
, errorCallback
]);
}
setReceiverMuted (muted, successCallback, errorCallback) {
const volumeId = uuid();
this._sendMessage("bridge:bridgesession/impl_setReceiverMuted", {
muted
, volumeId
});
this._setReceiverMutedCallbacks.set(volumeId, [
successCallback
, errorCallback
]);
}
setReceiverVolumeLevel (newLevel, successCallback, errorCallback) {
const volumeId = uuid();
this._sendMessage("bridge:bridgesession/impl_setReceiverVolumeLevel", {
newLevel
, volumeId
});
this._setReceiverVolumeLevelCallbacks.set(volumeId, [
successCallback
, errorCallback
]);
}
stop (successCallback, errorCallback) {
const stopId = uuid();
this._sendMessage("bridge:bridgesession/impl_stop", { stopId });
this._stopCallbacks.set(stopId, [
successCallback
, errorCallback
]);
}
}

View File

@@ -0,0 +1,20 @@
"use strict";
import { Capability } from "../enums";
import { requestSession as requestSessionTimeout } from "../../timeout.js";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.SessionRequest
export default class SessionRequest {
constructor (
appId
, opt_capabilities = [
Capability.VIDEO_OUT
, Capability.AUDIO_OUT ]
, opt_timeout = null) {
this.appId = appId;
this.capabilities = opt_capabilities;
this.language = null;
this.requestSessionTimeout = requestSessionTimeout;
}
};

View File

@@ -0,0 +1,10 @@
"use strict";
import * as timeouts from "../../timeout.js";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Timeout
export default class Timeout {
constructor () {
Object.assign(this, timeouts);
}
};

View File

@@ -0,0 +1,13 @@
"use strict";
import { VolumeControlType } from "../enums";
// https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Volume
export default class Volume {
constructor (
opt_level = null
, opt_muted = null) {
this.level = opt_level;
this.muted = opt_muted;
}
};

View File

@@ -0,0 +1,28 @@
"use strict";
import ApiConfig from "./ApiConfig";
import DialRequest from "./DialRequest";
import Error_ from "./Error";
import Image_ from "./Image";
import Receiver from "./Receiver";
import ReceiverDisplayStatus from "./ReceiverDisplayStatus";
import SenderApplication from "./SenderApplication";
import Session from "./Session";
import SessionRequest from "./SessionRequest";
import Timeout from "./Timeout";
import Volume from "./Volume";
export default {
AutoJoinPolicy
, Capability
, DefaultActionPolicy
, DialAppState
, ErrorCode
, ReceiverAction
, ReceiverAvailability
, ReceiverType
, SenderPlatform
, SessionStatus
, VolumeControlType
};

View File

@@ -0,0 +1,74 @@
"use strict";
export const AutoJoinPolicy = {
TAB_AND_ORIGIN_SCOPED: "tab_and_origin_scoped"
, ORIGIN_SCOPED: "origin_scoped"
, PAGE_SCOPED: "page_scoped"
};
export const Capability = {
VIDEO_OUT: "video_out"
, AUDIO_OUT: "audio_out"
, VIDEO_IN: "video_in"
, AUDIO_IN: "audio_in"
, MULTIZONE_GROUP: "multizone_group"
};
export const DefaultActionPolicy = {
CREATE_SESSION: "create_session"
, CAST_THIS_TAB: "cast_this_tab"
};
export const DialAppState = {
RUNNING: "running"
, STOPPED: "stopped"
, ERROR: "error"
};
export const ErrorCode = {
CANCEL: "cancel"
, TIMEOUT: "timeout"
, API_NOT_INITIALIZED: "api_not_initialized"
, INVALID_PARAMETER: "invalid_parameter"
, EXTENSION_NOT_COMPATIBLE: "extension_not_compatible"
, EXTENSION_MISSING: "extension_missing"
, RECEIVER_UNAVAILABLE: "receiver_unavailable"
, SESSION_ERROR: "session_error"
, CHANNEL_ERROR: "channel_error"
, LOAD_MEDIA_FAILED: "load_media_failed"
};
export const ReceiverAction = {
CAST: "cast"
, STOP: "stop"
};
export const ReceiverAvailability = {
AVAILABLE: "available"
, UNAVAILABLE: "unavailable"
};
export const ReceiverType = {
CAST: "cast"
, DIAL: "dial"
, HANGOUT: "hangout"
, CUSTOM: "custom"
};
export const SenderPlatform = {
CHROME: "chrome"
, IOS: "ios"
, ANDROID: "android"
};
export const SessionStatus = {
CONNECTED: "connected"
, DISCONNECTED: "disconnected"
, STOPPED: "stopped"
};
export const VolumeControlType = {
ATTENUATION: "attenuation"
, FIXED: "fixed"
, MASTER: "master"
};

261
ext/src/shim/cast/index.js Executable file
View File

@@ -0,0 +1,261 @@
"use strict";
import ApiConfig from "./classes/ApiConfig";
import DialRequest from "./classes/DialRequest";
import Error_ from "./classes/Error";
import Image_ from "./classes/Image";
import Receiver from "./classes/Receiver";
import ReceiverDisplayStatus from "./classes/ReceiverDisplayStatus";
import SenderApplication from "./classes/SenderApplication";
import Session from "./classes/Session";
import SessionRequest from "./classes/SessionRequest";
import Timeout from "./classes/Timeout";
import Volume from "./classes/Volume";
import { AutoJoinPolicy
, Capability
, DefaultActionPolicy
, DialAppState
, ErrorCode
, ReceiverAction
, ReceiverAvailability
, ReceiverType
, SenderPlatform
, SessionStatus
, VolumeControlType } from "./enums";
import { requestSession as requestSessionTimeout } from "../timeout";
import state from "../state";
import { onMessage, sendMessage } from "../messageBridge";
const cast = {
// Enums
AutoJoinPolicy
, Capability
, DefaultActionPolicy
, DialAppState
, ErrorCode
, ReceiverAction
, ReceiverAvailability
, ReceiverType
, SenderPlatform
, SessionStatus
, VolumeControlType
// Classes
, ApiConfig
, DialRequest
, Error: Error_
, Image: Image_
, Receiver
, ReceiverDisplayStatus
, SenderApplication
, Session
, SessionRequest
, Timeout
, Volume
, timeout: new Timeout()
, isAvailable: true
, VERSION: [ 1, 2 ]
};
const receiverListeners = new Set();
let sessionSuccessCallback;
let sessionErrorCallback;
cast.addReceiverActionListener = (listener) => {
console.info("Caster (Debug): cast.addReceiverActionListener");
receiverListeners.add(listener);
};
cast.initialize = (
apiConfig
, successCallback
, errorCallback) => {
console.info("Caster (Debug): cast.initialize");
// Already initialized
if (state.apiConfig) {
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
return;
}
state.apiConfig = apiConfig;
sendMessage({
subject: "bridge:discover"
});
apiConfig.receiverListener(state.receiverList.length
? ReceiverAvailability.AVAILABLE
: ReceiverAvailability.UNAVAILABLE);
successCallback();
};
cast.logMessage = (message) => {
console.log("CAST MSG:", message);
};
cast.precache = (data) => {
console.info("STUB :: cast.precache");
};
cast.removeReceiverActionListener = (listener) => {
receiverListeners.delete(listener);
}
cast.requestSession = (
successCallback
, errorCallback
, opt_sessionRequest = state.apiConfig.sessionRequest) => {
console.info("Caster (Debug): cast.requestSession");
// Called before initialization
if (!state.apiConfig) {
errorCallback(new Error_(ErrorCode.API_NOT_INITIALIZED));
return;
}
// No available receivers
if (!state.receiverList.length) {
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
return;
}
sessionSuccessCallback = successCallback;
sessionErrorCallback = errorCallback;
// Open destination chooser
sendMessage({
subject: "main:openPopup"
});
};
cast.requestSessionById = (sessionId) => {
console.info("STUB :: cast.requestSessionById");
};
cast.setCustomReceivers = (receivers, successCallback, errorCallback) => {
console.info("STUB :: cast.setCustomReceivers");
};
cast.setPageContext = (win) => {
console.info("STUB :: cast.setPageContext");
};
cast.setReceiverDisplayStatus = (sessionId) => {
console.info("STUB :: cast.setReceiverDisplayStatus");
};
cast.unescape = (escaped) => unescape(escaped);
onMessage(message => {
switch (message.subject) {
/**
* Cast destination found (serviceUp). Set the API availability
* property and call the page event function (__onGCastApiAvailable).
*/
case "shim:serviceUp":
const receiver = new Receiver(
message.data.id
, message.data.friendlyName);
receiver._address = message.data.address;
receiver._port = message.data.port;
if (state.receiverList.find(r => r.label === receiver.label)) {
break;
}
state.receiverList.push(receiver);
// Notify listeners of new cast destination
state.apiConfig.receiverListener(ReceiverAvailability.AVAILABLE);
receiverListeners.forEach(
listener => listener(ReceiverAvailability.AVAILABLE));
break;
/**
* Cast destination lost (serviceDown). Remove from the receiver list
* and update availability state.
*/
case "shim:serviceDown":
state.receiverList = state.receiverList.filter(
receiver => receiver.label !== message.data.id);
if (state.receiverList.length === 0) {
state.apiConfig.receiverListener(
ReceiverAvailability.UNAVAILABLE);
}
break;
case "shim:selectReceiver":
console.info("Caster (Debug): Selected receiver");
const selectedReceiver = message.data;
const sessionConstructorArgs = [
state.sessionList.length // sessionId
, state.apiConfig.sessionRequest.appId // appId
, selectedReceiver.friendlyName // displayName
, [] // appImages
, selectedReceiver // receiver
, (session) => {
sendMessage({
subject: "popup:close"
});
state.apiConfig.sessionListener(session);
sessionSuccessCallback(session);
}
];
// If existing session active, stop it and start new one
if (state.sessionList.length) {
const lastSession
= state.sessionList[state.sessionList.length - 1];
if (lastSession.status !== SessionStatus.STOPPED) {
lastSession.stop(() => {
state.sessionList.push(new Session(
...sessionConstructorArgs));
});
break;
}
}
state.sessionList.push(new Session(...sessionConstructorArgs));
break;
/**
* Popup is ready to receive data to populate the cast destination
* chooser.
*/
case "shim:popupReady":
sendMessage({
subject: "popup:populate"
, data: state.receiverList
});
break;
}
});
// Trigger bridge mDNS discovery
sendMessage({
subject: "main:initialize"
});
export default cast;

18
ext/src/shim/index.js Executable file
View File

@@ -0,0 +1,18 @@
"use strict";
import cast from "./cast";
import media from "./media";
if (!window.chrome) {
window.chrome = {};
}
window.chrome.cast = cast;
window.chrome.cast.media = media;
// Call page's API loaded function if defined
const readyFunction = window.__onGCastApiAvailable;
console.log(readyFunction);
if (readyFunction && typeof readyFunction === "function") {
readyFunction(true);
}

View File

@@ -0,0 +1,8 @@
"use strict";
export default class EditTracksInfoRequest {
constructor (opt_activeTrackIds = null, opt_textTrackStyle = null) {
this.activeTrackIds = opt_activeTrackIds;
this.textTrackStyle = opt_textTrackStyle;
}
}

View File

@@ -0,0 +1,11 @@
"use strict";
export default class GenericMediaMetadata {
constructor () {
this.images = [];
this.metadataType = null;
this.releaseDate = null;
this.subtitle = null;
this.title = null;
}
}

View File

@@ -0,0 +1,8 @@
"use strict";
export default class GetStatusRequest {
constructor () {
castConsole.info('GetStatusRequest');
this.customData = {};
}
}

View File

@@ -0,0 +1,11 @@
"use strict";
export default class LoadRequest {
constructor (mediaInfo) {
this.activeTrackIds = [];
this.autoplay = false;
this.currentTime = 0;
this.customData = {};
this.media = mediaInfo;
}
}

View File

@@ -0,0 +1,202 @@
"use strict";
import Volume from "../../cast/classes/Volume";
import { PlayerState
, RepeatMode
, MediaCommand } from "../enums";
import _Error from "../../cast/classes/Error";
import { ErrorCode } from "../../cast/enums";
import { onMessage, sendMessage } from "../../messageBridge";
import uuid from "uuid/v1";
export default class Media {
constructor (sessionId, mediaSessionId, _internalSessionId) {
this._id = uuid();
this.activeTrackIds = [];
this.currentItemId = 1;
this.customData = {};
this.currentTime = 0;
this.idleReason = null;
this.items = [];
this.loadingItemId = null;
this.media = null;
this.mediaSessionId = mediaSessionId;
this.playbackRate = 1;
this.playerState = PlayerState.PAUSED;
this.preloadedItemId = null;
this.RepeatMode = RepeatMode.OFF;
this.sessionId = sessionId;
this.supportedMediaCommands = [
MediaCommand.PAUSE
, MediaCommand.SEEK
, MediaCommand.STREAM_VOLUME
, MediaCommand.STREAM_MUTE
];
this.volume = new Volume();
this._sendMessage("bridge:bridgemedia/initialize", {
sessionId
, mediaSessionId
, _internalSessionId
});
onMessage(message => {
if (!message._id || message._id !== this._id) {
return;
}
switch (message.subject) {
case "shim:media/update":
const status = message.data;
this.currentTime = status.currentTime;
this._lastCurrentTime = status._lastCurrentTime;
this.customData = status.customData;
this.volume = new Volume(
status._volumeLevel
, status._volumeMuted);
this.playbackRate = status.playbackRate;
this.playerState = status.playerState;
this.repeatMode = status.repeatMode;
if (status.media) {
this.media = status.media;
}
if (status.mediaSessionId) {
this.mediaSessionId = status.mediaSessionId;
}
// Call update listeners
this._updateListeners.forEach(listener => listener(true));
break;
case "shim:media/sendMediaMessageResponse":
const { messageId, error } = message.data;
const [ successCallback, errorCallback ]
= this._sendMediaMessageCallbacks.get(messageId);
if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else if (successCallback) {
successCallback();
}
break;
}
});
this._updateListeners = new Set();
this._sendMediaMessageCallbacks = new Map();
}
_sendMessage (subject, data) {
sendMessage({
subject
, data
, _id: this._id
});
}
_sendMediaMessage (message, successCallback, errorCallback) {
message.mediaSessionId = this.mediaSessionId;
message.requestId = 0;
message.sessionId = this.sessionId;
message.customData = null;
const messageId = uuid();
this._sendMediaMessageCallbacks.set(messageId, [
successCallback
, errorCallback
]);
this._sendMessage("bridge:bridgemedia/sendMediaMessage", {
message
, messageId
});
}
addUpdateListener (listener) {
this._updateListeners.add(listener);
}
editTracksInfo (editTracksInfoRequest, successCallback, errorCallback) {
console.log("STUB :: Media#editTracksInfo");
}
getEstimatedTime () {
if (!this.currentTime) return 0;
return this.currentTime + ((Date.now() / 1000) - this._lastCurrentTime);
}
getStatus (getStatusRequest, successCallback, errorCallback) {
this._sendMediaMessage({ type: "MEDIA_GET_STATUS" }
, successCallback, errorCallback);
}
pause (pauseRequest, successCallback, errorCallback) {
this._sendMediaMessage({ type: "PAUSE" }
, successCallback, errorCallback);
}
play (playRequest, successCallback, errorCallback) {
this._sendMediaMessage({ type: "PLAY" }
, successCallback, errorCallback);
}
queueAppendItem (item, successCallback, errorCallback) {
console.log("STUB :: Media#queueAppendItem");
}
queueInsertItems (queueInsertItemsRequest, successCallback, errorCallback) {
console.log("STUB :: Media#queueInsertItems");
}
queueJumpToItem (itemId, successCallback, errorCallback) {
console.log("STUB :: Media#queueJumpToItem");
}
queueMoveItemToNewIndex (itemId, newIndex, successCallback, errorCallback) {
console.log("STUB :: Media#queueMoveItemToNewIndex");
}
queueNext (successCallback, errorCallback) {
console.log("STUB :: Media#queueNext");
}
queuePrev (successCallback, errorCallback) {
console.log("STUB :: Media#queuePrev");
}
queueRemoveItem(itemId, successCallback, errorCallback) {
console.log("STUB :: Media#queueRemoveItem");
}
queueReorderItems (queueReorderItemsRequest, successCallback, errorCallback) {
console.log("STUB :: Media#queueReorderItems");
}
queueSetRepeatMode (repeatMode, successCallback, errorCallback) {
console.log("STUB :: Media#queueSetRepeatMode");
}
queueUpdateItems (queueUpdateItemsRequest, successCallback, errorCallback) {
console.log("STUB :: Media#queueUpdateItems");
}
removeUpdateListener (listener) {
this._updateListeners.delete(listener);
}
seek (seekRequest, successCallback, errorCallback) {
console.log(seekRequest);
this._sendMediaMessage({
type: "SEEK"
, currentTime: seekRequest.currentTime
}, successCallback, errorCallback);
}
setVolume (volumeRequest, successCallback, errorCallback) {
this._sendMediaMessage({
type: "SET_VOLUME"
, volume: volumeRequest.volume
}, successCallback, errorCallback);
}
stop (stopRequest, successCallback, errorCallback) {
this._sendMediaMessage({ type: "STOP" }
, successCallback, errorCallback);
}
supportsCommand (command) {
console.log("STUB :: Media#supportsCommand");
}
}

View File

@@ -0,0 +1,16 @@
"use strict";
import { StreamType } from "../enums";
export default class MediaInfo {
constructor (contentId, contentType) {
this.contentId = contentId;
this.contentType = contentType;
this.customData = {};
this.duration = null;
this.metadata = null;
this.streamType = StreamType.BUFFERED;
this.textTrackStyle = null;
this.tracks = [];
}
}

View File

@@ -0,0 +1,12 @@
"use strict";
export default class MovieMediaMetadata {
constructor () {
this.images = [];
this.metadataType = null;
this.releaseDate = null;
this.studio = null;
this.subtitle = null;
this.title = null;
}
}

View File

@@ -0,0 +1,17 @@
"use strict";
export default class MusicTrackMediaMetadata {
constructor () {
this.albumArtist = null;
this.albumName = null;
this.artist = null;
this.composer = null;
this.discNumber = null;
this.images = [];
this.metadataType = this.type = 3;
this.releaseDate = null;
this.songName = null;
this.title = null;
this.trackNumber = null;
}
}

View File

@@ -0,0 +1,7 @@
"use strict";
export default class PauseRequest {
constructor () {
this.customData = {};
}
}

View File

@@ -0,0 +1,16 @@
"use strict";
export default class PhotoMediaMetadata {
constructor () {
this.artist = null;
this.creationDateTime = null;
this.height = null;
this.images = [];
this.latitude = null;
this.location = null;
this.longitude = null;
this.metadataType = null;
this.title = null;
this.width = null;
}
}

View File

@@ -0,0 +1,7 @@
"use strict";
export default class PlayRequest {
constructor () {
this.customData = {};
}
}

View File

@@ -0,0 +1,9 @@
"use strict";
export default class QueueInsertItemsRequest {
constructor (itemsToInsert) {
this.customData = {};
this.insertBefore = null;
this.items = itemsToInsert;
}
}

View File

@@ -0,0 +1,13 @@
"use strict";
export default class QueueItem {
constructor (mediaInfo) {
this.activeTrackIds = [];
this.autoplay = false;
this.customData = {};
this.itemId = null;
this.media = mediaInfo;
this.preloadTime = 10;
this.startTime = 0;
}
}

View File

@@ -0,0 +1,12 @@
"use strict";
import { RepeatMode } from "../enums";
export default class QueueLoadRequest {
constructor (items) {
this.customData = {};
this.items = items;
this.repeatMode = RepeatMode.OFF;
this.startIndex = 0;
}
}

View File

@@ -0,0 +1,8 @@
"use strict";
export default class QueueRemoveItemsRequest {
constructor (itemIdsToRemove) {
this.customData = {};
this.itemIds = itemIdsToRemove;
}
}

View File

@@ -0,0 +1,10 @@
"use strict";
export default class QueueReorderItemsRequest {
constructor (itemIdsToReorder) {
this.customData = {};
this.type = "QUEUE_REORDER";
this.insertBefore = null;
this.itemIds = itemIdsToReorder;
}
}

View File

@@ -0,0 +1,11 @@
"use strict";
export default class QueueSetPropertiesRequest {
constructor () {
this.type = "QUEUE_UPDATE";
this.customData = {};
this.repeatMode = null;
this.sessionId = null;
this.requestId = null;
}
}

View File

@@ -0,0 +1,8 @@
"use strict";
export default class QueueUpdateItemsRequest {
constructor () {
this.customData = {};
this.items = [];
}
}

View File

@@ -0,0 +1,9 @@
"use strict";
export default class SeekRequest {
constructor () {
this.currentTime = null;
this.customData = {};
this.resumeState = null;
}
}

View File

@@ -0,0 +1,7 @@
"use strict";
export default class StopRequest {
constructor () {
this.customData = {};
}
}

View File

@@ -0,0 +1,18 @@
"use strict";
export default class TextTrackStyle {
constructor () {
this.backgroundColor = null;
this.customData = {};
this.edgeColor = null;
this.edgeType = null;
this.fontFamily = null;
this.fontGenericFamily = null;
this.fontScale = null;
this.fontStyle = null;
this.foregroundColor = null;
this.windowColor = null;
this.windowRoundedCornerRadius = null;
this.windowType = null;
}
}

View File

@@ -0,0 +1,14 @@
"use strict";
export default class Track {
constructor (trackId, trackType) {
this.customData = {};
this.language = null;
this.name = null;
this.subtype = null;
this.trackContentId = null;
this.trackContentType = null;
this.trackId = trackId;
this.type = trackType;
}
}

View File

@@ -0,0 +1,13 @@
"use strict";
export default class TvShowMediaMetadata {
constructor () {
this.episode = null;
this.images = [];
this.metadataType = null;
this.originalAirdate = null;
this.season = null;
this.seriesTitle = null;
this.title = null;
}
}

View File

@@ -0,0 +1,8 @@
"use strict";
export default class VolumeRequest {
constructor (volume) {
this.volume = volume;
this.customData = {};
}
}

View File

@@ -0,0 +1,92 @@
"use strict";
export const IdleReason = {
CANCELLED: "cancelled"
, INTERRUPTED: "interrupted"
, FINISHED: "finished"
, ERROR: "error"
};
export const MediaCommand = {
PAUSE: "pause"
, SEEK: "seek"
, STREAM_VOLUME: "stream_volume"
, STREAM_MUTE: "stream_mute"
};
export const MetadataType = {
GENERIC: "GENERIC"
, MOVIE: "MOVIE"
, TV_SHOW: "TV_SHOW"
, MUSIC_TRACK: "MUSIC_TRACK"
, PHOTO: "PHOTO"
};
export const PlayerState = {
IDLE: "IDLE"
, PLAYING: "PLAYING"
, PAUSED: "PAUSED"
, BUFFERING: "BUFFERING"
};
export const RepeatMode = {
OFF: "OFF"
, ALL: "ALL"
, SINGLE: "SINGLE"
, ALL_AND_SHUFFLE: "ALL_AND_SHUFFLE"
};
export const ResumeState = {
PLAYBACK_START: "PLAYBACK_START"
, PLAYBACK_PAUSE: "PLAYBACK_PAUSE"
};
export const StreamType = {
BUFFERED: "BUFFERED"
, LIVE: "LIVE"
, OTHER: "OTHER"
};
export const TextTrackEdgeType = {
NONE: "NONE"
, OUTLINE: "OUTLINE"
, DROP_SHADOW: "DROP_SHADOW"
, RAISED: "RAISED"
, DEPRESSED: "DEPRESSED"
};
export const TextTrackFontGenericFamily = {
SANS_SERIF: "SANS_SERIF"
, MONOSPACED_SANS_SERIF: "MONOSPACED_SANS_SERIF"
, SERIF: "SERIF"
, CASUAL: "CASUAL"
, CURSIVE: "CURSIVE"
, SMALL_CAPITALS: "SMALL_CAPITALS"
};
export const TextTrackFontStyle = {
NORMAL: "NORMAL"
, BOLD: "BOLD"
, BOLD_ITALIC: "BOLD_ITALIC"
, ITALIC: "ITALIC"
};
export const TextTrackType = {
SUBTITLES: "SUBTITLES"
, CAPTIONS: "CAPTIONS"
, DESCRIPTIONS: "DESCRIPTIONS"
, CHAPTERS: "CHAPTERS"
, METADATA: "METADATA"
};
export const TextTrackWindowType = {
NONE: "NONE"
, NORMAL: "NORMAL"
, ROUNDED_CORNERS: "ROUNDED_CORNERS"
};
export const TrackType = {
TEXT: "TEXT"
, AUDIO: "AUDIO"
, VIDEO: "VIDEO"
};

86
ext/src/shim/media/index.js Executable file
View File

@@ -0,0 +1,86 @@
"use strict";
import EditTracksInfoRequest from "./classes/EditTracksInfoRequest";
import GenericMediaMetadata from "./classes/GenericMediaMetadata";
import GetStatusRequest from "./classes/GetStatusRequest";
import LoadRequest from "./classes/LoadRequest";
import Media from "./classes/Media";
import MediaInfo from "./classes/MediaInfo";
import MovieMediaMetadata from "./classes/MovieMediaMetadata";
import MusicTrackMediaMetadata from "./classes/MusicTrackMediaMetadata";
import PauseRequest from "./classes/PauseRequest";
import PhotoMediaMetadata from "./classes/PhotoMediaMetadata";
import PlayRequest from "./classes/PlayRequest";
import QueueInsertItemsRequest from "./classes/QueueInsertItemsRequest";
import QueueItem from "./classes/QueueItem";
import QueueLoadRequest from "./classes/QueueLoadRequest";
import QueueRemoveItemsRequest from "./classes/QueueRemoveItemsRequest";
import QueueReorderItemsRequest from "./classes/QueueReorderItemsRequest";
import QueueSetPropertiesRequest from "./classes/QueueSetPropertiesRequest";
import QueueUpdateItemsRequest from "./classes/QueueUpdateItemsRequest";
import SeekRequest from "./classes/SeekRequest";
import StopRequest from "./classes/StopRequest";
import TextTrackStyle from "./classes/TextTrackStyle";
import Track from "./classes/Track";
import TvShowMediaMetadata from "./classes/TvShowMediaMetadata";
import VolumeRequest from "./classes/VolumeRequest";
import { IdleReason
, MediaCommand
, MetadataType
, PlayerState
, RepeatMode
, ResumeState
, StreamType
, TextTrackEdgeType
, TextTrackFontGenericFamily
, TextTrackFontStyle
, TextTrackType
, TextTrackWindowType
, TrackType } from "./enums";
export default {
// Enums
IdleReason
, MediaCommand
, MetadataType
, PlayerState
, RepeatMode
, ResumeState
, StreamType
, TextTrackEdgeType
, TextTrackFontGenericFamily
, TextTrackFontStyle
, TextTrackType
, TextTrackWindowType
, TrackType
// Classes
, EditTracksInfoRequest
, GenericMediaMetadata
, GetStatusRequest
, LoadRequest
, Media
, MediaInfo
, MovieMediaMetadata
, MusicTrackMediaMetadata
, PauseRequest
, PhotoMediaMetadata
, PlayRequest
, QueueInsertItemsRequest
, QueueItem
, QueueLoadRequest
, QueueRemoveItemsRequest
, QueueReorderItemsRequest
, QueueSetPropertiesRequest
, QueueUpdateItemsRequest
, SeekRequest
, StopRequest
, TextTrackStyle
, Track
, TvShowMediaMetadata
, VolumeRequest
, DEFAULT_MEDIA_RECEIVER_APP_ID: "CC1AD845"
};

View File

@@ -0,0 +1,15 @@
"use strict";
export function onMessage (listener) {
document.addEventListener("__castMessage", ev => {
listener(JSON.parse(ev.detail));
});
}
export function sendMessage (message) {
const event = new CustomEvent("__castMessageResponse", {
detail: message
});
document.dispatchEvent(event);
}

10
ext/src/shim/state.js Executable file
View File

@@ -0,0 +1,10 @@
"use strict";
// Global API state
const state = {
apiConfig: null
, receiverList: []
, sessionList: []
};
export default state;

7
ext/src/shim/timeout.js Executable file
View File

@@ -0,0 +1,7 @@
"use strict";
export const leaveSession = 3000;
export const requestSession = 60000;
export const sendCustomMessage = 3000;
export const setReceiverVolume = 3000;
export const stopSession = 3000;