mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
Initial commit
This commit is contained in:
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2018 Matt Hensman <m@matt.tf>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
36
README.md
Normal file
36
README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Caster
|
||||||
|
|
||||||
|
Very WIP! Not ready for release. Expect many bugs. Please don't sign builds on AMO with current ID.
|
||||||
|
|
||||||
|
## Supported platforms
|
||||||
|
|
||||||
|
* Linux
|
||||||
|
* macOS (TODO)
|
||||||
|
* Windows (TODO)
|
||||||
|
|
||||||
|
Only tested on Linux. mDNS library issue to be fixed. `mdns` only works on Windows, `mdns-js` only works on Linux.
|
||||||
|
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
* NodeJS
|
||||||
|
* `node` binary in path
|
||||||
|
* ~~Bonjour SDK (Windows)~~
|
||||||
|
|
||||||
|
````
|
||||||
|
git clone https://github.com/hensm/caster.git
|
||||||
|
npm install ./ext --prefix ./ext
|
||||||
|
npm install ./app --prefix ./app
|
||||||
|
npm run build --prefix ./ext
|
||||||
|
````
|
||||||
|
|
||||||
|
Installer scripts aren't written yet, so registering the native messaging manifest with Firefox and specifiying the path must be done manually:
|
||||||
|
[MDN: Native Manifests](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Native_manifests)
|
||||||
|
|
||||||
|
`path` key within `app/caster_bridge.json` must be set to absolute path of `app/src/launcher.sh` or `app/src/launcher.bat`. Then, the manifest must be either moved to the correct location or the path added to the registry (Windows):
|
||||||
|
[MDN: Native Manifests # Manifest location](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Native_manifests#Manifest_location)
|
||||||
|
|
||||||
|
Extension can be loaded from `about:debugging` as a temporary extension.
|
||||||
|
|
||||||
7
app/caster_bridge.json
Executable file
7
app/caster_bridge.json
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "caster_bridge"
|
||||||
|
, "description": ""
|
||||||
|
, "path": ""
|
||||||
|
, "type": "stdio"
|
||||||
|
, "allowed_extensions": [ "caster@matt.tf" ]
|
||||||
|
}
|
||||||
3
app/install.sh
Executable file
3
app/install.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# TODO
|
||||||
114
app/package-lock.json
generated
Normal file
114
app/package-lock.json
generated
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"requires": true,
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"dependencies": {
|
||||||
|
"ascli": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ascli/-/ascli-0.3.0.tgz",
|
||||||
|
"integrity": "sha1-XmYjDlIZ/j6JUqTvtPIPrllqgTo=",
|
||||||
|
"requires": {
|
||||||
|
"colour": "^0.7.1",
|
||||||
|
"optjs": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bufferview": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bufferview/-/bufferview-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-ev10pF+Tf6QiodM4wIu/3HbNcl0="
|
||||||
|
},
|
||||||
|
"bytebuffer": {
|
||||||
|
"version": "3.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-3.5.5.tgz",
|
||||||
|
"integrity": "sha1-em+vGhNRSwg/H8+VQcTJv75+f9M=",
|
||||||
|
"requires": {
|
||||||
|
"bufferview": "~1",
|
||||||
|
"long": "~2 >=2.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"castv2": {
|
||||||
|
"version": "0.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/castv2/-/castv2-0.1.9.tgz",
|
||||||
|
"integrity": "sha1-0LD6sf0GsNnMpjaIZxbsEpOlkFo=",
|
||||||
|
"requires": {
|
||||||
|
"debug": "^2.2.0",
|
||||||
|
"protobufjs": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colour": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz",
|
||||||
|
"integrity": "sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g="
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dns-js": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dns-js/-/dns-js-0.2.1.tgz",
|
||||||
|
"integrity": "sha1-XWZimzwOal6w4U8K5wHQX26kZnM=",
|
||||||
|
"requires": {
|
||||||
|
"debug": "^2.1.0",
|
||||||
|
"qap": "^3.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"long": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz",
|
||||||
|
"integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8="
|
||||||
|
},
|
||||||
|
"mdns-js": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdns-js/-/mdns-js-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dwEtMzmoZCQcGlr004J4m2+W6dCMpCoGQ5kYIEY+7rMPdMM7ztT+1qD9ExmottvLGgbqAVsjllhwU8PyusecPg==",
|
||||||
|
"requires": {
|
||||||
|
"debug": "^3.1.0",
|
||||||
|
"dns-js": "~0.2.1",
|
||||||
|
"semver": "^5.4.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
|
},
|
||||||
|
"optjs": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz",
|
||||||
|
"integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4="
|
||||||
|
},
|
||||||
|
"protobufjs": {
|
||||||
|
"version": "3.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-3.8.2.tgz",
|
||||||
|
"integrity": "sha1-vIJuNMOvRpfo0K96Zp5NYSrtzRc=",
|
||||||
|
"requires": {
|
||||||
|
"ascli": "~0.3",
|
||||||
|
"bytebuffer": "~3 >=3.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qap": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/qap/-/qap-3.3.1.tgz",
|
||||||
|
"integrity": "sha1-Efno+oiQ/ny5khDA9E0GE7c3LKw="
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
app/package.json
Normal file
6
app/package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"castv2": "^0.1.9",
|
||||||
|
"mdns-js": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
app/src/launcher.bat
Executable file
2
app/src/launcher.bat
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
@echo off
|
||||||
|
node main.js
|
||||||
4
app/src/launcher.sh
Executable file
4
app/src/launcher.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
AVAHI_COMPAT_NOWARN=1 node main.js
|
||||||
|
|
||||||
463
app/src/main.js
Executable file
463
app/src/main.js
Executable file
@@ -0,0 +1,463 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { Client } = require("castv2");
|
||||||
|
const mdns = require("mdns-js");
|
||||||
|
|
||||||
|
const transforms = require("./transforms");
|
||||||
|
|
||||||
|
|
||||||
|
const browser = mdns.createBrowser(mdns.tcp("googlecast"));
|
||||||
|
|
||||||
|
// Increase listener limit
|
||||||
|
require('events').EventEmitter.defaultMaxListeners = 50;
|
||||||
|
|
||||||
|
// stdin -> stdout
|
||||||
|
process.stdin
|
||||||
|
.pipe(transforms.decode)
|
||||||
|
.pipe(transforms.response(handleMessage))
|
||||||
|
.pipe(transforms.encode)
|
||||||
|
.pipe(process.stdout);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode and send a message to the extension.
|
||||||
|
*/
|
||||||
|
function sendMessage (message) {
|
||||||
|
try {
|
||||||
|
transforms.encode.write(message);
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming messages from the extension and forward them to the
|
||||||
|
* appropriate handlers.
|
||||||
|
*/
|
||||||
|
async function handleMessage (message) {
|
||||||
|
if (message.subject.startsWith("bridge:bridgemedia/")) {
|
||||||
|
Media.messageHandler(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message.subject.startsWith("bridge:bridgesession/")) {
|
||||||
|
Session.messageHandler(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.subject) {
|
||||||
|
case "bridge:discover":
|
||||||
|
browser.discover();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
browser.on("update", service => {
|
||||||
|
if (!service.txt) return;
|
||||||
|
|
||||||
|
const txt = service.txt
|
||||||
|
.reduce((prev, current) => {
|
||||||
|
const [ key, value ] = current.split("=");
|
||||||
|
prev[key] = value;
|
||||||
|
return prev;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
sendMessage({
|
||||||
|
subject: "shim:serviceUp"
|
||||||
|
, data: {
|
||||||
|
address: service.addresses[0]
|
||||||
|
, port: service.port
|
||||||
|
, id: txt.id
|
||||||
|
, friendlyName: txt.fn
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
browser.on("serviceUp", service => {
|
||||||
|
transforms.encode.write({
|
||||||
|
subject: "shim:serviceUp"
|
||||||
|
, data: {
|
||||||
|
address: service.addresses[0]
|
||||||
|
, port: service.port
|
||||||
|
, id: service.txtRecord.id
|
||||||
|
, friendlyName: service.txtRecord.fn
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
browser.on("serviceDown", service => {
|
||||||
|
transforms.encode.write({
|
||||||
|
subject:"shim:serviceDown"
|
||||||
|
, data: {
|
||||||
|
address: service.addresses[0]
|
||||||
|
, port: service.port
|
||||||
|
, id: service.txtRecord.id
|
||||||
|
, friendlyName: service.txtRecord.fn
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})*/
|
||||||
|
|
||||||
|
|
||||||
|
const sessionMap = new Map();
|
||||||
|
|
||||||
|
class Session {
|
||||||
|
static messageHandler (message) {
|
||||||
|
const { _id } = message;
|
||||||
|
|
||||||
|
let session;
|
||||||
|
if (sessionMap.has(_id)) {
|
||||||
|
session = sessionMap.get(_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.subject) {
|
||||||
|
case "bridge:bridgesession/initialize":
|
||||||
|
sessionMap.set(_id, new Session(
|
||||||
|
message.data.address
|
||||||
|
, message.data.port
|
||||||
|
, message.data.appId
|
||||||
|
, message.data.sessionId));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bridge:bridgesession/close":
|
||||||
|
session.close();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bridge:bridgesession/impl_addMessageListener":
|
||||||
|
session._impl_addMessageListener(message.data.namespace);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bridge:bridgesession/impl_sendMessage":
|
||||||
|
session._impl_sendMessage(
|
||||||
|
message.data.namespace
|
||||||
|
, message.data.message
|
||||||
|
, message.data.messageId)
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bridge:bridgesession/impl_setReceiverMuted":
|
||||||
|
session._impl_setReceiverMuted(
|
||||||
|
message.data.muted
|
||||||
|
, message.data.volumeId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bridge:bridgesession/impl_setReceiverVolumeLevel":
|
||||||
|
session._impl_setReceiverVolumeLevel(
|
||||||
|
message.data.newLevel
|
||||||
|
, message.data.volumeId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bridge:bridgesession/impl_stop":
|
||||||
|
session._impl_stop(message.data.stopId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (host, port, appId, sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.clientConnection;
|
||||||
|
this.clientHeartbeat;
|
||||||
|
this.clientReceiver;
|
||||||
|
|
||||||
|
this.channelMap = new Map();
|
||||||
|
|
||||||
|
this.client = new Client();
|
||||||
|
this.client.connect({ host, port }, () => {
|
||||||
|
let transportHeartbeat;
|
||||||
|
|
||||||
|
this.clientConnection = this.client.createChannel(
|
||||||
|
"sender-0"
|
||||||
|
, "receiver-0"
|
||||||
|
, "urn:x-cast:com.google.cast.tp.connection"
|
||||||
|
, "JSON");
|
||||||
|
this.clientHeartbeat = this.client.createChannel(
|
||||||
|
"sender-0"
|
||||||
|
, "receiver-0"
|
||||||
|
, "urn:x-cast:com.google.cast.tp.heartbeat"
|
||||||
|
, "JSON");
|
||||||
|
this.clientReceiver = this.client.createChannel(
|
||||||
|
"sender-0"
|
||||||
|
, "receiver-0"
|
||||||
|
, "urn:x-cast:com.google.cast.receiver"
|
||||||
|
, "JSON");
|
||||||
|
|
||||||
|
this.clientConnection.send({ type: "CONNECT" });
|
||||||
|
this.clientHeartbeat.send({ type: "PING" });
|
||||||
|
|
||||||
|
this.clientHeartbeatInterval = setInterval(() => {
|
||||||
|
if (transportHeartbeat) {
|
||||||
|
transportHeartbeat.send({ type: "PING" });
|
||||||
|
}
|
||||||
|
this.clientHeartbeat.send({ type: "PING" });
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.clientReceiver.send({
|
||||||
|
type: "LAUNCH"
|
||||||
|
, appId
|
||||||
|
, requestId: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let sessionCreated = false;
|
||||||
|
|
||||||
|
this.clientReceiver.on("message", (data, broadcast) => {
|
||||||
|
switch (data.type) {
|
||||||
|
case "RECEIVER_STATUS":
|
||||||
|
|
||||||
|
this.sendMessage("shim:session/updateStatus", data.status);
|
||||||
|
|
||||||
|
if (!data.status.applications) return;
|
||||||
|
|
||||||
|
const receiverApp = data.status.applications[0];
|
||||||
|
const receiverAppId = receiverApp.appId;
|
||||||
|
|
||||||
|
this.app = receiverApp;
|
||||||
|
|
||||||
|
if (receiverAppId !== appId) {
|
||||||
|
// Close session
|
||||||
|
this.sendMessage("shim:session/stopped");
|
||||||
|
this.client.close();
|
||||||
|
clearInterval(this.clientHeartbeatInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionCreated) {
|
||||||
|
sessionCreated = true;
|
||||||
|
|
||||||
|
this.transport = this.app.transportId;
|
||||||
|
this.transportId = this.app.transportId;
|
||||||
|
this.clientId = `client-${Math.floor(Math.random() * 10e5)}`;
|
||||||
|
|
||||||
|
this.transportConnect = this.client.createChannel(
|
||||||
|
this.clientId
|
||||||
|
, this.transport
|
||||||
|
, "urn:x-cast:com.google.cast.tp.connection"
|
||||||
|
, "JSON");
|
||||||
|
|
||||||
|
this.transportConnect.send({ type: "CONNECT" });
|
||||||
|
|
||||||
|
transportHeartbeat = this.client.createChannel(
|
||||||
|
this.clientId
|
||||||
|
, this.transport
|
||||||
|
, "urn:x-cast:com.google.cast.tp.heartbeat"
|
||||||
|
, "JSON");
|
||||||
|
|
||||||
|
this.sessionId = this.app.sessionId;
|
||||||
|
|
||||||
|
this.sendMessage("shim:session/connected", {
|
||||||
|
sessionId: this.app.sessionId
|
||||||
|
, namespaces: this.app.namespaces
|
||||||
|
, displayName: this.app.displayName
|
||||||
|
, statusText: this.app.displayName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage (subject, data = {}) {
|
||||||
|
sendMessage({
|
||||||
|
subject
|
||||||
|
, data
|
||||||
|
, _id: this._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createChannel (namespace) {
|
||||||
|
if (!this.channelMap.has(namespace)) {
|
||||||
|
this.channelMap.set(namespace
|
||||||
|
, this.client.createChannel(
|
||||||
|
this.clientId, this.transport, namespace, "JSON"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close () {
|
||||||
|
this.clientConnection.send({ type: "CLOSE" });
|
||||||
|
if (this.transportConnect) {
|
||||||
|
this.transportConnect.send({ type: "CLOSE" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_impl_addMessageListener (namespace) {
|
||||||
|
this.createChannel(namespace);
|
||||||
|
this.channelMap.get(namespace).on("message", data => {
|
||||||
|
this.sendMessage("shim:session/impl_addMessageListener", {
|
||||||
|
namespace: namespace
|
||||||
|
, data: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_impl_sendMessage (namespace, message, messageId) {
|
||||||
|
let error = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.createChannel(namespace);
|
||||||
|
this.channelMap.get(namespace).send(message);
|
||||||
|
} catch (err) {
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendMessage("shim:session/impl_sendMessage", {
|
||||||
|
messageId
|
||||||
|
, error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_impl_setReceiverMuted (muted, volumeId) {
|
||||||
|
let error = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.clientReceiver.send({
|
||||||
|
type: "SET_VOLUME"
|
||||||
|
, volume: { muted }
|
||||||
|
, requestId: 0
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendMessage("shim:session/impl_setReceiverMuted", {
|
||||||
|
volumeId
|
||||||
|
, error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_impl_setReceiverVolumeLevel (newLevel, volumeId) {
|
||||||
|
let error = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.clientReceiver.send({
|
||||||
|
type: "SET_VOLUME"
|
||||||
|
, volume: { level: newLevel }
|
||||||
|
, requestId: 0
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendMessage("shim:session/impl_setReceiverVolumeLevel", {
|
||||||
|
volumeId
|
||||||
|
, error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_impl_stop (stopId) {
|
||||||
|
let error = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.clientReceiver.send({
|
||||||
|
type: "STOP"
|
||||||
|
, sessionId: this.sessionId
|
||||||
|
, requestId: 0
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client.close();
|
||||||
|
clearInterval(this.clientHeartbeatInterval);
|
||||||
|
|
||||||
|
this.sendMessage("shim:session/impl_stop", {
|
||||||
|
stopId
|
||||||
|
, error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const mediaMap = new Map();
|
||||||
|
|
||||||
|
class Media {
|
||||||
|
static messageHandler (message) {
|
||||||
|
const { _id } = message;
|
||||||
|
|
||||||
|
let media;
|
||||||
|
if (mediaMap.has(_id)) {
|
||||||
|
media = mediaMap.get(_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.subject) {
|
||||||
|
case "bridge:bridgemedia/initialize":
|
||||||
|
mediaMap.set(_id
|
||||||
|
, new Media(
|
||||||
|
message.data.sessionId
|
||||||
|
, message.data.mediaSessionId
|
||||||
|
, _id
|
||||||
|
, message.data._internalSessionId));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bridge:bridgemedia/sendMediaMessage":
|
||||||
|
const { messageId } = message.data;
|
||||||
|
let error = false;
|
||||||
|
try {
|
||||||
|
media.channel.send(message.data.message);
|
||||||
|
} catch (err) {
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
media.sendMessage("shim:media/sendMediaMessageResponse", {
|
||||||
|
messageId
|
||||||
|
, error
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (sessionId, mediaSessionId, _id, _internalSessionId) {
|
||||||
|
this._id = _id;
|
||||||
|
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.mediaSessionId = mediaSessionId;
|
||||||
|
|
||||||
|
const namespace = "urn:x-cast:com.google.cast.media";
|
||||||
|
|
||||||
|
this.session = sessionMap.get(_internalSessionId);
|
||||||
|
this.session.createChannel(namespace);
|
||||||
|
this.channel = this.session.channelMap.get(namespace);
|
||||||
|
|
||||||
|
this.channel.on("message", data => {
|
||||||
|
if (data && data.type === 'MEDIA_STATUS'
|
||||||
|
&& data.status && data.status.length > 0) {
|
||||||
|
|
||||||
|
const status = data.status[0];
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
currentTime: status.currentTime
|
||||||
|
, _lastCurrentTime: Date.now() / 1000
|
||||||
|
, customData: status.customData
|
||||||
|
, _volumeLevel: status.volume.level
|
||||||
|
, _volumeMuted: status.volume.muted
|
||||||
|
, playbackRate: status.playbackRate
|
||||||
|
, playerState: status.playerState
|
||||||
|
, repeatMode: status.repeatMode
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status.media) {
|
||||||
|
messageData.media = status.media;
|
||||||
|
}
|
||||||
|
if (status.mediaSessionId) {
|
||||||
|
messageData.mediaSessionId = status.mediaSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendMessage("shim:media/update", messageData);
|
||||||
|
|
||||||
|
// Update ID
|
||||||
|
if (status.mediaSessionId) {
|
||||||
|
this.mediaSessionId = status.mediaSessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage (subject, data = {}) {
|
||||||
|
sendMessage({
|
||||||
|
subject
|
||||||
|
, data
|
||||||
|
, _id: this._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
107
app/src/transforms.js
Executable file
107
app/src/transforms.js
Executable file
@@ -0,0 +1,107 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { Transform } = require("stream");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a handler function that implements the transform
|
||||||
|
* and calls the transform callback.
|
||||||
|
*/
|
||||||
|
const response = (handler) => new Transform({
|
||||||
|
readableObjectMode: true
|
||||||
|
, writableObjectMode: true
|
||||||
|
|
||||||
|
, transform (chunk, encoding, callback) {
|
||||||
|
|
||||||
|
Promise.resolve(handler(chunk, callback))
|
||||||
|
.then(response => {
|
||||||
|
if (response) {
|
||||||
|
callback(null, response);
|
||||||
|
} else {
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes input, decodes the message string, parses as JSON
|
||||||
|
* and outputs the parsed result.
|
||||||
|
*/
|
||||||
|
const decode = new Transform({
|
||||||
|
readableObjectMode: true
|
||||||
|
|
||||||
|
, transform (chunk, encoding, callback) {
|
||||||
|
// Setup persistent data
|
||||||
|
if (!this.hasOwnProperty("buf")
|
||||||
|
&& !this.hasOwnProperty("message_length")) {
|
||||||
|
|
||||||
|
this.buf = new Buffer(0);
|
||||||
|
this.message_length = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append next chunk to buffer
|
||||||
|
this.buf = Buffer.concat([ this.buf, chunk ]);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (this.message_length === null) {
|
||||||
|
if (this.buf.length >= 4) {
|
||||||
|
|
||||||
|
// Read message length
|
||||||
|
this.message_length = this.buf.readUInt32LE(0);
|
||||||
|
|
||||||
|
// Offset buffer
|
||||||
|
this.buf = this.buf.slice(4);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.buf.length >= this.message_length) {
|
||||||
|
const message = JSON.parse(this.buf.slice(
|
||||||
|
0, this.message_length));
|
||||||
|
|
||||||
|
this.push(message);
|
||||||
|
|
||||||
|
// Cleanup persistent data
|
||||||
|
this.buf = this.buf.slice(this.message_length);
|
||||||
|
this.message_length = null;
|
||||||
|
|
||||||
|
// Parse next message
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No complete messages left
|
||||||
|
callback();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes input, encodes the message length and content and
|
||||||
|
* outputs the encoded result.
|
||||||
|
*/
|
||||||
|
const encode = new Transform({
|
||||||
|
writableObjectMode: true
|
||||||
|
|
||||||
|
, transform (chunk, encoding, callback) {
|
||||||
|
const message_length = new Buffer(4);
|
||||||
|
const message = new Buffer(JSON.stringify(chunk));
|
||||||
|
|
||||||
|
// Write message length
|
||||||
|
message_length.writeUInt32LE(message.length, 0);
|
||||||
|
|
||||||
|
// Output joined message length and content
|
||||||
|
callback(null, Buffer.concat([message_length, message]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
response
|
||||||
|
, decode
|
||||||
|
, encode
|
||||||
|
};
|
||||||
3
app/uninstall.sh
Executable file
3
app/uninstall.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# TODO
|
||||||
5190
ext/package-lock.json
generated
Normal file
5190
ext/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
ext/package.json
Normal file
22
ext/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack",
|
||||||
|
"watch": "webpack -d --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.26.0",
|
||||||
|
"babel-loader": "^7.1.2",
|
||||||
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
|
"babel-plugin-transform-do-expressions": "^6.22.0",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
|
"babel-preset-env": "^1.6.1",
|
||||||
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"copy-webpack-plugin": "^4.3.1",
|
||||||
|
"react": "^16.2.0",
|
||||||
|
"react-dom": "^16.2.0",
|
||||||
|
"webpack": "^3.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": "^3.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
ext/src/_locales/en/messages.json
Executable file
19
ext/src/_locales/en/messages.json
Executable 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
19
ext/src/compat/youtube.js
Normal 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
12
ext/src/content.js
Normal 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
3
ext/src/contentSetup.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
window.wrappedJSObject.chrome = cloneInto({}, window);
|
||||||
11
ext/src/lib/utils.js
Normal file
11
ext/src/lib/utils.js
Normal 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
211
ext/src/main.js
Executable 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
34
ext/src/manifest.json
Executable 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
253
ext/src/mediaCast.js
Normal 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
52
ext/src/popup/index.css
Executable 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
11
ext/src/popup/index.html
Executable 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
146
ext/src/popup/index.js
Executable 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"));
|
||||||
16
ext/src/shim/cast/classes/ApiConfig.js
Executable file
16
ext/src/shim/cast/classes/ApiConfig.js
Executable 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
12
ext/src/shim/cast/classes/DialRequest.js
Executable file
12
ext/src/shim/cast/classes/DialRequest.js
Executable 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
14
ext/src/shim/cast/classes/Error.js
Executable file
14
ext/src/shim/cast/classes/Error.js
Executable 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
11
ext/src/shim/cast/classes/Image.js
Executable file
11
ext/src/shim/cast/classes/Image.js
Executable 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
24
ext/src/shim/cast/classes/Receiver.js
Executable file
24
ext/src/shim/cast/classes/Receiver.js
Executable 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
10
ext/src/shim/cast/classes/ReceiverDisplayStatus.js
Executable file
10
ext/src/shim/cast/classes/ReceiverDisplayStatus.js
Executable 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
10
ext/src/shim/cast/classes/SenderApplication.js
Executable file
10
ext/src/shim/cast/classes/SenderApplication.js
Executable 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
319
ext/src/shim/cast/classes/Session.js
Executable file
319
ext/src/shim/cast/classes/Session.js
Executable 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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ext/src/shim/cast/classes/SessionRequest.js
Executable file
20
ext/src/shim/cast/classes/SessionRequest.js
Executable 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
10
ext/src/shim/cast/classes/Timeout.js
Executable file
10
ext/src/shim/cast/classes/Timeout.js
Executable 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
13
ext/src/shim/cast/classes/Volume.js
Executable file
13
ext/src/shim/cast/classes/Volume.js
Executable 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
28
ext/src/shim/cast/classes/index.js
Executable file
28
ext/src/shim/cast/classes/index.js
Executable 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
|
||||||
|
};
|
||||||
74
ext/src/shim/cast/enums/index.js
Executable file
74
ext/src/shim/cast/enums/index.js
Executable 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
261
ext/src/shim/cast/index.js
Executable 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
18
ext/src/shim/index.js
Executable 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);
|
||||||
|
}
|
||||||
8
ext/src/shim/media/classes/EditTracksInfoRequest.js
Normal file
8
ext/src/shim/media/classes/EditTracksInfoRequest.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
ext/src/shim/media/classes/GenericMediaMetadata.js
Normal file
11
ext/src/shim/media/classes/GenericMediaMetadata.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
ext/src/shim/media/classes/GetStatusRequest.js
Normal file
8
ext/src/shim/media/classes/GetStatusRequest.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default class GetStatusRequest {
|
||||||
|
constructor () {
|
||||||
|
castConsole.info('GetStatusRequest');
|
||||||
|
this.customData = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
11
ext/src/shim/media/classes/LoadRequest.js
Normal file
11
ext/src/shim/media/classes/LoadRequest.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
202
ext/src/shim/media/classes/Media.js
Normal file
202
ext/src/shim/media/classes/Media.js
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
16
ext/src/shim/media/classes/MediaInfo.js
Normal file
16
ext/src/shim/media/classes/MediaInfo.js
Normal 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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
12
ext/src/shim/media/classes/MovieMediaMetadata.js
Normal file
12
ext/src/shim/media/classes/MovieMediaMetadata.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
ext/src/shim/media/classes/MusicTrackMediaMetadata.js
Normal file
17
ext/src/shim/media/classes/MusicTrackMediaMetadata.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
ext/src/shim/media/classes/PauseRequest.js
Normal file
7
ext/src/shim/media/classes/PauseRequest.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default class PauseRequest {
|
||||||
|
constructor () {
|
||||||
|
this.customData = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
16
ext/src/shim/media/classes/PhotoMediaMetadata.js
Normal file
16
ext/src/shim/media/classes/PhotoMediaMetadata.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
ext/src/shim/media/classes/PlayRequest.js
Normal file
7
ext/src/shim/media/classes/PlayRequest.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default class PlayRequest {
|
||||||
|
constructor () {
|
||||||
|
this.customData = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
9
ext/src/shim/media/classes/QueueInsertItemsRequest.js
Normal file
9
ext/src/shim/media/classes/QueueInsertItemsRequest.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default class QueueInsertItemsRequest {
|
||||||
|
constructor (itemsToInsert) {
|
||||||
|
this.customData = {};
|
||||||
|
this.insertBefore = null;
|
||||||
|
this.items = itemsToInsert;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
ext/src/shim/media/classes/QueueItem.js
Normal file
13
ext/src/shim/media/classes/QueueItem.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
ext/src/shim/media/classes/QueueLoadRequest.js
Normal file
12
ext/src/shim/media/classes/QueueLoadRequest.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
ext/src/shim/media/classes/QueueRemoveItemsRequest.js
Normal file
8
ext/src/shim/media/classes/QueueRemoveItemsRequest.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default class QueueRemoveItemsRequest {
|
||||||
|
constructor (itemIdsToRemove) {
|
||||||
|
this.customData = {};
|
||||||
|
this.itemIds = itemIdsToRemove;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
ext/src/shim/media/classes/QueueReorderItemsRequest.js
Normal file
10
ext/src/shim/media/classes/QueueReorderItemsRequest.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
ext/src/shim/media/classes/QueueSetPropertiesRequest.js
Normal file
11
ext/src/shim/media/classes/QueueSetPropertiesRequest.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
ext/src/shim/media/classes/QueueUpdateItemsRequest.js
Normal file
8
ext/src/shim/media/classes/QueueUpdateItemsRequest.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default class QueueUpdateItemsRequest {
|
||||||
|
constructor () {
|
||||||
|
this.customData = {};
|
||||||
|
this.items = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
9
ext/src/shim/media/classes/SeekRequest.js
Normal file
9
ext/src/shim/media/classes/SeekRequest.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default class SeekRequest {
|
||||||
|
constructor () {
|
||||||
|
this.currentTime = null;
|
||||||
|
this.customData = {};
|
||||||
|
this.resumeState = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
ext/src/shim/media/classes/StopRequest.js
Normal file
7
ext/src/shim/media/classes/StopRequest.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default class StopRequest {
|
||||||
|
constructor () {
|
||||||
|
this.customData = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
18
ext/src/shim/media/classes/TextTrackStyle.js
Normal file
18
ext/src/shim/media/classes/TextTrackStyle.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
ext/src/shim/media/classes/Track.js
Normal file
14
ext/src/shim/media/classes/Track.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
ext/src/shim/media/classes/TvShowMediaMetadata.js
Normal file
13
ext/src/shim/media/classes/TvShowMediaMetadata.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
ext/src/shim/media/classes/VolumeRequest.js
Normal file
8
ext/src/shim/media/classes/VolumeRequest.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default class VolumeRequest {
|
||||||
|
constructor (volume) {
|
||||||
|
this.volume = volume;
|
||||||
|
this.customData = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
92
ext/src/shim/media/enums/index.js
Executable file
92
ext/src/shim/media/enums/index.js
Executable 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
86
ext/src/shim/media/index.js
Executable 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"
|
||||||
|
};
|
||||||
15
ext/src/shim/messageBridge.js
Normal file
15
ext/src/shim/messageBridge.js
Normal 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
10
ext/src/shim/state.js
Executable 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
7
ext/src/shim/timeout.js
Executable 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;
|
||||||
57
ext/webpack.config.js
Executable file
57
ext/webpack.config.js
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const webpack_copy = require("copy-webpack-plugin");
|
||||||
|
|
||||||
|
|
||||||
|
const include_path = path.resolve(__dirname, "src");
|
||||||
|
const output_path = path.resolve(__dirname, "dist");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
"main" : `${include_path}/main.js`
|
||||||
|
, "popup/bundle" : `${include_path}/popup/index.js`
|
||||||
|
, "shim/bundle" : `${include_path}/shim/index.js`
|
||||||
|
, "content" : `${include_path}/content.js`
|
||||||
|
, "contentSetup" : `${include_path}/contentSetup.js`
|
||||||
|
, "mediaCast" : `${include_path}/mediaCast.js`
|
||||||
|
, "compat/youtube" : `${include_path}/compat/youtube.js`
|
||||||
|
}
|
||||||
|
, output: {
|
||||||
|
filename: "[name].js"
|
||||||
|
, path: `${output_path}`
|
||||||
|
}
|
||||||
|
, plugins: [
|
||||||
|
new webpack.optimize.UglifyJsPlugin()
|
||||||
|
//, new webpack.optimize.CommonsChunkPlugin("lib/init.bundle")
|
||||||
|
, new webpack.DefinePlugin({
|
||||||
|
"process.env.NODE_ENV": `"production"`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ext copy assets
|
||||||
|
, new webpack_copy([{
|
||||||
|
from: `${include_path}`
|
||||||
|
, to: `${output_path}`
|
||||||
|
, ignore: [ "*.js" ]
|
||||||
|
}])
|
||||||
|
]
|
||||||
|
, devtool: "source-map"
|
||||||
|
, module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.js/
|
||||||
|
, include: `${include_path}`
|
||||||
|
, loader: "babel-loader"
|
||||||
|
, options: {
|
||||||
|
presets: [ "react" ]
|
||||||
|
, plugins: [
|
||||||
|
"transform-class-properties"
|
||||||
|
, "transform-do-expressions"
|
||||||
|
, "transform-object-rest-spread"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
61
webpack.config.js
Normal file
61
webpack.config.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const webpack_copy = require("copy-webpack-plugin");
|
||||||
|
|
||||||
|
|
||||||
|
const include_path = path.resolve(__dirname, "src");
|
||||||
|
const output_path = path.resolve(__dirname, "dist");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
"main" : `${include_path}/ext/main.js`
|
||||||
|
, "popup/bundle" : `${include_path}/ext/popup/index.js`
|
||||||
|
, "shim/bundle" : `${include_path}/ext/shim/index.js`
|
||||||
|
, "content" : `${include_path}/ext/content.js`
|
||||||
|
, "contentSetup" : `${include_path}/ext/contentSetup.js`
|
||||||
|
, "mediaCast" : `${include_path}/ext/mediaCast.js`
|
||||||
|
, "compat/youtube" : `${include_path}/ext/compat/youtube.js`
|
||||||
|
}
|
||||||
|
, output: {
|
||||||
|
filename: "[name].js"
|
||||||
|
, path: `${output_path}/ext`
|
||||||
|
}
|
||||||
|
, plugins: [
|
||||||
|
new webpack.optimize.UglifyJsPlugin()
|
||||||
|
//, new webpack.optimize.CommonsChunkPlugin("lib/init.bundle")
|
||||||
|
, new webpack.DefinePlugin({
|
||||||
|
"process.env.NODE_ENV": `"production"`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ext copy assets
|
||||||
|
, new webpack_copy([{
|
||||||
|
from: `${include_path}/ext`
|
||||||
|
, to: `${output_path}/ext`
|
||||||
|
, ignore: [ "*.js" ]
|
||||||
|
}, {
|
||||||
|
from: `${include_path}`
|
||||||
|
, to: `${output_path}`
|
||||||
|
, ignore: [ "app" ]
|
||||||
|
}])
|
||||||
|
]
|
||||||
|
, devtool: "source-map"
|
||||||
|
, module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.js/
|
||||||
|
, include: `${include_path}/ext`
|
||||||
|
, loader: "babel-loader"
|
||||||
|
, options: {
|
||||||
|
presets: [ "react" ]
|
||||||
|
, plugins: [
|
||||||
|
"transform-class-properties"
|
||||||
|
, "transform-do-expressions"
|
||||||
|
, "transform-object-rest-spread"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user