mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
prettier: Re-format .ts files
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none"
|
||||
"trailingComma": "none",
|
||||
"quoteProps": "consistent"
|
||||
}
|
||||
|
||||
4
app/@types/bplist-creator/index.d.ts
vendored
4
app/@types/bplist-creator/index.d.ts
vendored
@@ -3,13 +3,13 @@
|
||||
declare module "bplist-creator" {
|
||||
import Buffer from "buffer";
|
||||
|
||||
function bplist (dicts: object): Buffer;
|
||||
function bplist(dicts: object): Buffer;
|
||||
export = bplist;
|
||||
|
||||
namespace bplist {
|
||||
export class Real {
|
||||
public value: number;
|
||||
constructor (value: number);
|
||||
constructor(value: number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
app/@types/bplist-parser/index.d.ts
vendored
15
app/@types/bplist-parser/index.d.ts
vendored
@@ -7,17 +7,16 @@ declare module "bplist-parser" {
|
||||
export var maxObjectCount: number;
|
||||
|
||||
export class UID {
|
||||
constructor (id: number);
|
||||
constructor(id: number);
|
||||
UID: number;
|
||||
}
|
||||
|
||||
type ParseFileCallback = (
|
||||
err: string
|
||||
, result?: Buffer) => void;
|
||||
type ParseFileCallback = (err: string, result?: Buffer) => void;
|
||||
|
||||
export function parseFile (
|
||||
fileNameOrBuffer: Buffer | string
|
||||
, callback: ParseFileCallback): void;
|
||||
export function parseFile(
|
||||
fileNameOrBuffer: Buffer | string,
|
||||
callback: ParseFileCallback
|
||||
): void;
|
||||
|
||||
export function parseBuffer (buffer: Buffer): any;
|
||||
export function parseBuffer(buffer: Buffer): any;
|
||||
}
|
||||
|
||||
67
app/@types/castv2/index.d.ts
vendored
67
app/@types/castv2/index.d.ts
vendored
@@ -10,7 +10,6 @@ declare module "castv2" {
|
||||
|
||||
type CallbackFunction = () => void;
|
||||
|
||||
|
||||
export interface Channel extends EventEmitter {
|
||||
bus: Client;
|
||||
sourceId: string;
|
||||
@@ -18,51 +17,55 @@ declare module "castv2" {
|
||||
namespace: string;
|
||||
encoding: string;
|
||||
|
||||
send (data: any): void;
|
||||
close (): void;
|
||||
send(data: any): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export interface DeviceAuthMessage {
|
||||
parse (data: any): any;
|
||||
serialize (data: any): any;
|
||||
parse(data: any): any;
|
||||
serialize(data: any): any;
|
||||
}
|
||||
|
||||
|
||||
export class Client extends EventEmitter {
|
||||
public connect (
|
||||
options: ClientConnectOptions | string
|
||||
, callback?: CallbackFunction): void;
|
||||
public connect(
|
||||
options: ClientConnectOptions | string,
|
||||
callback?: CallbackFunction
|
||||
): void;
|
||||
|
||||
public close (): void;
|
||||
public close(): void;
|
||||
|
||||
public send (
|
||||
sourceId: string
|
||||
, destinationId: string
|
||||
, namespace: string
|
||||
, data: Buffer | string): void;
|
||||
public send(
|
||||
sourceId: string,
|
||||
destinationId: string,
|
||||
namespace: string,
|
||||
data: Buffer | string
|
||||
): void;
|
||||
|
||||
public createChannel (
|
||||
sourceId: string
|
||||
, destinationId: string
|
||||
, namespace: string
|
||||
, encoding: string): Channel;
|
||||
public createChannel(
|
||||
sourceId: string,
|
||||
destinationId: string,
|
||||
namespace: string,
|
||||
encoding: string
|
||||
): Channel;
|
||||
}
|
||||
|
||||
export class Server extends EventEmitter {
|
||||
constructor (options: object);
|
||||
constructor(options: object);
|
||||
|
||||
public listen (
|
||||
port: number
|
||||
, host: string
|
||||
, callback?: CallbackFunction): void;
|
||||
public listen(
|
||||
port: number,
|
||||
host: string,
|
||||
callback?: CallbackFunction
|
||||
): void;
|
||||
|
||||
public send (
|
||||
clientId: string
|
||||
, sourceId: string
|
||||
, destinationId: string
|
||||
, namespace: string
|
||||
, data: Buffer | string): void;
|
||||
public send(
|
||||
clientId: string,
|
||||
sourceId: string,
|
||||
destinationId: string,
|
||||
namespace: string,
|
||||
data: Buffer | string
|
||||
): void;
|
||||
|
||||
public close (): void;
|
||||
public close(): void;
|
||||
}
|
||||
}
|
||||
|
||||
53
app/@types/fast-srp-hap/index.d.ts
vendored
53
app/@types/fast-srp-hap/index.d.ts
vendored
@@ -12,44 +12,39 @@ declare module "fast-srp-hap" {
|
||||
|
||||
export const params: { [key: number]: Param };
|
||||
|
||||
type GenKeyCallback = (
|
||||
err: string
|
||||
, buf: Buffer) => void;
|
||||
type GenKeyCallback = (err: string, buf: Buffer) => void;
|
||||
|
||||
export function genKey (
|
||||
bytes: number
|
||||
, callback: GenKeyCallback): void;
|
||||
export function genKey(bytes: number, callback: GenKeyCallback): void;
|
||||
|
||||
export function computeVerifier (
|
||||
params: object
|
||||
, salt: Buffer
|
||||
, I: Buffer
|
||||
, P: Buffer): Buffer;
|
||||
export function computeVerifier(
|
||||
params: object,
|
||||
salt: Buffer,
|
||||
I: Buffer,
|
||||
P: Buffer
|
||||
): Buffer;
|
||||
|
||||
export class Client {
|
||||
constructor (
|
||||
params: object
|
||||
, salt_buf: Buffer
|
||||
, identity_buf: Buffer
|
||||
, password_buf: Buffer
|
||||
, secret1_buf: Buffer);
|
||||
constructor(
|
||||
params: object,
|
||||
salt_buf: Buffer,
|
||||
identity_buf: Buffer,
|
||||
password_buf: Buffer,
|
||||
secret1_buf: Buffer
|
||||
);
|
||||
|
||||
computeA (): Buffer;
|
||||
computeA(): Buffer;
|
||||
setB(B_buf: Buffer): void;
|
||||
computeM1 (): Buffer;
|
||||
checkM2 (serverM2_buf: Buffer): void;
|
||||
computeK (): Buffer;
|
||||
computeM1(): Buffer;
|
||||
checkM2(serverM2_buf: Buffer): void;
|
||||
computeK(): Buffer;
|
||||
}
|
||||
|
||||
export class Server {
|
||||
constructor (
|
||||
params: object
|
||||
, verifier_buf: Buffer
|
||||
, secret2_buf: Buffer);
|
||||
constructor(params: object, verifier_buf: Buffer, secret2_buf: Buffer);
|
||||
|
||||
computeB (): Buffer;
|
||||
setA (A_buf: Buffer): void;
|
||||
checkM1 (clientM1_buf: Buffer): Buffer;
|
||||
computeK (): Buffer;
|
||||
computeB(): Buffer;
|
||||
setA(A_buf: Buffer): void;
|
||||
checkM1(clientM1_buf: Buffer): Buffer;
|
||||
computeK(): Buffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import fetch, { Headers } from "node-fetch";
|
||||
import nacl from "tweetnacl";
|
||||
import bplist from "./bplist";
|
||||
|
||||
|
||||
const AIRPLAY_PORT = 7000;
|
||||
const MIMETYPE_BPLIST = "application/x-apple-binary-plist";
|
||||
|
||||
@@ -27,10 +26,10 @@ export class AirPlayAuthCredentials {
|
||||
public clientPk: Uint8Array;
|
||||
|
||||
constructor(
|
||||
clientId?: string
|
||||
, clientSk?: Uint8Array
|
||||
, clientPk?: Uint8Array) {
|
||||
|
||||
clientId?: string,
|
||||
clientSk?: Uint8Array,
|
||||
clientPk?: Uint8Array
|
||||
) {
|
||||
if (clientId && clientSk && clientPk) {
|
||||
this.clientId = clientId;
|
||||
this.clientSk = clientSk;
|
||||
@@ -74,8 +73,7 @@ export class AirPlayAuth {
|
||||
*/
|
||||
public async finishPairing(pin: string) {
|
||||
// Stage 1 response
|
||||
const { pk: serverPk
|
||||
, salt: serverSalt } = await this.pairSetupPin1();
|
||||
const { pk: serverPk, salt: serverSalt } = await this.pairSetupPin1();
|
||||
|
||||
// SRP params must 2048-bit SHA1
|
||||
const srpParams = srp6a.params[2048];
|
||||
@@ -83,19 +81,21 @@ export class AirPlayAuth {
|
||||
|
||||
// Create SRP client
|
||||
const srpClient = new srp6a.Client(
|
||||
srpParams // Params
|
||||
, serverSalt // Receiver salt
|
||||
, Buffer.from(this.credentials.clientId) // Username
|
||||
, Buffer.from(pin) // Password (receiver pin)
|
||||
, Buffer.from(this.credentials.clientSk)); // Client secret key
|
||||
srpParams, // Params
|
||||
serverSalt, // Receiver salt
|
||||
Buffer.from(this.credentials.clientId), // Username
|
||||
Buffer.from(pin), // Password (receiver pin)
|
||||
Buffer.from(this.credentials.clientSk) // Client secret key
|
||||
);
|
||||
|
||||
// Add receiver's public key
|
||||
srpClient.setB(serverPk);
|
||||
|
||||
// Stage 2 response
|
||||
await this.pairSetupPin2(
|
||||
srpClient.computeA() // SRP public key
|
||||
, srpClient.computeM1()); // SRP proof
|
||||
srpClient.computeA(), // SRP public key
|
||||
srpClient.computeM1() // SRP proof
|
||||
);
|
||||
|
||||
// Stage 3 response
|
||||
await this.pairSetupPin3(srpClient.computeK());
|
||||
@@ -108,12 +108,10 @@ export class AirPlayAuth {
|
||||
* its public key / salt.
|
||||
*/
|
||||
public async pairSetupPin1(): Promise<any> {
|
||||
const [ response ] = await this.sendPostRequestBplist(
|
||||
"/pair-setup-pin"
|
||||
, {
|
||||
method: "pin"
|
||||
, user: this.credentials.clientId
|
||||
});
|
||||
const [response] = await this.sendPostRequestBplist("/pair-setup-pin", {
|
||||
method: "pin",
|
||||
user: this.credentials.clientId
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -125,13 +123,11 @@ export class AirPlayAuth {
|
||||
* public keys, sending them to the receiver and receiving its
|
||||
* proof.
|
||||
*/
|
||||
public async pairSetupPin2(
|
||||
pk: Buffer
|
||||
, proof: Buffer): Promise<any> {
|
||||
|
||||
const [ response ] = await this.sendPostRequestBplist(
|
||||
"/pair-setup-pin"
|
||||
, { pk, proof });
|
||||
public async pairSetupPin2(pk: Buffer, proof: Buffer): Promise<any> {
|
||||
const [response] = await this.sendPostRequestBplist("/pair-setup-pin", {
|
||||
pk,
|
||||
proof
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -144,17 +140,19 @@ export class AirPlayAuth {
|
||||
* responds confirming the pairing is complete.
|
||||
*/
|
||||
public async pairSetupPin3(
|
||||
sharedSecretHash: crypto.BinaryLike): Promise<any> {
|
||||
|
||||
sharedSecretHash: crypto.BinaryLike
|
||||
): Promise<any> {
|
||||
// Create AES key
|
||||
const aesKey = crypto.createHash("sha512")
|
||||
const aesKey = crypto
|
||||
.createHash("sha512")
|
||||
.update("Pair-Setup-AES-Key")
|
||||
.update(sharedSecretHash)
|
||||
.digest()
|
||||
.slice(0, 16);
|
||||
|
||||
// Create AES IV
|
||||
const aesIv = crypto.createHash("sha512")
|
||||
const aesIv = crypto
|
||||
.createHash("sha512")
|
||||
.update("Pair-Setup-AES-IV")
|
||||
.update(sharedSecretHash)
|
||||
.digest()
|
||||
@@ -162,7 +160,6 @@ export class AirPlayAuth {
|
||||
|
||||
aesIv[15]++;
|
||||
|
||||
|
||||
const cipher = crypto.createCipheriv("aes-128-gcm", aesKey, aesIv);
|
||||
|
||||
// Encode client public key
|
||||
@@ -170,23 +167,23 @@ export class AirPlayAuth {
|
||||
cipher.final();
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
const [ response ] = await this.sendPostRequestBplist(
|
||||
"/pair-setup-pin"
|
||||
, { epk, authTag });
|
||||
const [response] = await this.sendPostRequestBplist("/pair-setup-pin", {
|
||||
epk,
|
||||
authTag
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends a POST request to receiver and returns the
|
||||
* response.
|
||||
*/
|
||||
public async sendPostRequest(
|
||||
path: string
|
||||
, contentType?: string
|
||||
, data?: Buffer | string): Promise<any> {
|
||||
|
||||
path: string,
|
||||
contentType?: string,
|
||||
data?: Buffer | string
|
||||
): Promise<any> {
|
||||
// Create URL from base receiver URL and path
|
||||
const requestUrl = new URL(path, this.baseUrl);
|
||||
|
||||
@@ -200,9 +197,9 @@ export class AirPlayAuth {
|
||||
}
|
||||
|
||||
const response = await fetch(requestUrl.href, {
|
||||
method: "POST"
|
||||
, headers: requestHeaders
|
||||
, body: data
|
||||
method: "POST",
|
||||
headers: requestHeaders,
|
||||
body: data
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -217,16 +214,17 @@ export class AirPlayAuth {
|
||||
* receiver, then decodes and returns the response.
|
||||
*/
|
||||
public async sendPostRequestBplist(
|
||||
path: string
|
||||
, data?: object): Promise<any> {
|
||||
|
||||
path: string,
|
||||
data?: object
|
||||
): Promise<any> {
|
||||
// Convert data to compatible type
|
||||
const requestBody = data
|
||||
? bplist.create(data)
|
||||
: undefined;
|
||||
const requestBody = data ? bplist.create(data) : undefined;
|
||||
|
||||
const response = await this.sendPostRequest(
|
||||
path, MIMETYPE_BPLIST, requestBody);
|
||||
path,
|
||||
MIMETYPE_BPLIST,
|
||||
requestBody
|
||||
);
|
||||
|
||||
// Convert response data to Buffer for bplist-parser
|
||||
return bplist.parse.parseBuffer(response);
|
||||
|
||||
@@ -7,14 +7,12 @@ import { sendMessage } from "../../lib/nativeMessaging";
|
||||
import { ReceiverDevice } from "../../types";
|
||||
import { ReceiverApplication, ReceiverMessage, SenderMessage } from "./types";
|
||||
|
||||
|
||||
export const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection";
|
||||
export const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
|
||||
export const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
|
||||
|
||||
const HEARTBEAT_INTERVAL = 5000;
|
||||
|
||||
|
||||
class CastClient {
|
||||
protected client = new Client();
|
||||
|
||||
@@ -22,18 +20,26 @@ class CastClient {
|
||||
protected heartbeatChannel?: Channel;
|
||||
protected heartbeatIntervalId?: NodeJS.Timeout;
|
||||
|
||||
constructor(protected sourceId = "sender-0"
|
||||
, protected destinationId = "receiver-0") {}
|
||||
constructor(
|
||||
protected sourceId = "sender-0",
|
||||
protected destinationId = "receiver-0"
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a channel on the client connection with a given
|
||||
* namespace.
|
||||
*/
|
||||
createChannel(namespace: string
|
||||
, sourceId = this.sourceId
|
||||
, destinationId = this.destinationId) {
|
||||
|
||||
return this.client.createChannel(sourceId, destinationId, namespace, "JSON");
|
||||
createChannel(
|
||||
namespace: string,
|
||||
sourceId = this.sourceId,
|
||||
destinationId = this.destinationId
|
||||
) {
|
||||
return this.client.createChannel(
|
||||
sourceId,
|
||||
destinationId,
|
||||
namespace,
|
||||
"JSON"
|
||||
);
|
||||
}
|
||||
|
||||
connect(host: string, port: number, onHeartbeat?: () => void) {
|
||||
@@ -66,7 +72,6 @@ class CastClient {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type OnSessionCreatedCallback = (sessionId: string) => void;
|
||||
|
||||
export default class Session extends CastClient {
|
||||
@@ -93,12 +98,17 @@ export default class Session extends CastClient {
|
||||
|
||||
private onSessionCreated?: OnSessionCreatedCallback;
|
||||
|
||||
|
||||
private establishAppConnection(transportId: string) {
|
||||
this.transportConnection = this.createChannel(
|
||||
NS_CONNECTION, this.sourceId, transportId);
|
||||
NS_CONNECTION,
|
||||
this.sourceId,
|
||||
transportId
|
||||
);
|
||||
this.transportHeartbeat = this.createChannel(
|
||||
NS_HEARTBEAT, this.sourceId, transportId);
|
||||
NS_HEARTBEAT,
|
||||
this.sourceId,
|
||||
transportId
|
||||
);
|
||||
|
||||
this.transportConnection.send({ type: "CONNECT" });
|
||||
}
|
||||
@@ -111,7 +121,8 @@ export default class Session extends CastClient {
|
||||
case "RECEIVER_STATUS": {
|
||||
const { status } = message;
|
||||
const application = status.applications?.find(
|
||||
app => app.appId === this.appId);
|
||||
app => app.appId === this.appId
|
||||
);
|
||||
|
||||
/**
|
||||
* If application isn't set, still waiting on the launch
|
||||
@@ -133,20 +144,20 @@ export default class Session extends CastClient {
|
||||
const { friendlyName } = this.receiverDevice;
|
||||
|
||||
sendMessage({
|
||||
subject: "shim:castSessionCreated"
|
||||
, data: {
|
||||
sessionId: this.sessionId
|
||||
, statusText: application.statusText
|
||||
, namespaces: application.namespaces
|
||||
, volume: status.volume
|
||||
, appId: application.appId
|
||||
, displayName: application.displayName
|
||||
, receiverFriendlyName: friendlyName
|
||||
, transportId: this.sessionId
|
||||
subject: "shim:castSessionCreated",
|
||||
data: {
|
||||
sessionId: this.sessionId,
|
||||
statusText: application.statusText,
|
||||
namespaces: application.namespaces,
|
||||
volume: status.volume,
|
||||
appId: application.appId,
|
||||
displayName: application.displayName,
|
||||
receiverFriendlyName: friendlyName,
|
||||
transportId: this.sessionId,
|
||||
|
||||
// TODO: Fix this
|
||||
, senderApps: []
|
||||
, appImages: []
|
||||
// TODO: Fix this
|
||||
senderApps: [],
|
||||
appImages: []
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -161,12 +172,12 @@ export default class Session extends CastClient {
|
||||
}
|
||||
|
||||
sendMessage({
|
||||
subject: "shim:castSessionUpdated"
|
||||
, data: {
|
||||
sessionId: this.sessionId
|
||||
, statusText: application.statusText
|
||||
, namespaces: application.namespaces
|
||||
, volume: message.status.volume
|
||||
subject: "shim:castSessionUpdated",
|
||||
data: {
|
||||
sessionId: this.sessionId,
|
||||
statusText: application.statusText,
|
||||
namespaces: application.namespaces,
|
||||
volume: message.status.volume
|
||||
}
|
||||
});
|
||||
|
||||
@@ -179,13 +190,16 @@ export default class Session extends CastClient {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sendMessage(namespace: string, message: unknown) {
|
||||
let channel = this.namespaceChannelMap.get(namespace);
|
||||
if (!channel) {
|
||||
channel = this.createChannel(
|
||||
namespace, this.sourceId, this.transportId);
|
||||
namespace,
|
||||
this.sourceId,
|
||||
this.transportId
|
||||
);
|
||||
|
||||
channel.on("message", messageData => {
|
||||
if (!this.sessionId) {
|
||||
@@ -195,11 +209,11 @@ export default class Session extends CastClient {
|
||||
messageData = JSON.stringify(messageData);
|
||||
|
||||
sendMessage({
|
||||
subject: "shim:receivedCastSessionMessage"
|
||||
, data: {
|
||||
sessionId: this.sessionId
|
||||
, namespace
|
||||
, messageData
|
||||
subject: "shim:receivedCastSessionMessage",
|
||||
data: {
|
||||
sessionId: this.sessionId,
|
||||
namespace,
|
||||
messageData
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -222,25 +236,24 @@ export default class Session extends CastClient {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
constructor(public appId: string
|
||||
, public receiverDevice: ReceiverDevice) {
|
||||
|
||||
constructor(public appId: string, public receiverDevice: ReceiverDevice) {
|
||||
super();
|
||||
|
||||
this.client.on("close", () => {
|
||||
if (this.sessionId) {
|
||||
sendMessage({
|
||||
subject: "shim:castSessionStopped"
|
||||
, data: { sessionId: this.sessionId }
|
||||
subject: "shim:castSessionStopped",
|
||||
data: { sessionId: this.sessionId }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async connect(host: string
|
||||
, port: number
|
||||
, onSessionCreated?: OnSessionCreatedCallback) {
|
||||
|
||||
async connect(
|
||||
host: string,
|
||||
port: number,
|
||||
onSessionCreated?: OnSessionCreatedCallback
|
||||
) {
|
||||
if (onSessionCreated) {
|
||||
this.onSessionCreated = onSessionCreated;
|
||||
}
|
||||
@@ -253,8 +266,8 @@ export default class Session extends CastClient {
|
||||
});
|
||||
|
||||
this.launchRequestId = this.sendReceiverMessage({
|
||||
type: "LAUNCH"
|
||||
, appId: this.appId
|
||||
type: "LAUNCH",
|
||||
appId: this.appId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ import castv2 from "castv2";
|
||||
import { sendMessage } from "../../lib/nativeMessaging";
|
||||
import { Message } from "../../messaging";
|
||||
|
||||
import Session, { NS_CONNECTION
|
||||
, NS_RECEIVER } from "./Session";
|
||||
|
||||
import Session, { NS_CONNECTION, NS_RECEIVER } from "./Session";
|
||||
|
||||
const sessions = new Map<string, Session>();
|
||||
|
||||
@@ -19,9 +17,12 @@ export function handleCastMessage(message: Message) {
|
||||
// Connect and store with returned ID
|
||||
const session = new Session(appId, receiverDevice);
|
||||
session.connect(
|
||||
receiverDevice.host, receiverDevice.port, sessionId => {
|
||||
sessions.set(sessionId, session);
|
||||
});
|
||||
receiverDevice.host,
|
||||
receiverDevice.port,
|
||||
sessionId => {
|
||||
sessions.set(sessionId, session);
|
||||
}
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -32,10 +33,11 @@ export function handleCastMessage(message: Message) {
|
||||
const session = sessions.get(sessionId);
|
||||
if (!session) {
|
||||
sendMessage({
|
||||
subject: "shim:impl_sendCastMessage"
|
||||
, data: {
|
||||
error: "Session does not exist"
|
||||
, sessionId, messageId
|
||||
subject: "shim:impl_sendCastMessage",
|
||||
data: {
|
||||
error: "Session does not exist",
|
||||
sessionId,
|
||||
messageId
|
||||
}
|
||||
});
|
||||
|
||||
@@ -46,10 +48,11 @@ export function handleCastMessage(message: Message) {
|
||||
session.sendReceiverMessage(messageData);
|
||||
} catch (err) {
|
||||
sendMessage({
|
||||
subject: "shim:impl_sendCastMessage"
|
||||
, data: {
|
||||
error: `Failed to send message (${err})`
|
||||
, sessionId, messageId
|
||||
subject: "shim:impl_sendCastMessage",
|
||||
data: {
|
||||
error: `Failed to send message (${err})`,
|
||||
sessionId,
|
||||
messageId
|
||||
}
|
||||
});
|
||||
|
||||
@@ -58,8 +61,8 @@ export function handleCastMessage(message: Message) {
|
||||
|
||||
// Success
|
||||
sendMessage({
|
||||
subject: "shim:impl_sendCastMessage"
|
||||
, data: { sessionId, messageId }
|
||||
subject: "shim:impl_sendCastMessage",
|
||||
data: { sessionId, messageId }
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -71,10 +74,11 @@ export function handleCastMessage(message: Message) {
|
||||
const session = sessions.get(sessionId);
|
||||
if (!session) {
|
||||
sendMessage({
|
||||
subject: "shim:impl_sendCastMessage"
|
||||
, data: {
|
||||
error: "Session does not exist"
|
||||
, sessionId, messageId
|
||||
subject: "shim:impl_sendCastMessage",
|
||||
data: {
|
||||
error: "Session does not exist",
|
||||
sessionId,
|
||||
messageId
|
||||
}
|
||||
});
|
||||
|
||||
@@ -91,10 +95,11 @@ export function handleCastMessage(message: Message) {
|
||||
session.sendMessage(namespace, messageData);
|
||||
} catch (err) {
|
||||
sendMessage({
|
||||
subject: "shim:impl_sendCastMessage"
|
||||
, data: {
|
||||
error: `Failed to send message (${err})`
|
||||
, sessionId, messageId
|
||||
subject: "shim:impl_sendCastMessage",
|
||||
data: {
|
||||
error: `Failed to send message (${err})`,
|
||||
sessionId,
|
||||
messageId
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,8 +108,8 @@ export function handleCastMessage(message: Message) {
|
||||
|
||||
// Success
|
||||
sendMessage({
|
||||
subject: "shim:impl_sendCastMessage"
|
||||
, data: { sessionId, messageId }
|
||||
subject: "shim:impl_sendCastMessage",
|
||||
data: { sessionId, messageId }
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -119,9 +124,17 @@ export function handleCastMessage(message: Message) {
|
||||
const destinationId = "receiver-0";
|
||||
|
||||
const clientConnection = client.createChannel(
|
||||
sourceId, destinationId, NS_CONNECTION, "JSON");
|
||||
sourceId,
|
||||
destinationId,
|
||||
NS_CONNECTION,
|
||||
"JSON"
|
||||
);
|
||||
const clientReceiver = client.createChannel(
|
||||
sourceId, destinationId, NS_RECEIVER, "JSON");
|
||||
sourceId,
|
||||
destinationId,
|
||||
NS_RECEIVER,
|
||||
"JSON"
|
||||
);
|
||||
|
||||
clientConnection.send({ type: "CONNECT" });
|
||||
clientReceiver.send({ type: "STOP", requestId: 1 });
|
||||
|
||||
@@ -7,18 +7,18 @@ export interface Image {
|
||||
}
|
||||
|
||||
enum Capability {
|
||||
VIDEO_OUT = "video_out"
|
||||
, AUDIO_OUT = "audio_out"
|
||||
, VIDEO_IN = "video_in"
|
||||
, AUDIO_IN = "audio_in"
|
||||
, MULTIZONE_GROUP = "multizone_group"
|
||||
VIDEO_OUT = "video_out",
|
||||
AUDIO_OUT = "audio_out",
|
||||
VIDEO_IN = "video_in",
|
||||
AUDIO_IN = "audio_in",
|
||||
MULTIZONE_GROUP = "multizone_group"
|
||||
}
|
||||
|
||||
enum ReceiverType {
|
||||
CAST = "cast"
|
||||
, DIAL = "dial"
|
||||
, HANGOUT = "hangout"
|
||||
, CUSTOM = "custom"
|
||||
CAST = "cast",
|
||||
DIAL = "dial",
|
||||
HANGOUT = "hangout",
|
||||
CUSTOM = "custom"
|
||||
}
|
||||
|
||||
export interface SenderApplication {
|
||||
@@ -28,9 +28,9 @@ export interface SenderApplication {
|
||||
}
|
||||
|
||||
enum VolumeControlType {
|
||||
ATTENUATION = "attenuation"
|
||||
, FIXED = "fixed"
|
||||
, MASTER = "master"
|
||||
ATTENUATION = "attenuation",
|
||||
FIXED = "fixed",
|
||||
MASTER = "master"
|
||||
}
|
||||
|
||||
export interface Volume {
|
||||
@@ -43,75 +43,74 @@ export interface Volume {
|
||||
// Media
|
||||
|
||||
enum IdleReason {
|
||||
CANCELLED = "CANCELLED"
|
||||
, INTERRUPTED = "INTERRUPTED"
|
||||
, FINISHED = "FINISHED"
|
||||
, ERROR = "ERROR"
|
||||
CANCELLED = "CANCELLED",
|
||||
INTERRUPTED = "INTERRUPTED",
|
||||
FINISHED = "FINISHED",
|
||||
ERROR = "ERROR"
|
||||
}
|
||||
|
||||
enum HlsSegmentFormat {
|
||||
AAC = "aac"
|
||||
, AC3 = "ac3"
|
||||
, MP3 = "mp3"
|
||||
, TS = "ts"
|
||||
, TS_AAC = "ts_aac"
|
||||
, E_AC3 = "e_ac3"
|
||||
, FMP4 = "fmp4"
|
||||
AAC = "aac",
|
||||
AC3 = "ac3",
|
||||
MP3 = "mp3",
|
||||
TS = "ts",
|
||||
TS_AAC = "ts_aac",
|
||||
E_AC3 = "e_ac3",
|
||||
FMP4 = "fmp4"
|
||||
}
|
||||
|
||||
export enum HlsVideoSegmentFormat {
|
||||
MPEG2_TS = "mpeg2_ts"
|
||||
, FMP4 = "fmp4"
|
||||
MPEG2_TS = "mpeg2_ts",
|
||||
FMP4 = "fmp4"
|
||||
}
|
||||
|
||||
enum MetadataType {
|
||||
GENERIC
|
||||
, MOVIE
|
||||
, TV_SHOW
|
||||
, MUSIC_TRACK
|
||||
, PHOTO
|
||||
, AUDIOBOOK_CHAPTER
|
||||
GENERIC,
|
||||
MOVIE,
|
||||
TV_SHOW,
|
||||
MUSIC_TRACK,
|
||||
PHOTO,
|
||||
AUDIOBOOK_CHAPTER
|
||||
}
|
||||
|
||||
enum PlayerState {
|
||||
IDLE = "IDLE"
|
||||
, PLAYING = "PLAYING"
|
||||
, PAUSED = "PAUSED"
|
||||
, BUFFERING = "BUFFERING"
|
||||
IDLE = "IDLE",
|
||||
PLAYING = "PLAYING",
|
||||
PAUSED = "PAUSED",
|
||||
BUFFERING = "BUFFERING"
|
||||
}
|
||||
|
||||
enum RepeatMode {
|
||||
OFF = "REPEAT_OFF"
|
||||
, ALL = "REPEAT_ALL"
|
||||
, SINGLE = "REPEAT_SINGLE"
|
||||
, ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
|
||||
OFF = "REPEAT_OFF",
|
||||
ALL = "REPEAT_ALL",
|
||||
SINGLE = "REPEAT_SINGLE",
|
||||
ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
|
||||
}
|
||||
|
||||
enum ResumeState {
|
||||
PLAYBACK_START = "PLAYBACK_START"
|
||||
, PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
|
||||
PLAYBACK_START = "PLAYBACK_START",
|
||||
PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
|
||||
}
|
||||
|
||||
enum StreamType {
|
||||
BUFFERED = "BUFFERED"
|
||||
, LIVE = "LIVE"
|
||||
, OTHER = "OTHER"
|
||||
BUFFERED = "BUFFERED",
|
||||
LIVE = "LIVE",
|
||||
OTHER = "OTHER"
|
||||
}
|
||||
|
||||
enum TrackType {
|
||||
TEXT = "TEXT"
|
||||
, AUDIO = "AUDIO"
|
||||
, VIDEO = "VIDEO"
|
||||
TEXT = "TEXT",
|
||||
AUDIO = "AUDIO",
|
||||
VIDEO = "VIDEO"
|
||||
}
|
||||
|
||||
export enum UserAction {
|
||||
LIKE = "LIKE"
|
||||
, DISLIKE = "DISLIKE"
|
||||
, FOLLOW = "FOLLOW"
|
||||
, UNFOLLOW = "UNFOLLOW"
|
||||
LIKE = "LIKE",
|
||||
DISLIKE = "DISLIKE",
|
||||
FOLLOW = "FOLLOW",
|
||||
UNFOLLOW = "UNFOLLOW"
|
||||
}
|
||||
|
||||
|
||||
interface Break {
|
||||
breakClipIds: string[];
|
||||
duration?: number;
|
||||
@@ -173,11 +172,11 @@ interface VastAdsRequest {
|
||||
}
|
||||
|
||||
type Metadata =
|
||||
GenericMediaMetadata
|
||||
| MovieMediaMetadata
|
||||
| MusicTrackMediaMetadata
|
||||
| PhotoMediaMetadata
|
||||
| TvShowMediaMetadata;
|
||||
| GenericMediaMetadata
|
||||
| MovieMediaMetadata
|
||||
| MusicTrackMediaMetadata
|
||||
| PhotoMediaMetadata
|
||||
| TvShowMediaMetadata;
|
||||
|
||||
interface MediaInformation {
|
||||
atvEntity?: string;
|
||||
@@ -284,11 +283,11 @@ export interface MediaStatus {
|
||||
playbackRate: number;
|
||||
playerState: PlayerState;
|
||||
idleReason?: IdleReason;
|
||||
items?: QueueItem[]
|
||||
items?: QueueItem[];
|
||||
currentTime: number;
|
||||
supportedMediaCommands: number;
|
||||
repeatMode: RepeatMode;
|
||||
volume: Volume
|
||||
volume: Volume;
|
||||
customData: unknown;
|
||||
}
|
||||
|
||||
@@ -329,23 +328,21 @@ export interface ReceiverStatus {
|
||||
volume: Volume;
|
||||
}
|
||||
|
||||
|
||||
interface ReqBase {
|
||||
requestId: number;
|
||||
}
|
||||
|
||||
// NS: urn:x-cast:com.google.cast.receiver
|
||||
export type SenderMessage =
|
||||
ReqBase & { type: "LAUNCH", appId: string }
|
||||
| ReqBase & { type: "STOP", sessionId: string }
|
||||
| ReqBase & { type: "GET_STATUS" }
|
||||
| ReqBase & { type: "GET_APP_AVAILABILITY", appId: string[] }
|
||||
| ReqBase & { type: "SET_VOLUME", volume: Volume };
|
||||
| (ReqBase & { type: "LAUNCH"; appId: string })
|
||||
| (ReqBase & { type: "STOP"; sessionId: string })
|
||||
| (ReqBase & { type: "GET_STATUS" })
|
||||
| (ReqBase & { type: "GET_APP_AVAILABILITY"; appId: string[] })
|
||||
| (ReqBase & { type: "SET_VOLUME"; volume: Volume });
|
||||
|
||||
export type ReceiverMessage =
|
||||
ReqBase & { type: "RECEIVER_STATUS", status: ReceiverStatus }
|
||||
| ReqBase & { type: "LAUNCH_ERROR", reason: string }
|
||||
|
||||
| (ReqBase & { type: "RECEIVER_STATUS"; status: ReceiverStatus })
|
||||
| (ReqBase & { type: "LAUNCH_ERROR"; reason: string });
|
||||
|
||||
interface MediaReqBase extends ReqBase {
|
||||
mediaSessionId: number;
|
||||
@@ -354,84 +351,84 @@ interface MediaReqBase extends ReqBase {
|
||||
|
||||
// NS: urn:x-cast:com.google.cast.media
|
||||
export type SenderMediaMessage =
|
||||
| MediaReqBase & { type: "PLAY" }
|
||||
| MediaReqBase & { type: "PAUSE" }
|
||||
| MediaReqBase & { type: "MEDIA_GET_STATUS" }
|
||||
| MediaReqBase & { type: "STOP" }
|
||||
| MediaReqBase & { type: "MEDIA_SET_VOLUME", volume: Volume }
|
||||
| MediaReqBase & { type: "SET_PLAYBACK_RATE" , playbackRate: number }
|
||||
| ReqBase & {
|
||||
type: "LOAD"
|
||||
, activeTrackIds: Nullable<number[]>
|
||||
, atvCredentials?: string
|
||||
, atvCredentialsType?: string
|
||||
, autoplay: Nullable<boolean>
|
||||
, currentTime: Nullable<number>
|
||||
, customData?: unknown
|
||||
, media: MediaInformation
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
| MediaReqBase & {
|
||||
type: "SEEK"
|
||||
, resumeState: Nullable<ResumeState>
|
||||
, currentTime: Nullable<number>
|
||||
}
|
||||
| MediaReqBase & {
|
||||
type: "EDIT_TRACKS_INFO"
|
||||
, activeTrackIds: Nullable<number[]>
|
||||
, textTrackStyle: Nullable<string>
|
||||
}
|
||||
// QueueLoadRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_LOAD"
|
||||
, items: QueueItem[]
|
||||
, startIndex: number
|
||||
, repeatMode: string
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueInsertItemsRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_INSERT"
|
||||
, items: QueueItem[]
|
||||
, insertBefore: Nullable<number>
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueUpdateItemsRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_UPDATE"
|
||||
, items: QueueItem[]
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueJumpRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_UPDATE"
|
||||
, jump: Nullable<number>
|
||||
, currentItemId: Nullable<number>
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueRemoveItemsRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_REMOVE"
|
||||
, itemIds: number[]
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueReorderItemsRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_REORDER"
|
||||
, itemIds: number[]
|
||||
, insertBefore: Nullable<number>
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueSetPropertiesRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_UPDATE"
|
||||
, repeatMode: Nullable<string>
|
||||
, sessionId: Nullable<string>
|
||||
};
|
||||
| (MediaReqBase & { type: "PLAY" })
|
||||
| (MediaReqBase & { type: "PAUSE" })
|
||||
| (MediaReqBase & { type: "MEDIA_GET_STATUS" })
|
||||
| (MediaReqBase & { type: "STOP" })
|
||||
| (MediaReqBase & { type: "MEDIA_SET_VOLUME"; volume: Volume })
|
||||
| (MediaReqBase & { type: "SET_PLAYBACK_RATE"; playbackRate: number })
|
||||
| (ReqBase & {
|
||||
type: "LOAD";
|
||||
activeTrackIds: Nullable<number[]>;
|
||||
atvCredentials?: string;
|
||||
atvCredentialsType?: string;
|
||||
autoplay: Nullable<boolean>;
|
||||
currentTime: Nullable<number>;
|
||||
customData?: unknown;
|
||||
media: MediaInformation;
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
| (MediaReqBase & {
|
||||
type: "SEEK";
|
||||
resumeState: Nullable<ResumeState>;
|
||||
currentTime: Nullable<number>;
|
||||
})
|
||||
| (MediaReqBase & {
|
||||
type: "EDIT_TRACKS_INFO";
|
||||
activeTrackIds: Nullable<number[]>;
|
||||
textTrackStyle: Nullable<string>;
|
||||
})
|
||||
// QueueLoadRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_LOAD";
|
||||
items: QueueItem[];
|
||||
startIndex: number;
|
||||
repeatMode: string;
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueInsertItemsRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_INSERT";
|
||||
items: QueueItem[];
|
||||
insertBefore: Nullable<number>;
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueUpdateItemsRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_UPDATE";
|
||||
items: QueueItem[];
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueJumpRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_UPDATE";
|
||||
jump: Nullable<number>;
|
||||
currentItemId: Nullable<number>;
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueRemoveItemsRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_REMOVE";
|
||||
itemIds: number[];
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueReorderItemsRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_REORDER";
|
||||
itemIds: number[];
|
||||
insertBefore: Nullable<number>;
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueSetPropertiesRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_UPDATE";
|
||||
repeatMode: Nullable<string>;
|
||||
sessionId: Nullable<string>;
|
||||
});
|
||||
|
||||
export type ReceiverMediaMessage =
|
||||
MediaReqBase & { type: "MEDIA_STATUS", status: MediaStatus[] }
|
||||
| MediaReqBase & { type: "INVALID_PLAYER_STATE" }
|
||||
| MediaReqBase & { type: "LOAD_FAILED" }
|
||||
| MediaReqBase & { type: "LOAD_CANCELLED" }
|
||||
| MediaReqBase & { type: "INVALID_REQUEST" };
|
||||
| (MediaReqBase & { type: "MEDIA_STATUS"; status: MediaStatus[] })
|
||||
| (MediaReqBase & { type: "INVALID_PLAYER_STATE" })
|
||||
| (MediaReqBase & { type: "LOAD_FAILED" })
|
||||
| (MediaReqBase & { type: "LOAD_CANCELLED" })
|
||||
| (MediaReqBase & { type: "INVALID_REQUEST" });
|
||||
|
||||
@@ -9,25 +9,31 @@ import mdns from "mdns";
|
||||
import { sendMessage } from "../lib/nativeMessaging";
|
||||
|
||||
import { ReceiverStatus } from "./cast/types";
|
||||
import { NS_CONNECTION, NS_HEARTBEAT, NS_RECEIVER }
|
||||
from "./cast/Session";
|
||||
|
||||
import { NS_CONNECTION, NS_HEARTBEAT, NS_RECEIVER } from "./cast/Session";
|
||||
|
||||
interface CastTxtRecord {
|
||||
id: string; cd: string; rm: string;
|
||||
ve: string; md: string; ic: string;
|
||||
fn: string; ca: string; st: string;
|
||||
bs: string; nf: string; rs: string;
|
||||
id: string;
|
||||
cd: string;
|
||||
rm: string;
|
||||
ve: string;
|
||||
md: string;
|
||||
ic: string;
|
||||
fn: string;
|
||||
ca: string;
|
||||
st: string;
|
||||
bs: string;
|
||||
nf: string;
|
||||
rs: string;
|
||||
}
|
||||
|
||||
const browser = mdns.createBrowser(mdns.tcp("googlecast"), {
|
||||
resolverSequence: [
|
||||
mdns.rst.DNSServiceResolve()
|
||||
, "DNSServiceGetAddrInfo" in mdns.dns_sd
|
||||
mdns.rst.DNSServiceResolve(),
|
||||
"DNSServiceGetAddrInfo" in mdns.dns_sd
|
||||
? mdns.rst.DNSServiceGetAddrInfo()
|
||||
// Some issues on Linux with IPv6, so restrict to IPv4
|
||||
: mdns.rst.getaddrinfo({ families: [ 4 ]})
|
||||
, mdns.rst.makeAddressesUnique()
|
||||
: // Some issues on Linux with IPv6, so restrict to IPv4
|
||||
mdns.rst.getaddrinfo({ families: [4] }),
|
||||
mdns.rst.makeAddressesUnique()
|
||||
]
|
||||
});
|
||||
|
||||
@@ -39,13 +45,13 @@ function onBrowserServiceUp(service: mdns.Service) {
|
||||
|
||||
const txtRecord = service.txtRecord as CastTxtRecord;
|
||||
sendMessage({
|
||||
subject: "main:receiverDeviceUp"
|
||||
, data: {
|
||||
subject: "main:receiverDeviceUp",
|
||||
data: {
|
||||
receiverDevice: {
|
||||
host: service.addresses[0]
|
||||
, port: service.port
|
||||
, id: service.name
|
||||
, friendlyName: txtRecord.fn
|
||||
host: service.addresses[0],
|
||||
port: service.port,
|
||||
id: service.name,
|
||||
friendlyName: txtRecord.fn
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -59,15 +65,14 @@ function onBrowserServiceDown(service: mdns.Service) {
|
||||
|
||||
const txtRecord = service.txtRecord as CastTxtRecord;
|
||||
sendMessage({
|
||||
subject: "main:receiverDeviceDown"
|
||||
, data: { receiverDeviceId: service.name }
|
||||
subject: "main:receiverDeviceDown",
|
||||
data: { receiverDeviceId: service.name }
|
||||
});
|
||||
}
|
||||
|
||||
browser.on("serviceUp", onBrowserServiceUp);
|
||||
browser.on("serviceDown", onBrowserServiceDown);
|
||||
|
||||
|
||||
interface InitializeOptions {
|
||||
shouldWatchStatus?: boolean;
|
||||
}
|
||||
@@ -88,8 +93,7 @@ export function startDiscovery(options: InitializeOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listener = new StatusListener(
|
||||
service.addresses[0], service.port);
|
||||
const listener = new StatusListener(service.addresses[0], service.port);
|
||||
|
||||
listener.on("receiverStatus", (status: ReceiverStatus) => {
|
||||
if (!service.name) {
|
||||
@@ -97,10 +101,10 @@ export function startDiscovery(options: InitializeOptions) {
|
||||
}
|
||||
|
||||
sendMessage({
|
||||
subject: "main:receiverDeviceUpdated"
|
||||
, data: {
|
||||
receiverDeviceId: service.name
|
||||
, status
|
||||
subject: "main:receiverDeviceUpdated",
|
||||
data: {
|
||||
receiverDeviceId: service.name,
|
||||
status
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -122,12 +126,11 @@ export function stopDiscovery() {
|
||||
browser.stop();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a connection to a receiver device and forwards
|
||||
* RECEIVER_STATUS updates to the extension.
|
||||
*/
|
||||
export default class StatusListener extends EventEmitter {
|
||||
export default class StatusListener extends EventEmitter {
|
||||
private client: Client;
|
||||
private clientReceiver?: Channel;
|
||||
private clientHeartbeatIntervalId?: NodeJS.Timeout;
|
||||
@@ -160,17 +163,28 @@ export function stopDiscovery() {
|
||||
this.client.close();
|
||||
}
|
||||
|
||||
|
||||
private onConnect(): void {
|
||||
const sourceId = "sender-0";
|
||||
const destinationId = "receiver-0";
|
||||
|
||||
const clientConnection = this.client.createChannel(
|
||||
sourceId, destinationId, NS_CONNECTION, "JSON");
|
||||
sourceId,
|
||||
destinationId,
|
||||
NS_CONNECTION,
|
||||
"JSON"
|
||||
);
|
||||
const clientHeartbeat = this.client.createChannel(
|
||||
sourceId, destinationId, NS_HEARTBEAT, "JSON");
|
||||
sourceId,
|
||||
destinationId,
|
||||
NS_HEARTBEAT,
|
||||
"JSON"
|
||||
);
|
||||
const clientReceiver = this.client.createChannel(
|
||||
sourceId, destinationId, NS_RECEIVER, "JSON");
|
||||
sourceId,
|
||||
destinationId,
|
||||
NS_RECEIVER,
|
||||
"JSON"
|
||||
);
|
||||
|
||||
clientReceiver.on("message", data => {
|
||||
switch (data.type) {
|
||||
|
||||
@@ -11,7 +11,6 @@ import mime from "mime-types";
|
||||
import { sendMessage } from "../lib/nativeMessaging";
|
||||
import { convertSrtToVtt } from "../lib/subtitles";
|
||||
|
||||
|
||||
export let mediaServer: http.Server | undefined;
|
||||
|
||||
export async function startMediaServer(filePath: string, port: number) {
|
||||
@@ -41,7 +40,7 @@ export async function startMediaServer(filePath: string, port: number) {
|
||||
} catch (err) {
|
||||
console.error("Error: Failed to find media path.");
|
||||
sendMessage({
|
||||
subject: "mediaCast:mediaServerError"
|
||||
subject: "mediaCast:mediaServerError"
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -63,15 +62,19 @@ export async function startMediaServer(filePath: string, port: number) {
|
||||
*/
|
||||
const subtitles = new Map<string, string>();
|
||||
try {
|
||||
const dirEntries = await fs.promises.readdir(
|
||||
fileDir, { withFileTypes: true });
|
||||
const dirEntries = await fs.promises.readdir(fileDir, {
|
||||
withFileTypes: true
|
||||
});
|
||||
|
||||
for (const dirEntry of dirEntries) {
|
||||
if (dirEntry.isFile()
|
||||
&& mime.lookup(dirEntry.name) === "application/x-subrip") {
|
||||
|
||||
subtitles.set(dirEntry.name, await convertSrtToVtt(
|
||||
path.join(fileDir, dirEntry.name)));
|
||||
if (
|
||||
dirEntry.isFile() &&
|
||||
mime.lookup(dirEntry.name) === "application/x-subrip"
|
||||
) {
|
||||
subtitles.set(
|
||||
dirEntry.name,
|
||||
await convertSrtToVtt(path.join(fileDir, dirEntry.name))
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -96,22 +99,20 @@ export async function startMediaServer(filePath: string, port: number) {
|
||||
if (range) {
|
||||
const bounds = range.substring(6).split("-");
|
||||
const start = parseInt(bounds[0]);
|
||||
const end = bounds[1]
|
||||
? parseInt(bounds[1]) : fileSize - 1;
|
||||
const end = bounds[1] ? parseInt(bounds[1]) : fileSize - 1;
|
||||
|
||||
res.writeHead(206, {
|
||||
"Accept-Ranges": "bytes"
|
||||
, "Content-Range": `bytes ${start}-${end}/${fileSize}`
|
||||
, "Content-Length": (end - start) + 1
|
||||
, "Content-Type": contentType
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
|
||||
"Content-Length": end - start + 1,
|
||||
"Content-Type": contentType
|
||||
});
|
||||
|
||||
fs.createReadStream(
|
||||
filePath, { start, end }).pipe(res);
|
||||
fs.createReadStream(filePath, { start, end }).pipe(res);
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
"Content-Length": fileSize
|
||||
, "Content-Type": contentType
|
||||
"Content-Length": fileSize,
|
||||
"Content-Type": contentType
|
||||
});
|
||||
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
@@ -139,7 +140,8 @@ export async function startMediaServer(filePath: string, port: number) {
|
||||
const ifaces = Object.values(os.networkInterfaces());
|
||||
for (const iface of ifaces) {
|
||||
const matchingIface = iface?.find(
|
||||
details => details.family === "IPv4" && !details.internal);
|
||||
details => details.family === "IPv4" && !details.internal
|
||||
);
|
||||
if (matchingIface) {
|
||||
localAddress = matchingIface.address;
|
||||
}
|
||||
@@ -155,21 +157,25 @@ export async function startMediaServer(filePath: string, port: number) {
|
||||
}
|
||||
|
||||
sendMessage({
|
||||
subject: "mediaCast:mediaServerStarted"
|
||||
, data: {
|
||||
mediaPath: fileName
|
||||
, subtitlePaths: Array.from(subtitles.keys())
|
||||
, localAddress
|
||||
subject: "mediaCast:mediaServerStarted",
|
||||
data: {
|
||||
mediaPath: fileName,
|
||||
subtitlePaths: Array.from(subtitles.keys()),
|
||||
localAddress
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mediaServer.on("close", () => sendMessage({
|
||||
subject: "mediaCast:mediaServerStopped"
|
||||
}));
|
||||
mediaServer.on("error", () => sendMessage({
|
||||
subject: "mediaCast:mediaServerError"
|
||||
}));
|
||||
mediaServer.on("close", () =>
|
||||
sendMessage({
|
||||
subject: "mediaCast:mediaServerStopped"
|
||||
})
|
||||
);
|
||||
mediaServer.on("error", () =>
|
||||
sendMessage({
|
||||
subject: "mediaCast:mediaServerError"
|
||||
})
|
||||
);
|
||||
|
||||
mediaServer.listen(port);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,11 @@ import path from "path";
|
||||
|
||||
import { sendMessage } from "../lib/nativeMessaging";
|
||||
|
||||
|
||||
function fatal(message: string) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
let selectorApp: child_process.ChildProcess | undefined;
|
||||
let selectorAppOpen = false;
|
||||
|
||||
@@ -35,10 +33,12 @@ export function startReceiverSelector(data: string) {
|
||||
selectorAppOpen = false;
|
||||
}
|
||||
|
||||
const selectorPath = path.join(process.cwd()
|
||||
, "fx_cast_selector.app/Contents/MacOS/fx_cast_selector");
|
||||
const selectorPath = path.join(
|
||||
process.cwd(),
|
||||
"fx_cast_selector.app/Contents/MacOS/fx_cast_selector"
|
||||
);
|
||||
|
||||
selectorApp = child_process.spawn(selectorPath, [ data ]);
|
||||
selectorApp = child_process.spawn(selectorPath, [data]);
|
||||
selectorAppOpen = true;
|
||||
|
||||
if (selectorApp.stdout) {
|
||||
@@ -48,24 +48,24 @@ export function startReceiverSelector(data: string) {
|
||||
|
||||
if (!jsonData.mediaType) {
|
||||
sendMessage({
|
||||
subject: "main:receiverSelector/stopped"
|
||||
, data: jsonData
|
||||
subject: "main:receiverSelector/stopped",
|
||||
data: jsonData
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessage({
|
||||
subject: "main:receiverSelector/selected"
|
||||
, data: jsonData
|
||||
subject: "main:receiverSelector/selected",
|
||||
data: jsonData
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
selectorApp.on("error", err => {
|
||||
sendMessage({
|
||||
subject: "main:receiverSelector/error"
|
||||
, data: err.message
|
||||
subject: "main:receiverSelector/error",
|
||||
data: err.message
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,19 +6,19 @@ import { Message } from "./messaging";
|
||||
import { handleCastMessage } from "./components/cast";
|
||||
import { startDiscovery, stopDiscovery } from "./components/discovery";
|
||||
import { startMediaServer, stopMediaServer } from "./components/mediaServer";
|
||||
import { startReceiverSelector, stopReceiverSelector }
|
||||
from "./components/receiverSelector";
|
||||
import {
|
||||
startReceiverSelector,
|
||||
stopReceiverSelector
|
||||
} from "./components/receiverSelector";
|
||||
|
||||
import { __applicationName, __applicationVersion } from "../../package.json";
|
||||
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
stopDiscovery();
|
||||
stopMediaServer();
|
||||
stopReceiverSelector();
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Handle incoming messages from the extension and forward
|
||||
* them to the appropriate handlers.
|
||||
@@ -41,10 +41,12 @@ decodeTransform.on("data", (message: Message) => {
|
||||
|
||||
// Receiver selector
|
||||
case "bridge:openReceiverSelector": {
|
||||
startReceiverSelector(message.data); break;
|
||||
startReceiverSelector(message.data);
|
||||
break;
|
||||
}
|
||||
case "bridge:closeReceiverSelector": {
|
||||
stopReceiverSelector(); break;
|
||||
stopReceiverSelector();
|
||||
break;
|
||||
}
|
||||
|
||||
// Media server
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { DecodeTransform, EncodeTransform } from "../../transforms";
|
||||
import { Message } from "../messaging";
|
||||
|
||||
|
||||
export const decodeTransform = new DecodeTransform();
|
||||
export const encodeTransform = new EncodeTransform();
|
||||
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
import fs from "fs";
|
||||
|
||||
|
||||
/**
|
||||
* Reads a SubRip file and outputs text content as WebVTT.
|
||||
*/
|
||||
export async function convertSrtToVtt(srtFilePath: string) {
|
||||
const fileStream = fs.createReadStream(
|
||||
srtFilePath, { encoding: "utf-8" });
|
||||
const fileStream = fs.createReadStream(srtFilePath, { encoding: "utf-8" });
|
||||
|
||||
let fileContents = "";
|
||||
for await (let chunk of fileStream) {
|
||||
@@ -21,7 +19,6 @@ export async function convertSrtToVtt(srtFilePath: string) {
|
||||
fileContents += chunk.replace(/$\r\n/gm, "\n");
|
||||
}
|
||||
|
||||
|
||||
let vttText = "WEBVTT\n";
|
||||
|
||||
/**
|
||||
@@ -30,7 +27,8 @@ export async function convertSrtToVtt(srtFilePath: string) {
|
||||
* (followed by a new line), then any text content until a blank
|
||||
* line.
|
||||
*/
|
||||
const REGEX_CAPTION = /(?:(\d+)\n(\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}))\n((?:.+)\n?)*/g;
|
||||
const REGEX_CAPTION =
|
||||
/(?:(\d+)\n(\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}))\n((?:.+)\n?)*/g;
|
||||
|
||||
/**
|
||||
* WebVTT is very similar to SubRip, the main differences being
|
||||
|
||||
@@ -1,121 +1,110 @@
|
||||
"use strict";
|
||||
|
||||
import { Image
|
||||
, ReceiverStatus
|
||||
, SenderApplication
|
||||
, SenderMessage
|
||||
, Volume } from "./components/cast/types";
|
||||
|
||||
import { ReceiverDevice
|
||||
, ReceiverSelectionCast
|
||||
, ReceiverSelectionStop } from "./types";
|
||||
import {
|
||||
Image,
|
||||
ReceiverStatus,
|
||||
SenderApplication,
|
||||
SenderMessage,
|
||||
Volume
|
||||
} from "./components/cast/types";
|
||||
|
||||
import {
|
||||
ReceiverDevice,
|
||||
ReceiverSelectionCast,
|
||||
ReceiverSelectionStop
|
||||
} from "./types";
|
||||
|
||||
interface CastSessionUpdated {
|
||||
sessionId: string
|
||||
, statusText: string
|
||||
, namespaces: Array<{ name: string }>
|
||||
, volume: Volume
|
||||
sessionId: string;
|
||||
statusText: string;
|
||||
namespaces: Array<{ name: string }>;
|
||||
volume: Volume;
|
||||
}
|
||||
|
||||
interface CastSessionCreated extends CastSessionUpdated {
|
||||
appId: string
|
||||
, appImages: Image[]
|
||||
, displayName: string
|
||||
, receiverFriendlyName: string
|
||||
, senderApps: SenderApplication[]
|
||||
, transportId: string
|
||||
appId: string;
|
||||
appImages: Image[];
|
||||
displayName: string;
|
||||
receiverFriendlyName: string;
|
||||
senderApps: SenderApplication[];
|
||||
transportId: string;
|
||||
}
|
||||
|
||||
type MessageDefinitions = {
|
||||
"shim:castSessionCreated": CastSessionCreated
|
||||
, "shim:castSessionUpdated": CastSessionUpdated
|
||||
, "shim:castSessionStopped": {
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
, "shim:receivedCastSessionMessage": {
|
||||
sessionId: string
|
||||
, namespace: string
|
||||
, messageData: string
|
||||
}
|
||||
|
||||
, "shim:impl_sendCastMessage": {
|
||||
sessionId: string
|
||||
, messageId: string
|
||||
, error?: string
|
||||
}
|
||||
|
||||
, "bridge:createCastSession": {
|
||||
appId: string
|
||||
, receiverDevice: ReceiverDevice
|
||||
}
|
||||
, "bridge:sendCastReceiverMessage": {
|
||||
sessionId: string
|
||||
, messageData: SenderMessage
|
||||
, messageId: string
|
||||
}
|
||||
, "bridge:sendCastSessionMessage": {
|
||||
sessionId: string
|
||||
, namespace: string
|
||||
, messageData: object | string
|
||||
, messageId: string
|
||||
}
|
||||
|
||||
, "bridge:stopCastApp": { receiverDevice: ReceiverDevice }
|
||||
"shim:castSessionCreated": CastSessionCreated;
|
||||
"shim:castSessionUpdated": CastSessionUpdated;
|
||||
"shim:castSessionStopped": {
|
||||
sessionId: string;
|
||||
};
|
||||
"shim:receivedCastSessionMessage": {
|
||||
sessionId: string;
|
||||
namespace: string;
|
||||
messageData: string;
|
||||
};
|
||||
"shim:impl_sendCastMessage": {
|
||||
sessionId: string;
|
||||
messageId: string;
|
||||
error?: string;
|
||||
};
|
||||
"bridge:createCastSession": {
|
||||
appId: string;
|
||||
receiverDevice: ReceiverDevice;
|
||||
};
|
||||
"bridge:sendCastReceiverMessage": {
|
||||
sessionId: string;
|
||||
messageData: SenderMessage;
|
||||
messageId: string;
|
||||
};
|
||||
"bridge:sendCastSessionMessage": {
|
||||
sessionId: string;
|
||||
namespace: string;
|
||||
messageData: object | string;
|
||||
messageId: string;
|
||||
};
|
||||
"bridge:stopCastApp": { receiverDevice: ReceiverDevice };
|
||||
|
||||
// Bridge messages
|
||||
, "main:receiverSelector/selected": ReceiverSelectionCast
|
||||
, "main:receiverSelector/stopped": ReceiverSelectionStop
|
||||
, "main:receiverSelector/cancelled": {}
|
||||
, "main:receiverSelector/error": string
|
||||
"main:receiverSelector/selected": ReceiverSelectionCast;
|
||||
"main:receiverSelector/stopped": ReceiverSelectionStop;
|
||||
"main:receiverSelector/cancelled": {};
|
||||
"main:receiverSelector/error": string;
|
||||
|
||||
/**
|
||||
* getInfo uses the old :/ form for compat with old bridge
|
||||
* versions.
|
||||
*/
|
||||
, "bridge:getInfo": string
|
||||
, "bridge:/getInfo": string
|
||||
|
||||
, "bridge:startDiscovery": {
|
||||
shouldWatchStatus: boolean
|
||||
}
|
||||
|
||||
, "bridge:openReceiverSelector": string
|
||||
, "bridge:closeReceiverSelector": {}
|
||||
|
||||
, "bridge:startMediaServer": {
|
||||
filePath: string
|
||||
, port: number
|
||||
}
|
||||
, "bridge:stopMediaServer": {}
|
||||
|
||||
, "mediaCast:mediaServerStarted": {
|
||||
mediaPath: string
|
||||
, subtitlePaths: string[]
|
||||
, localAddress: string
|
||||
}
|
||||
, "mediaCast:mediaServerStopped": {}
|
||||
, "mediaCast:mediaServerError": {}
|
||||
|
||||
|
||||
, "main:serviceUp": ReceiverDevice
|
||||
, "main:serviceDown": { id: string }
|
||||
|
||||
, "main:updateReceiverStatus": {
|
||||
id: string
|
||||
, status: ReceiverStatus
|
||||
}
|
||||
|
||||
|
||||
, "main:receiverDeviceUp": { receiverDevice: ReceiverDevice }
|
||||
, "main:receiverDeviceDown": { receiverDeviceId: string }
|
||||
, "main:receiverDeviceUpdated": {
|
||||
receiverDeviceId: string
|
||||
, status: ReceiverStatus
|
||||
}
|
||||
}
|
||||
|
||||
"bridge:getInfo": string;
|
||||
"bridge:/getInfo": string;
|
||||
"bridge:startDiscovery": {
|
||||
shouldWatchStatus: boolean;
|
||||
};
|
||||
"bridge:openReceiverSelector": string;
|
||||
"bridge:closeReceiverSelector": {};
|
||||
"bridge:startMediaServer": {
|
||||
filePath: string;
|
||||
port: number;
|
||||
};
|
||||
"bridge:stopMediaServer": {};
|
||||
"mediaCast:mediaServerStarted": {
|
||||
mediaPath: string;
|
||||
subtitlePaths: string[];
|
||||
localAddress: string;
|
||||
};
|
||||
"mediaCast:mediaServerStopped": {};
|
||||
"mediaCast:mediaServerError": {};
|
||||
"main:serviceUp": ReceiverDevice;
|
||||
"main:serviceDown": { id: string };
|
||||
"main:updateReceiverStatus": {
|
||||
id: string;
|
||||
status: ReceiverStatus;
|
||||
};
|
||||
"main:receiverDeviceUp": { receiverDevice: ReceiverDevice };
|
||||
"main:receiverDeviceDown": { receiverDeviceId: string };
|
||||
"main:receiverDeviceUpdated": {
|
||||
receiverDeviceId: string;
|
||||
status: ReceiverStatus;
|
||||
};
|
||||
};
|
||||
|
||||
interface MessageBase<K extends keyof MessageDefinitions> {
|
||||
subject: K;
|
||||
@@ -124,7 +113,7 @@ interface MessageBase<K extends keyof MessageDefinitions> {
|
||||
|
||||
type Messages = {
|
||||
[K in keyof MessageDefinitions]: MessageBase<K>;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* For better call semantics, make message data key optional if
|
||||
@@ -137,5 +126,4 @@ type NarrowedMessage<L extends MessageBase<keyof MessageDefinitions>> =
|
||||
: L
|
||||
: never;
|
||||
|
||||
|
||||
export type Message = NarrowedMessage<Messages[keyof Messages]>;
|
||||
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
import { ReceiverStatus } from "./components/cast/types";
|
||||
|
||||
|
||||
export enum ReceiverSelectorMediaType {
|
||||
App = 1
|
||||
, Tab = 2
|
||||
, Screen = 4
|
||||
, File = 8
|
||||
App = 1,
|
||||
Tab = 2,
|
||||
Screen = 4,
|
||||
File = 8
|
||||
}
|
||||
|
||||
export enum ReceiverSelectionActionType {
|
||||
Cast = 1
|
||||
, Stop = 2
|
||||
Cast = 1,
|
||||
Stop = 2
|
||||
}
|
||||
|
||||
export interface ReceiverSelectionCast {
|
||||
|
||||
@@ -6,9 +6,7 @@ import { Readable } from "stream";
|
||||
import minimist from "minimist";
|
||||
import WebSocket from "ws";
|
||||
|
||||
import { DecodeTransform
|
||||
, EncodeTransform } from "./transforms";
|
||||
|
||||
import { DecodeTransform, EncodeTransform } from "./transforms";
|
||||
|
||||
export function init(port: number) {
|
||||
process.stdout.write("Starting WebSocket server... ");
|
||||
@@ -18,13 +16,12 @@ export function init(port: number) {
|
||||
console.log("Done!");
|
||||
});
|
||||
|
||||
wss.on("error", (err) => {
|
||||
wss.on("error", err => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Failed!");
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
|
||||
wss.on("connection", socket => {
|
||||
// Stream for incoming WebSocket messages
|
||||
const messageStream = new Readable({ objectMode: true });
|
||||
@@ -35,27 +32,22 @@ export function init(port: number) {
|
||||
messageStream.push(JSON.parse(message));
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Daemon and bridge are the same binary, so spawn a new
|
||||
* version of self in bridge mode.
|
||||
*/
|
||||
const bridge = spawn(process.execPath, [ process.argv[1] ]);
|
||||
const bridge = spawn(process.execPath, [process.argv[1]]);
|
||||
|
||||
// socket -> bridge.stdin
|
||||
messageStream
|
||||
.pipe(new EncodeTransform())
|
||||
.pipe(bridge.stdin);
|
||||
messageStream.pipe(new EncodeTransform()).pipe(bridge.stdin);
|
||||
|
||||
// bridge.stdout -> socket
|
||||
bridge.stdout
|
||||
.pipe(new DecodeTransform())
|
||||
.on("data", data => {
|
||||
// Socket can be CLOSING here
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
bridge.stdout.pipe(new DecodeTransform()).on("data", data => {
|
||||
// Socket can be CLOSING here
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle termination
|
||||
socket.on("close", () => bridge.kill());
|
||||
|
||||
7
app/src/global.d.ts
vendored
7
app/src/global.d.ts
vendored
@@ -1,6 +1,5 @@
|
||||
declare type Nullable<T> = T | null;
|
||||
|
||||
declare type DistributiveOmit<T, K extends keyof any> =
|
||||
T extends any
|
||||
? Omit<T, K>
|
||||
: never;
|
||||
declare type DistributiveOmit<T, K extends keyof any> = T extends any
|
||||
? Omit<T, K>
|
||||
: never;
|
||||
|
||||
@@ -5,29 +5,28 @@ import minimist from "minimist";
|
||||
import { __applicationVersion } from "../package.json";
|
||||
|
||||
const argv = minimist(process.argv.slice(2), {
|
||||
boolean: [ "daemon", "help", "version" ]
|
||||
, string: [ "__name", "port" ]
|
||||
, alias: {
|
||||
d: "daemon"
|
||||
, h: "help"
|
||||
, v: "version"
|
||||
, p: "port"
|
||||
}
|
||||
, default: {
|
||||
__name: path.basename(process.argv[0])
|
||||
, daemon: false
|
||||
, port: "9556"
|
||||
boolean: ["daemon", "help", "version"],
|
||||
string: ["__name", "port"],
|
||||
alias: {
|
||||
d: "daemon",
|
||||
h: "help",
|
||||
v: "version",
|
||||
p: "port"
|
||||
},
|
||||
default: {
|
||||
__name: path.basename(process.argv[0]),
|
||||
daemon: false,
|
||||
port: "9556"
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (argv.version) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`v${__applicationVersion}`);
|
||||
} else if (argv.help) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`Usage: ${argv.__name} [options]
|
||||
`Usage: ${argv.__name} [options]
|
||||
|
||||
Options:
|
||||
-h, --help Print usage info
|
||||
@@ -37,8 +36,8 @@ Options:
|
||||
options.
|
||||
-p, --port Set port number for WebSocket server. This must match the
|
||||
port set in the extension options.
|
||||
`);
|
||||
|
||||
`
|
||||
);
|
||||
} else if (argv.daemon) {
|
||||
const port = parseInt(argv.port);
|
||||
if (!port || port < 1025 || port > 65535) {
|
||||
@@ -46,10 +45,9 @@ Options:
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import("./daemon")
|
||||
.then(daemon => {
|
||||
daemon.init(port);
|
||||
});
|
||||
import("./daemon").then(daemon => {
|
||||
daemon.init(port);
|
||||
});
|
||||
} else {
|
||||
import("./bridge");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { Transform } from "stream";
|
||||
import { Message } from "./bridge/messaging";
|
||||
|
||||
|
||||
type ResponseHandlerFunction = (message: Message) => Promise<any>;
|
||||
|
||||
/**
|
||||
@@ -13,29 +12,27 @@ type ResponseHandlerFunction = (message: Message) => Promise<any>;
|
||||
export class ResponseTransform extends Transform {
|
||||
constructor(private _handler: ResponseHandlerFunction) {
|
||||
super({
|
||||
readableObjectMode: true
|
||||
, writableObjectMode: true
|
||||
readableObjectMode: true,
|
||||
writableObjectMode: true
|
||||
});
|
||||
}
|
||||
|
||||
public _transform(
|
||||
chunk: Message
|
||||
, _encoding: string
|
||||
// tslint:disable-next-line:ban-types
|
||||
, callback: Function) {
|
||||
|
||||
Promise.resolve(this._handler(chunk))
|
||||
.then(res => {
|
||||
if (res) {
|
||||
callback(null, res);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
chunk: Message,
|
||||
_encoding: string,
|
||||
// tslint:disable-next-line:ban-types
|
||||
callback: Function
|
||||
) {
|
||||
Promise.resolve(this._handler(chunk)).then(res => {
|
||||
if (res) {
|
||||
callback(null, res);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Takes input, decodes the message string, parses as JSON
|
||||
* and outputs the parsed result.
|
||||
@@ -52,53 +49,52 @@ export class DecodeTransform extends Transform {
|
||||
}
|
||||
|
||||
public _transform(
|
||||
chunk: any
|
||||
, _encoding: string
|
||||
// tslint:disable-next-line:ban-types
|
||||
, callback: Function) {
|
||||
chunk: any,
|
||||
_encoding: string,
|
||||
// tslint:disable-next-line:ban-types
|
||||
callback: Function
|
||||
) {
|
||||
// Append next chunk to buffer
|
||||
this._messageBuffer = Buffer.concat([this._messageBuffer, chunk]);
|
||||
|
||||
// Append next chunk to buffer
|
||||
this._messageBuffer = Buffer.concat([
|
||||
this._messageBuffer
|
||||
, chunk
|
||||
]);
|
||||
for (;;) {
|
||||
if (this._messageLength === undefined) {
|
||||
if (this._messageBuffer.length >= 4) {
|
||||
// Read message length and offset buffer
|
||||
this._messageLength = this._messageBuffer.readUInt32LE(0);
|
||||
this._messageBuffer = this._messageBuffer.slice(4);
|
||||
|
||||
for (;;) {
|
||||
if (this._messageLength === undefined) {
|
||||
if (this._messageBuffer.length >= 4) {
|
||||
// Read message length and offset buffer
|
||||
this._messageLength = this._messageBuffer.readUInt32LE(0);
|
||||
this._messageBuffer = this._messageBuffer.slice(4);
|
||||
// Next message chunk
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (this._messageBuffer.length >= this._messageLength) {
|
||||
const message = JSON.parse(
|
||||
this._messageBuffer
|
||||
.slice(0, this._messageLength)
|
||||
.toString()
|
||||
);
|
||||
|
||||
// Next message chunk
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (this._messageBuffer.length >= this._messageLength) {
|
||||
const message = JSON.parse(this._messageBuffer
|
||||
.slice(0, this._messageLength)
|
||||
.toString());
|
||||
// Push message content
|
||||
this.push(message);
|
||||
|
||||
// Push message content
|
||||
this.push(message);
|
||||
// Offset buffer to start of next message
|
||||
this._messageBuffer = this._messageBuffer.slice(
|
||||
this._messageLength
|
||||
);
|
||||
this._messageLength = undefined;
|
||||
|
||||
// Offset buffer to start of next message
|
||||
this._messageBuffer = this._messageBuffer.slice(
|
||||
this._messageLength);
|
||||
this._messageLength = undefined;
|
||||
// Next message
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Next message
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// No complete messages left
|
||||
callback();
|
||||
break;
|
||||
}
|
||||
// No complete messages left
|
||||
callback();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes input, encodes the message length and content and
|
||||
@@ -112,11 +108,11 @@ export class EncodeTransform extends Transform {
|
||||
}
|
||||
|
||||
public _transform(
|
||||
chunk: any
|
||||
, _encoding: string
|
||||
// tslint:disable-next-line:ban-types
|
||||
, callback: Function) {
|
||||
|
||||
chunk: any,
|
||||
_encoding: string,
|
||||
// tslint:disable-next-line:ban-types
|
||||
callback: Function
|
||||
) {
|
||||
const messageLength = Buffer.alloc(4);
|
||||
const message = Buffer.from(JSON.stringify(chunk));
|
||||
|
||||
@@ -124,9 +120,6 @@ export class EncodeTransform extends Transform {
|
||||
messageLength.writeUInt32LE(message.length, 0);
|
||||
|
||||
// Output joined length and content
|
||||
callback(null, Buffer.concat([
|
||||
messageLength
|
||||
, message
|
||||
]));
|
||||
callback(null, Buffer.concat([messageLength, message]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,15 @@ import logger from "../lib/logger";
|
||||
import messaging, { Message, Port } from "../messaging";
|
||||
import options from "../lib/options";
|
||||
|
||||
import { ReceiverSelectionActionType
|
||||
, ReceiverSelectorMediaType } from "./receiverSelector";
|
||||
import {
|
||||
ReceiverSelectionActionType,
|
||||
ReceiverSelectorMediaType
|
||||
} from "./receiverSelector";
|
||||
|
||||
import ReceiverSelectorManager
|
||||
from "./receiverSelector/ReceiverSelectorManager";
|
||||
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
|
||||
|
||||
import receiverDevices from "./receiverDevices";
|
||||
|
||||
|
||||
type AnyPort = Port | MessagePort;
|
||||
|
||||
export interface Shim {
|
||||
@@ -25,8 +25,7 @@ export interface Shim {
|
||||
appId?: string;
|
||||
}
|
||||
|
||||
|
||||
export default new class ShimManager {
|
||||
export default new (class ShimManager {
|
||||
private activeShims = new Set<Shim>();
|
||||
|
||||
public async init() {
|
||||
@@ -40,8 +39,8 @@ export default new class ShimManager {
|
||||
receiverDevices.addEventListener("receiverDeviceUp", ev => {
|
||||
for (const shim of this.activeShims) {
|
||||
shim.contentPort.postMessage({
|
||||
subject: "shim:serviceUp"
|
||||
, data: { receiverDevice: ev.detail.receiverDevice }
|
||||
subject: "shim:serviceUp",
|
||||
data: { receiverDevice: ev.detail.receiverDevice }
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -49,8 +48,8 @@ export default new class ShimManager {
|
||||
receiverDevices.addEventListener("receiverDeviceDown", ev => {
|
||||
for (const shim of this.activeShims) {
|
||||
shim.contentPort.postMessage({
|
||||
subject: "shim:serviceDown"
|
||||
, data: { receiverDeviceId: ev.detail.receiverDeviceId }
|
||||
subject: "shim:serviceDown",
|
||||
data: { receiverDeviceId: ev.detail.receiverDeviceId }
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -74,19 +73,19 @@ export default new class ShimManager {
|
||||
: this.createShimFromContent(port));
|
||||
|
||||
shim.contentPort.postMessage({
|
||||
subject: "shim:initialized"
|
||||
, data: await bridge.getInfo()
|
||||
subject: "shim:initialized",
|
||||
data: await bridge.getInfo()
|
||||
});
|
||||
|
||||
this.activeShims.add(shim);
|
||||
}
|
||||
|
||||
private async createShimFromBackground(
|
||||
contentPort: MessagePort): Promise<Shim> {
|
||||
|
||||
contentPort: MessagePort
|
||||
): Promise<Shim> {
|
||||
const shim: Shim = {
|
||||
bridgePort: await bridge.connect()
|
||||
, contentPort
|
||||
bridgePort: await bridge.connect(),
|
||||
contentPort
|
||||
};
|
||||
|
||||
shim.bridgePort.onDisconnect.addListener(() => {
|
||||
@@ -105,12 +104,14 @@ export default new class ShimManager {
|
||||
return shim;
|
||||
}
|
||||
|
||||
private async createShimFromContent(
|
||||
contentPort: Port): Promise<Shim> {
|
||||
|
||||
if (contentPort.sender?.tab?.id === undefined
|
||||
|| contentPort.sender?.frameId === undefined) {
|
||||
throw logger.error("Content shim created with an invalid port context.");
|
||||
private async createShimFromContent(contentPort: Port): Promise<Shim> {
|
||||
if (
|
||||
contentPort.sender?.tab?.id === undefined ||
|
||||
contentPort.sender?.frameId === undefined
|
||||
) {
|
||||
throw logger.error(
|
||||
"Content shim created with an invalid port context."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,20 +119,21 @@ export default new class ShimManager {
|
||||
* tab/frame ID, disconnect it.
|
||||
*/
|
||||
for (const activeShim of this.activeShims) {
|
||||
if (activeShim.contentTabId === contentPort.sender.tab.id
|
||||
&& activeShim.contentFrameId === contentPort.sender.frameId) {
|
||||
if (
|
||||
activeShim.contentTabId === contentPort.sender.tab.id &&
|
||||
activeShim.contentFrameId === contentPort.sender.frameId
|
||||
) {
|
||||
activeShim.bridgePort.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
const shim: Shim = {
|
||||
bridgePort: await bridge.connect()
|
||||
, contentPort
|
||||
, contentTabId: contentPort.sender.tab.id
|
||||
, contentFrameId: contentPort.sender.frameId
|
||||
bridgePort: await bridge.connect(),
|
||||
contentPort,
|
||||
contentTabId: contentPort.sender.tab.id,
|
||||
contentFrameId: contentPort.sender.frameId
|
||||
};
|
||||
|
||||
|
||||
const onContentPortMessage = (message: Message) => {
|
||||
this.handleContentMessage(shim, message);
|
||||
};
|
||||
@@ -150,7 +152,6 @@ export default new class ShimManager {
|
||||
this.activeShims.delete(shim);
|
||||
};
|
||||
|
||||
|
||||
shim.bridgePort.onDisconnect.addListener(onDisconnect);
|
||||
shim.bridgePort.onMessage.addListener(onBridgePortMessage);
|
||||
|
||||
@@ -161,7 +162,7 @@ export default new class ShimManager {
|
||||
}
|
||||
|
||||
private async handleContentMessage(shim: Shim, message: Message) {
|
||||
const [ destination ] = message.subject.split(":");
|
||||
const [destination] = message.subject.split(":");
|
||||
if (destination === "bridge") {
|
||||
shim.bridgePort.postMessage(message);
|
||||
}
|
||||
@@ -172,8 +173,8 @@ export default new class ShimManager {
|
||||
|
||||
for (const receiverDevice of receiverDevices.getDevices()) {
|
||||
shim.contentPort.postMessage({
|
||||
subject: "shim:serviceUp"
|
||||
, data: { receiverDevice }
|
||||
subject: "shim:serviceUp",
|
||||
data: { receiverDevice }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -181,15 +182,21 @@ export default new class ShimManager {
|
||||
}
|
||||
|
||||
case "main:selectReceiver": {
|
||||
if (shim.contentTabId === undefined
|
||||
|| shim.contentFrameId === undefined) {
|
||||
throw logger.error("Shim associated with content sender missing tab/frame ID");
|
||||
if (
|
||||
shim.contentTabId === undefined ||
|
||||
shim.contentFrameId === undefined
|
||||
) {
|
||||
throw logger.error(
|
||||
"Shim associated with content sender missing tab/frame ID"
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const selection =
|
||||
await ReceiverSelectorManager.getSelection(
|
||||
shim.contentTabId, shim.contentFrameId);
|
||||
await ReceiverSelectorManager.getSelection(
|
||||
shim.contentTabId,
|
||||
shim.contentFrameId
|
||||
);
|
||||
|
||||
// Handle cancellation
|
||||
if (!selection) {
|
||||
@@ -207,25 +214,26 @@ export default new class ShimManager {
|
||||
* been changed, we need to cancel the current
|
||||
* sender and switch it out for the right one.
|
||||
*/
|
||||
if (selection.mediaType !==
|
||||
ReceiverSelectorMediaType.App) {
|
||||
|
||||
if (
|
||||
selection.mediaType !==
|
||||
ReceiverSelectorMediaType.App
|
||||
) {
|
||||
shim.contentPort.postMessage({
|
||||
subject: "shim:selectReceiver/cancelled"
|
||||
});
|
||||
|
||||
loadSender({
|
||||
tabId: shim.contentTabId
|
||||
, frameId: shim.contentFrameId
|
||||
, selection
|
||||
tabId: shim.contentTabId,
|
||||
frameId: shim.contentFrameId,
|
||||
selection
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
shim.contentPort.postMessage({
|
||||
subject: "shim:selectReceiver/selected"
|
||||
, data: selection
|
||||
subject: "shim:selectReceiver/selected",
|
||||
data: selection
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -233,8 +241,8 @@ export default new class ShimManager {
|
||||
|
||||
case ReceiverSelectionActionType.Stop: {
|
||||
shim.contentPort.postMessage({
|
||||
subject: "shim:selectReceiver/stopped"
|
||||
, data: selection
|
||||
subject: "shim:selectReceiver/stopped",
|
||||
data: selection
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -257,7 +265,8 @@ export default new class ShimManager {
|
||||
case "main:sessionCreated": {
|
||||
const selector = await ReceiverSelectorManager.getSelector();
|
||||
const shouldClose = await options.get(
|
||||
"receiverSelectorWaitForConnection");
|
||||
"receiverSelectorWaitForConnection"
|
||||
);
|
||||
|
||||
if (selector.isOpen && shouldClose) {
|
||||
selector.close();
|
||||
@@ -267,4 +276,4 @@ export default new class ShimManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -7,8 +7,7 @@ import messaging from "../messaging";
|
||||
import options from "../lib/options";
|
||||
import bridge, { BridgeInfo } from "../lib/bridge";
|
||||
|
||||
import ReceiverSelectorManager
|
||||
from "./receiverSelector/ReceiverSelectorManager";
|
||||
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
|
||||
|
||||
import ShimManager from "./ShimManager";
|
||||
|
||||
@@ -17,10 +16,8 @@ import receiverDevices from "./receiverDevices";
|
||||
import { initMenus } from "./menus";
|
||||
import { initWhitelist } from "./whitelist";
|
||||
|
||||
|
||||
const _ = browser.i18n.getMessage;
|
||||
|
||||
|
||||
/**
|
||||
* On install, set the default options before initializing the
|
||||
* extension. On update, handle any unset values and set to
|
||||
@@ -45,7 +42,6 @@ browser.runtime.onInstalled.addListener(async details => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Sets up media overlay content script and handles toggling
|
||||
* on options change.
|
||||
@@ -62,10 +58,10 @@ async function initMediaOverlay() {
|
||||
|
||||
try {
|
||||
contentScript = await browser.contentScripts.register({
|
||||
allFrames: true
|
||||
, js: [{ file: "senders/media/overlay/overlayContentLoader.js" }]
|
||||
, matches: [ "<all_urls>" ]
|
||||
, runAt: "document_start"
|
||||
allFrames: true,
|
||||
js: [{ file: "senders/media/overlay/overlayContentLoader.js" }],
|
||||
matches: ["<all_urls>"],
|
||||
runAt: "document_start"
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("Failed to register media overlay");
|
||||
@@ -76,7 +72,6 @@ async function initMediaOverlay() {
|
||||
await contentScript?.unregister();
|
||||
}
|
||||
|
||||
|
||||
registerMediaOverlayContentScript();
|
||||
|
||||
// Update if toggled
|
||||
@@ -90,7 +85,6 @@ async function initMediaOverlay() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the bridge can be reached and is compatible
|
||||
* with the current version of the extension. If not, triggers
|
||||
@@ -113,10 +107,11 @@ async function notifyBridgeCompat() {
|
||||
logger.info("... bridge incompatible!");
|
||||
|
||||
const updateNotificationId = await browser.notifications.create({
|
||||
type: "basic"
|
||||
, title: `${
|
||||
_("extensionName")} — ${_("optionsBridgeIssueStatusTitle")}`
|
||||
, message: info.isVersionOlder
|
||||
type: "basic",
|
||||
title: `${_("extensionName")} — ${_(
|
||||
"optionsBridgeIssueStatusTitle"
|
||||
)}`,
|
||||
message: info.isVersionOlder
|
||||
? _("optionsBridgeOlderAction")
|
||||
: _("optionsBridgeNewerAction")
|
||||
});
|
||||
@@ -127,14 +122,12 @@ async function notifyBridgeCompat() {
|
||||
}
|
||||
|
||||
browser.tabs.create({
|
||||
url: `https://github.com/hensm/fx_cast/releases/tag/v${
|
||||
info.expectedVersion}`
|
||||
url: `https://github.com/hensm/fx_cast/releases/tag/v${info.expectedVersion}`
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
async function init() {
|
||||
@@ -163,14 +156,13 @@ async function init() {
|
||||
await initWhitelist();
|
||||
await initMediaOverlay();
|
||||
|
||||
|
||||
/**
|
||||
* When the browser action is clicked, open a receiver
|
||||
* selector and load a sender for the response. The
|
||||
* mirroring sender is loaded into the current tab at the
|
||||
* top-level frame.
|
||||
*/
|
||||
browser.browserAction.onClicked.addListener(async tab => {
|
||||
browser.browserAction.onClicked.addListener(async tab => {
|
||||
if (tab.id === undefined) {
|
||||
throw logger.error("Tab ID not found in browser action handler.");
|
||||
}
|
||||
@@ -178,9 +170,9 @@ async function init() {
|
||||
const selection = await ReceiverSelectorManager.getSelection(tab.id);
|
||||
if (selection) {
|
||||
loadSender({
|
||||
tabId: tab.id
|
||||
, frameId: 0
|
||||
, selection
|
||||
tabId: tab.id,
|
||||
frameId: 0,
|
||||
selection
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,22 +6,21 @@ import options from "../lib/options";
|
||||
|
||||
import { stringify } from "../lib/utils";
|
||||
|
||||
import { ReceiverSelectionActionType
|
||||
, ReceiverSelectorMediaType } from "./receiverSelector";
|
||||
import {
|
||||
ReceiverSelectionActionType,
|
||||
ReceiverSelectorMediaType
|
||||
} from "./receiverSelector";
|
||||
|
||||
import ReceiverSelectorManager
|
||||
from "./receiverSelector/ReceiverSelectorManager";
|
||||
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
|
||||
|
||||
const _ = browser.i18n.getMessage;
|
||||
|
||||
|
||||
const URL_PATTERN_HTTP = "http://*/*";
|
||||
const URL_PATTERN_HTTPS = "https://*/*";
|
||||
const URL_PATTERN_FILE = "file://*/*";
|
||||
|
||||
const URL_PATTERNS_REMOTE = [ URL_PATTERN_HTTP, URL_PATTERN_HTTPS ];
|
||||
const URL_PATTERNS_ALL = [ ...URL_PATTERNS_REMOTE, URL_PATTERN_FILE ];
|
||||
|
||||
const URL_PATTERNS_REMOTE = [URL_PATTERN_HTTP, URL_PATTERN_HTTPS];
|
||||
const URL_PATTERNS_ALL = [...URL_PATTERNS_REMOTE, URL_PATTERN_FILE];
|
||||
|
||||
type MenuId = string | number;
|
||||
|
||||
@@ -39,44 +38,44 @@ export async function initMenus() {
|
||||
|
||||
// Global "Cast..." menu item
|
||||
menuIdCast = browser.menus.create({
|
||||
contexts: [ "browser_action", "page", "tools_menu" ]
|
||||
, title: _("contextCast")
|
||||
contexts: ["browser_action", "page", "tools_menu"],
|
||||
title: _("contextCast")
|
||||
});
|
||||
|
||||
// <video>/<audio> "Cast..." context menu item
|
||||
menuIdMediaCast = browser.menus.create({
|
||||
contexts: [ "audio", "video", "image" ]
|
||||
, title: _("contextCast")
|
||||
, visible: opts.mediaEnabled
|
||||
, targetUrlPatterns: opts.localMediaEnabled
|
||||
contexts: ["audio", "video", "image"],
|
||||
title: _("contextCast"),
|
||||
visible: opts.mediaEnabled,
|
||||
targetUrlPatterns: opts.localMediaEnabled
|
||||
? URL_PATTERNS_ALL
|
||||
: URL_PATTERNS_REMOTE
|
||||
});
|
||||
|
||||
|
||||
menuIdWhitelist = browser.menus.create({
|
||||
contexts: [ "browser_action" ]
|
||||
, title: _("contextAddToWhitelist")
|
||||
, enabled: false
|
||||
contexts: ["browser_action"],
|
||||
title: _("contextAddToWhitelist"),
|
||||
enabled: false
|
||||
});
|
||||
|
||||
menuIdWhitelistRecommended = browser.menus.create({
|
||||
title: _("contextAddToWhitelistRecommended")
|
||||
, parentId: menuIdWhitelist
|
||||
title: _("contextAddToWhitelistRecommended"),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
browser.menus.create({
|
||||
type: "separator"
|
||||
, parentId: menuIdWhitelist
|
||||
type: "separator",
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
if (info.parentMenuItemId === menuIdWhitelist) {
|
||||
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
|
||||
if (!pattern) {
|
||||
throw logger.error(`Whitelist pattern not found for menu item ID ${info.menuItemId}.`);
|
||||
throw logger.error(
|
||||
`Whitelist pattern not found for menu item ID ${info.menuItemId}.`
|
||||
);
|
||||
}
|
||||
|
||||
const whitelist = await options.get("userAgentWhitelist");
|
||||
@@ -99,7 +98,9 @@ browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
switch (info.menuItemId) {
|
||||
case menuIdCast: {
|
||||
const selection = await ReceiverSelectorManager.getSelection(
|
||||
tab.id, info.frameId);
|
||||
tab.id,
|
||||
info.frameId
|
||||
);
|
||||
|
||||
// Selection cancelled
|
||||
if (!selection) {
|
||||
@@ -107,9 +108,9 @@ browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
}
|
||||
|
||||
loadSender({
|
||||
tabId: tab.id
|
||||
, frameId: info.frameId
|
||||
, selection
|
||||
tabId: tab.id,
|
||||
frameId: info.frameId,
|
||||
selection
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -117,7 +118,10 @@ browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
|
||||
case menuIdMediaCast: {
|
||||
const selection = await ReceiverSelectorManager.getSelection(
|
||||
tab.id, info.frameId, true);
|
||||
tab.id,
|
||||
info.frameId,
|
||||
true
|
||||
);
|
||||
|
||||
// Selection cancelled
|
||||
if (!selection) {
|
||||
@@ -130,30 +134,26 @@ browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
* If the selected media type is App, that refers to the
|
||||
* media sender in this context, so load media sender.
|
||||
*/
|
||||
if (selection.mediaType ===
|
||||
ReceiverSelectorMediaType.App) {
|
||||
|
||||
if (selection.mediaType === ReceiverSelectorMediaType.App) {
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
code: stringify`
|
||||
window.receiver = ${selection.receiver};
|
||||
window.mediaUrl = ${info.srcUrl};
|
||||
window.targetElementId = ${
|
||||
info.targetElementId};
|
||||
`
|
||||
, frameId: info.frameId
|
||||
window.targetElementId = ${info.targetElementId};
|
||||
`,
|
||||
frameId: info.frameId
|
||||
});
|
||||
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
file: "senders/media/index.js"
|
||||
, frameId: info.frameId
|
||||
file: "senders/media/index.js",
|
||||
frameId: info.frameId
|
||||
});
|
||||
} else {
|
||||
|
||||
// Handle other responses
|
||||
loadSender({
|
||||
tabId: tab.id
|
||||
, frameId: info.frameId
|
||||
, selection
|
||||
tabId: tab.id,
|
||||
frameId: info.frameId,
|
||||
selection
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ browser.menus.onShown.addListener(async info => {
|
||||
enabled: true
|
||||
});
|
||||
|
||||
for (const [ menuId ] of whitelistChildMenuPatterns) {
|
||||
for (const [menuId] of whitelistChildMenuPatterns) {
|
||||
// Clear all page-specific temporary menus
|
||||
if (menuId !== menuIdWhitelistRecommended) {
|
||||
browser.menus.remove(menuId);
|
||||
@@ -235,9 +235,10 @@ browser.menus.onShown.addListener(async info => {
|
||||
}
|
||||
|
||||
// If there is more than one subdomain, get the base domain
|
||||
const baseDomain = (url.hostname.match(/\./g) || []).length > 1
|
||||
? url.hostname.substring(url.hostname.indexOf(".") + 1)
|
||||
: url.hostname;
|
||||
const baseDomain =
|
||||
(url.hostname.match(/\./g) || []).length > 1
|
||||
? url.hostname.substring(url.hostname.indexOf(".") + 1)
|
||||
: url.hostname;
|
||||
|
||||
const portlessOrigin = `${url.protocol}//${url.hostname}`;
|
||||
|
||||
@@ -253,17 +254,17 @@ browser.menus.onShown.addListener(async info => {
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
menuIdWhitelistRecommended, patternRecommended);
|
||||
|
||||
menuIdWhitelistRecommended,
|
||||
patternRecommended
|
||||
);
|
||||
|
||||
if (url.search) {
|
||||
const whitelistSearchMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd", patternSearch)
|
||||
, parentId: menuIdWhitelist
|
||||
title: _("contextAddToWhitelistAdvancedAdd", patternSearch),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
whitelistSearchMenuId, patternSearch);
|
||||
whitelistChildMenuPatterns.set(whitelistSearchMenuId, patternSearch);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,65 +276,63 @@ browser.menus.onShown.addListener(async info => {
|
||||
? url.pathname.substring(0, url.pathname.length - 1)
|
||||
: url.pathname;
|
||||
|
||||
const pathSegments = pathTrimmed.split("/")
|
||||
.filter(segment => segment)
|
||||
.reverse();
|
||||
const pathSegments = pathTrimmed
|
||||
.split("/")
|
||||
.filter(segment => segment)
|
||||
.reverse();
|
||||
|
||||
if (pathSegments.length) {
|
||||
for (let i = 0; i < pathSegments.length; i++) {
|
||||
const partialPath = pathSegments
|
||||
.slice(i)
|
||||
.reverse()
|
||||
.join("/");
|
||||
const partialPath = pathSegments.slice(i).reverse().join("/");
|
||||
|
||||
const pattern = `${portlessOrigin}/${partialPath}/*`;
|
||||
|
||||
const partialPathMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd", pattern)
|
||||
, parentId: menuIdWhitelist
|
||||
title: _("contextAddToWhitelistAdvancedAdd", pattern),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
partialPathMenuId, pattern);
|
||||
whitelistChildMenuPatterns.set(partialPathMenuId, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wildcardProtocolMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd"
|
||||
, patternWildcardProtocol)
|
||||
, parentId: menuIdWhitelist
|
||||
title: _("contextAddToWhitelistAdvancedAdd", patternWildcardProtocol),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardProtocolMenuId, patternWildcardProtocol);
|
||||
|
||||
wildcardProtocolMenuId,
|
||||
patternWildcardProtocol
|
||||
);
|
||||
|
||||
const wildcardSubdomainMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd"
|
||||
, patternWildcardSubdomain)
|
||||
, parentId: menuIdWhitelist
|
||||
title: _("contextAddToWhitelistAdvancedAdd", patternWildcardSubdomain),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardSubdomainMenuId, patternWildcardSubdomain);
|
||||
|
||||
wildcardSubdomainMenuId,
|
||||
patternWildcardSubdomain
|
||||
);
|
||||
|
||||
const wildcardProtocolAndSubdomainMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd"
|
||||
, patternWildcardProtocolAndSubdomain)
|
||||
, parentId: menuIdWhitelist
|
||||
title: _(
|
||||
"contextAddToWhitelistAdvancedAdd",
|
||||
patternWildcardProtocolAndSubdomain
|
||||
),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardProtocolAndSubdomainMenuId
|
||||
, patternWildcardProtocolAndSubdomain);
|
||||
|
||||
wildcardProtocolAndSubdomainMenuId,
|
||||
patternWildcardProtocolAndSubdomain
|
||||
);
|
||||
|
||||
await browser.menus.refresh();
|
||||
});
|
||||
|
||||
|
||||
options.addEventListener("changed", async ev => {
|
||||
const alteredOpts = ev.detail;
|
||||
const newOpts = await options.getAll();
|
||||
|
||||
@@ -8,17 +8,16 @@ import { Message, Port } from "../messaging";
|
||||
import { ReceiverDevice } from "../types";
|
||||
import { ReceiverStatus } from "../shim/cast/types";
|
||||
|
||||
|
||||
interface EventMap {
|
||||
"receiverDeviceUp": { receiverDevice: ReceiverDevice }
|
||||
, "receiverDeviceDown": { receiverDeviceId: string }
|
||||
, "receiverDeviceUpdated": {
|
||||
receiverDeviceId: string
|
||||
, status: ReceiverStatus
|
||||
}
|
||||
receiverDeviceUp: { receiverDevice: ReceiverDevice };
|
||||
receiverDeviceDown: { receiverDeviceId: string };
|
||||
receiverDeviceUpdated: {
|
||||
receiverDeviceId: string;
|
||||
status: ReceiverStatus;
|
||||
};
|
||||
}
|
||||
|
||||
export default new class extends TypedEventTarget<EventMap> {
|
||||
export default new (class extends TypedEventTarget<EventMap> {
|
||||
/**
|
||||
* Map of receiver device IDs to devices. Updated as
|
||||
* receiverDevice messages are received from the bridge.
|
||||
@@ -27,7 +26,6 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
|
||||
private bridgePort?: Port;
|
||||
|
||||
|
||||
async init() {
|
||||
if (!this.bridgePort) {
|
||||
await this.refresh();
|
||||
@@ -47,8 +45,8 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
port.onDisconnect.addListener(this.onBridgeDisconnect);
|
||||
|
||||
port.postMessage({
|
||||
subject: "bridge:startDiscovery"
|
||||
, data: {
|
||||
subject: "bridge:startDiscovery",
|
||||
data: {
|
||||
// Also send back status messages
|
||||
shouldWatchStatus: true
|
||||
}
|
||||
@@ -69,15 +67,17 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
*/
|
||||
stopReceiverApp(receiverDeviceId: string) {
|
||||
if (!this.bridgePort) {
|
||||
logger.error("Failed to stop receiver device, no bridge connection");
|
||||
logger.error(
|
||||
"Failed to stop receiver device, no bridge connection"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const receiverDevice = this.receiverDevices.get(receiverDeviceId);
|
||||
if (receiverDevice) {
|
||||
this.bridgePort.postMessage({
|
||||
subject: "bridge:stopCastApp"
|
||||
, data: { receiverDevice }
|
||||
subject: "bridge:stopCastApp",
|
||||
data: { receiverDevice }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -89,10 +89,10 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
|
||||
this.receiverDevices.set(receiverDevice.id, receiverDevice);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("receiverDeviceUp"
|
||||
, {
|
||||
detail: { receiverDevice }
|
||||
}));
|
||||
new CustomEvent("receiverDeviceUp", {
|
||||
detail: { receiverDevice }
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -104,10 +104,10 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
this.receiverDevices.delete(receiverDeviceId);
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("receiverDeviceDown"
|
||||
, {
|
||||
detail: { receiverDeviceId }
|
||||
}));
|
||||
new CustomEvent("receiverDeviceDown", {
|
||||
detail: { receiverDeviceId }
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -115,10 +115,12 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
case "main:receiverDeviceUpdated": {
|
||||
const { receiverDeviceId, status } = message.data;
|
||||
const receiverDevice =
|
||||
this.receiverDevices.get(receiverDeviceId);
|
||||
this.receiverDevices.get(receiverDeviceId);
|
||||
|
||||
if (!receiverDevice) {
|
||||
logger.error(`Receiver ID \`${receiverDeviceId}\` not found!`);
|
||||
logger.error(
|
||||
`Receiver ID \`${receiverDeviceId}\` not found!`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -129,27 +131,27 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
|
||||
if (status.applications) {
|
||||
receiverDevice.status.applications =
|
||||
status.applications;
|
||||
status.applications;
|
||||
}
|
||||
} else {
|
||||
receiverDevice.status = status;
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("receiverDeviceUpdated"
|
||||
, {
|
||||
detail: {
|
||||
receiverDeviceId
|
||||
, status: receiverDevice.status
|
||||
}
|
||||
}));
|
||||
new CustomEvent("receiverDeviceUpdated", {
|
||||
detail: {
|
||||
receiverDeviceId,
|
||||
status: receiverDevice.status
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onBridgeDisconnect = () => {
|
||||
// Notify listeners of device availablility
|
||||
for (const [ , receiverDevice ] of this.receiverDevices) {
|
||||
for (const [, receiverDevice] of this.receiverDevices) {
|
||||
const event = new CustomEvent("receiverDeviceDown", {
|
||||
detail: { receiverDeviceId: receiverDevice.id }
|
||||
});
|
||||
@@ -163,5 +165,5 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
window.setTimeout(() => {
|
||||
this.refresh();
|
||||
}, 10000);
|
||||
}
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -8,25 +8,22 @@ import { TypedEventTarget } from "../../lib/TypedEventTarget";
|
||||
import { getWindowCenteredProps, WindowCenteredProps } from "../../lib/utils";
|
||||
import { ReceiverDevice } from "../../types";
|
||||
|
||||
import { ReceiverSelectionCast
|
||||
, ReceiverSelectionStop
|
||||
, ReceiverSelectorMediaType } from "./index";
|
||||
|
||||
|
||||
import {
|
||||
ReceiverSelectionCast,
|
||||
ReceiverSelectionStop,
|
||||
ReceiverSelectorMediaType
|
||||
} from "./index";
|
||||
|
||||
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
|
||||
|
||||
|
||||
interface ReceiverSelectorEvents {
|
||||
"selected": ReceiverSelectionCast;
|
||||
"error": string;
|
||||
"cancelled": void;
|
||||
"stop": ReceiverSelectionStop;
|
||||
selected: ReceiverSelectionCast;
|
||||
error: string;
|
||||
cancelled: void;
|
||||
stop: ReceiverSelectionStop;
|
||||
}
|
||||
|
||||
export default class ReceiverSelector
|
||||
extends TypedEventTarget<ReceiverSelectorEvents> {
|
||||
|
||||
export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorEvents> {
|
||||
private windowId?: number;
|
||||
|
||||
private messagePort?: Port;
|
||||
@@ -65,11 +62,11 @@ export default class ReceiverSelector
|
||||
}
|
||||
|
||||
public async open(
|
||||
receivers: ReceiverDevice[]
|
||||
, defaultMediaType: ReceiverSelectorMediaType
|
||||
, availableMediaTypes: ReceiverSelectorMediaType
|
||||
, appId?: string): Promise<void> {
|
||||
|
||||
receivers: ReceiverDevice[],
|
||||
defaultMediaType: ReceiverSelectorMediaType,
|
||||
availableMediaTypes: ReceiverSelectorMediaType,
|
||||
appId?: string
|
||||
): Promise<void> {
|
||||
this.appId = appId;
|
||||
|
||||
// If popup already exists, close it
|
||||
@@ -81,27 +78,28 @@ export default class ReceiverSelector
|
||||
this.defaultMediaType = defaultMediaType;
|
||||
this.availableMediaTypes = availableMediaTypes;
|
||||
|
||||
|
||||
let centeredProps: WindowCenteredProps = {
|
||||
left: 100
|
||||
, top: 100
|
||||
, width: 350
|
||||
, height: 200
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: 350,
|
||||
height: 200
|
||||
};
|
||||
|
||||
try {
|
||||
// Calculate centered size/position based on current window
|
||||
centeredProps = getWindowCenteredProps(
|
||||
await browser.windows.getCurrent()
|
||||
, centeredProps.width, centeredProps.height);
|
||||
await browser.windows.getCurrent(),
|
||||
centeredProps.width,
|
||||
centeredProps.height
|
||||
);
|
||||
} catch {
|
||||
// Shouldn't ever hit this, but defaults are provided in case
|
||||
}
|
||||
|
||||
const popup = await browser.windows.create({
|
||||
url: POPUP_URL
|
||||
, type: "popup"
|
||||
, ...centeredProps
|
||||
url: POPUP_URL,
|
||||
type: "popup",
|
||||
...centeredProps
|
||||
});
|
||||
|
||||
if (popup?.id === undefined) {
|
||||
@@ -116,22 +114,23 @@ export default class ReceiverSelector
|
||||
...centeredProps
|
||||
});
|
||||
|
||||
|
||||
const closeIfFocusLost = await options.get(
|
||||
"receiverSelectorCloseIfFocusLost");
|
||||
"receiverSelectorCloseIfFocusLost"
|
||||
);
|
||||
|
||||
if (closeIfFocusLost) {
|
||||
// Add focus listener
|
||||
browser.windows.onFocusChanged.addListener(
|
||||
this.onWindowsFocusChanged);
|
||||
this.onWindowsFocusChanged
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public update(receivers: ReceiverDevice[]) {
|
||||
this.receivers = receivers;
|
||||
this.messagePort?.postMessage({
|
||||
subject: "popup:update"
|
||||
, data: {
|
||||
subject: "popup:update",
|
||||
data: {
|
||||
receivers: this.receivers
|
||||
}
|
||||
});
|
||||
@@ -167,23 +166,25 @@ export default class ReceiverSelector
|
||||
this.messagePortDisconnected = true;
|
||||
});
|
||||
|
||||
if (!this.receivers
|
||||
|| !this.defaultMediaType
|
||||
|| !this.availableMediaTypes) {
|
||||
if (
|
||||
!this.receivers ||
|
||||
!this.defaultMediaType ||
|
||||
!this.availableMediaTypes
|
||||
) {
|
||||
throw logger.error("Popup receiver data not found.");
|
||||
}
|
||||
|
||||
this.messagePort.postMessage({
|
||||
subject: "popup:init"
|
||||
, data: { appId: this.appId }
|
||||
subject: "popup:init",
|
||||
data: { appId: this.appId }
|
||||
});
|
||||
|
||||
this.messagePort.postMessage({
|
||||
subject: "popup:update"
|
||||
, data: {
|
||||
receivers: this.receivers
|
||||
, defaultMediaType: this.defaultMediaType
|
||||
, availableMediaTypes: this.availableMediaTypes
|
||||
subject: "popup:update",
|
||||
data: {
|
||||
receivers: this.receivers,
|
||||
defaultMediaType: this.defaultMediaType,
|
||||
availableMediaTypes: this.availableMediaTypes
|
||||
}
|
||||
});
|
||||
|
||||
@@ -197,17 +198,21 @@ export default class ReceiverSelector
|
||||
switch (message.subject) {
|
||||
case "receiverSelector:selected": {
|
||||
this.wasReceiverSelected = true;
|
||||
this.dispatchEvent(new CustomEvent("selected", {
|
||||
detail: message.data
|
||||
}));
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("selected", {
|
||||
detail: message.data
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "receiverSelector:stop": {
|
||||
this.dispatchEvent(new CustomEvent("stop", {
|
||||
detail: message.data
|
||||
}));
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("stop", {
|
||||
detail: message.data
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -226,7 +231,8 @@ export default class ReceiverSelector
|
||||
|
||||
browser.windows.onRemoved.removeListener(this.onWindowsRemoved);
|
||||
browser.windows.onFocusChanged.removeListener(
|
||||
this.onWindowsFocusChanged);
|
||||
this.onWindowsFocusChanged
|
||||
);
|
||||
|
||||
if (!this.wasReceiverSelected) {
|
||||
this.dispatchEvent(new CustomEvent("cancelled"));
|
||||
@@ -247,12 +253,14 @@ export default class ReceiverSelector
|
||||
* `WINDOW_ID_NONE` or if the popup window is re-focused.
|
||||
*/
|
||||
private onWindowsFocusChanged(windowId: number) {
|
||||
if (windowId !== browser.windows.WINDOW_ID_NONE
|
||||
&& windowId !== this.windowId) {
|
||||
|
||||
if (
|
||||
windowId !== browser.windows.WINDOW_ID_NONE &&
|
||||
windowId !== this.windowId
|
||||
) {
|
||||
// Only run once
|
||||
browser.windows.onFocusChanged.removeListener(
|
||||
this.onWindowsFocusChanged);
|
||||
this.onWindowsFocusChanged
|
||||
);
|
||||
|
||||
if (this.windowId) {
|
||||
browser.windows.remove(this.windowId);
|
||||
|
||||
@@ -8,18 +8,18 @@ import receiverDevices from "../receiverDevices";
|
||||
|
||||
import { getMediaTypesForPageUrl } from "../../lib/utils";
|
||||
|
||||
import { ReceiverSelection
|
||||
, ReceiverSelectionActionType
|
||||
, ReceiverSelectorMediaType } from "./index";
|
||||
import {
|
||||
ReceiverSelection,
|
||||
ReceiverSelectionActionType,
|
||||
ReceiverSelectorMediaType
|
||||
} from "./index";
|
||||
|
||||
import ReceiverSelector from "./ReceiverSelector";
|
||||
|
||||
|
||||
async function createSelector() {
|
||||
return new ReceiverSelector();
|
||||
}
|
||||
|
||||
|
||||
let sharedSelector: ReceiverSelector;
|
||||
|
||||
async function getSelector() {
|
||||
@@ -34,7 +34,6 @@ async function getSelector() {
|
||||
return sharedSelector;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens a receiver selector with the specified
|
||||
* default/available media types.
|
||||
@@ -46,21 +45,18 @@ async function getSelector() {
|
||||
* - Rejects if the selection fails.
|
||||
*/
|
||||
async function getSelection(
|
||||
contextTabId: number
|
||||
, contextFrameId = 0
|
||||
, withMediaSender = false)
|
||||
: Promise<ReceiverSelection | null> {
|
||||
|
||||
contextTabId: number,
|
||||
contextFrameId = 0,
|
||||
withMediaSender = false
|
||||
): Promise<ReceiverSelection | null> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let currentShim = ShimManager.getShim(
|
||||
contextTabId, contextFrameId);
|
||||
let currentShim = ShimManager.getShim(contextTabId, contextFrameId);
|
||||
|
||||
/**
|
||||
* If the current context is running the mirroring app, pretend
|
||||
* it doesn't exist because it shouldn't be launched like this.
|
||||
*/
|
||||
if (currentShim?.appId ===
|
||||
await options.get("mirroringAppId")) {
|
||||
if (currentShim?.appId === (await options.get("mirroringAppId"))) {
|
||||
currentShim = undefined;
|
||||
}
|
||||
|
||||
@@ -69,13 +65,15 @@ async function getSelection(
|
||||
|
||||
try {
|
||||
const { url } = await browser.webNavigation.getFrame({
|
||||
tabId: contextTabId
|
||||
, frameId: contextFrameId
|
||||
tabId: contextTabId,
|
||||
frameId: contextFrameId
|
||||
});
|
||||
|
||||
availableMediaTypes = getMediaTypesForPageUrl(url);
|
||||
} catch {
|
||||
logger.error("Failed to locate frame, falling back to default available media types.");
|
||||
logger.error(
|
||||
"Failed to locate frame, falling back to default available media types."
|
||||
);
|
||||
availableMediaTypes = ReceiverSelectorMediaType.File;
|
||||
}
|
||||
|
||||
@@ -90,8 +88,8 @@ async function getSelection(
|
||||
// Remove mirroring media types if mirroring is not enabled
|
||||
if (!opts.mirroringEnabled) {
|
||||
availableMediaTypes &= ~(
|
||||
ReceiverSelectorMediaType.Tab
|
||||
| ReceiverSelectorMediaType.Screen);
|
||||
ReceiverSelectorMediaType.Tab | ReceiverSelectorMediaType.Screen
|
||||
);
|
||||
}
|
||||
|
||||
// Remove file media type if local media is not enabled
|
||||
@@ -107,26 +105,28 @@ async function getSelection(
|
||||
// Get a new selector for each selection
|
||||
sharedSelector = await createSelector();
|
||||
|
||||
|
||||
function onReceiverChange() {
|
||||
sharedSelector.update(receiverDevices.getDevices());
|
||||
}
|
||||
|
||||
receiverDevices.addEventListener("receiverDeviceUp", onReceiverChange);
|
||||
receiverDevices.addEventListener(
|
||||
"receiverDeviceUp", onReceiverChange);
|
||||
"receiverDeviceDown",
|
||||
onReceiverChange
|
||||
);
|
||||
receiverDevices.addEventListener(
|
||||
"receiverDeviceDown", onReceiverChange);
|
||||
receiverDevices.addEventListener(
|
||||
"receiverDeviceUpdated", onReceiverChange);
|
||||
|
||||
"receiverDeviceUpdated",
|
||||
onReceiverChange
|
||||
);
|
||||
|
||||
let onSelected: any;
|
||||
let onCancelled: any;
|
||||
let onError: any;
|
||||
let onStop: any;
|
||||
|
||||
type EvParamsType =
|
||||
Parameters<typeof sharedSelector.addEventListener>[0];
|
||||
type EvParamsType = Parameters<
|
||||
typeof sharedSelector.addEventListener
|
||||
>[0];
|
||||
|
||||
function storeListener<T>(type: EvParamsType, fn: T) {
|
||||
if (type === "selected") {
|
||||
@@ -149,67 +149,78 @@ async function getSelection(
|
||||
sharedSelector.removeEventListener("stop", onStop);
|
||||
|
||||
receiverDevices.removeEventListener(
|
||||
"receiverDeviceUp", onReceiverChange);
|
||||
"receiverDeviceUp",
|
||||
onReceiverChange
|
||||
);
|
||||
receiverDevices.removeEventListener(
|
||||
"receiverDeviceDown", onReceiverChange);
|
||||
"receiverDeviceDown",
|
||||
onReceiverChange
|
||||
);
|
||||
receiverDevices.removeEventListener(
|
||||
"receiverDeviceUpdated", onReceiverChange);
|
||||
"receiverDeviceUpdated",
|
||||
onReceiverChange
|
||||
);
|
||||
}
|
||||
|
||||
sharedSelector.addEventListener("selected"
|
||||
, storeListener("selected", ev => {
|
||||
sharedSelector.addEventListener(
|
||||
"selected",
|
||||
storeListener("selected", ev => {
|
||||
logger.info("Selected receiver", ev.detail);
|
||||
resolve({
|
||||
actionType: ReceiverSelectionActionType.Cast,
|
||||
receiver: ev.detail.receiver,
|
||||
mediaType: ev.detail.mediaType,
|
||||
filePath: ev.detail.filePath
|
||||
});
|
||||
removeListeners();
|
||||
})
|
||||
);
|
||||
|
||||
logger.info("Selected receiver", ev.detail);
|
||||
resolve({
|
||||
actionType: ReceiverSelectionActionType.Cast
|
||||
, receiver: ev.detail.receiver
|
||||
, mediaType: ev.detail.mediaType
|
||||
, filePath: ev.detail.filePath
|
||||
});
|
||||
removeListeners();
|
||||
}));
|
||||
sharedSelector.addEventListener(
|
||||
"cancelled",
|
||||
storeListener("cancelled", () => {
|
||||
logger.info("Cancelled receiver selection");
|
||||
resolve(null);
|
||||
removeListeners();
|
||||
})
|
||||
);
|
||||
|
||||
sharedSelector.addEventListener("cancelled"
|
||||
, storeListener("cancelled", () => {
|
||||
sharedSelector.addEventListener(
|
||||
"error",
|
||||
storeListener("error", () => {
|
||||
reject();
|
||||
removeListeners();
|
||||
})
|
||||
);
|
||||
|
||||
logger.info("Cancelled receiver selection");
|
||||
resolve(null);
|
||||
removeListeners();
|
||||
}));
|
||||
sharedSelector.addEventListener(
|
||||
"stop",
|
||||
storeListener("stop", async ev => {
|
||||
logger.info("Stopping receiver app...", ev.detail);
|
||||
|
||||
sharedSelector.addEventListener("error"
|
||||
, storeListener("error", () => {
|
||||
reject();
|
||||
removeListeners();
|
||||
}));
|
||||
|
||||
sharedSelector.addEventListener("stop"
|
||||
, storeListener("stop", async ev => {
|
||||
|
||||
logger.info("Stopping receiver app...", ev.detail);
|
||||
|
||||
receiverDevices.stopReceiverApp(ev.detail.receiver.id);
|
||||
|
||||
resolve({
|
||||
actionType: ReceiverSelectionActionType.Stop
|
||||
, receiver: ev.detail.receiver
|
||||
});
|
||||
removeListeners();
|
||||
}));
|
||||
receiverDevices.stopReceiverApp(ev.detail.receiver.id);
|
||||
|
||||
resolve({
|
||||
actionType: ReceiverSelectionActionType.Stop,
|
||||
receiver: ev.detail.receiver
|
||||
});
|
||||
removeListeners();
|
||||
})
|
||||
);
|
||||
|
||||
// Ensure status manager is initialized
|
||||
await receiverDevices.init();
|
||||
|
||||
sharedSelector.open(
|
||||
receiverDevices.getDevices()
|
||||
, defaultMediaType
|
||||
, availableMediaTypes
|
||||
, currentShim?.appId);
|
||||
receiverDevices.getDevices(),
|
||||
defaultMediaType,
|
||||
availableMediaTypes,
|
||||
currentShim?.appId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
getSelection
|
||||
, getSelector
|
||||
getSelection,
|
||||
getSelector
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
"use strict";
|
||||
|
||||
export enum ReceiverSelectorType {
|
||||
Popup
|
||||
, Native
|
||||
Popup,
|
||||
Native
|
||||
}
|
||||
|
||||
export enum ReceiverSelectorMediaType {
|
||||
App = 1
|
||||
, Tab = 2
|
||||
, Screen = 4
|
||||
, File = 8
|
||||
App = 1,
|
||||
Tab = 2,
|
||||
Screen = 4,
|
||||
File = 8
|
||||
}
|
||||
|
||||
export enum ReceiverSelectionActionType {
|
||||
Cast = 1
|
||||
, Stop = 2
|
||||
Cast = 1,
|
||||
Stop = 2
|
||||
}
|
||||
|
||||
export interface ReceiverSelectionCast {
|
||||
|
||||
@@ -5,21 +5,23 @@ import options from "../lib/options";
|
||||
|
||||
import { getChromeUserAgent } from "../lib/userAgents";
|
||||
|
||||
import { CAST_FRAMEWORK_LOADER_SCRIPT_URL
|
||||
, CAST_LOADER_SCRIPT_URL } from "../lib/endpoints";
|
||||
|
||||
import {
|
||||
CAST_FRAMEWORK_LOADER_SCRIPT_URL,
|
||||
CAST_LOADER_SCRIPT_URL
|
||||
} from "../lib/endpoints";
|
||||
|
||||
// Missing on @types/firefox-webext-browser
|
||||
type OnBeforeSendHeadersDetails = Parameters<Parameters<
|
||||
typeof browser.webRequest.onBeforeSendHeaders.addListener>[0]>[0] & {
|
||||
frameAncestors?: Array<{ url: string, frameId: number }>
|
||||
type OnBeforeSendHeadersDetails = Parameters<
|
||||
Parameters<typeof browser.webRequest.onBeforeSendHeaders.addListener>[0]
|
||||
>[0] & {
|
||||
frameAncestors?: Array<{ url: string; frameId: number }>;
|
||||
};
|
||||
type OnBeforeRequestDetails = Parameters<Parameters<
|
||||
typeof browser.webRequest.onBeforeRequest.addListener>[0]>[0] & {
|
||||
frameAncestors?: Array<{ url: string, frameId: number }>
|
||||
type OnBeforeRequestDetails = Parameters<
|
||||
Parameters<typeof browser.webRequest.onBeforeRequest.addListener>[0]
|
||||
>[0] & {
|
||||
frameAncestors?: Array<{ url: string; frameId: number }>;
|
||||
};
|
||||
|
||||
|
||||
const originUrlCache: string[] = [];
|
||||
|
||||
let platform: string;
|
||||
@@ -51,40 +53,43 @@ export async function initWhitelist() {
|
||||
options.addEventListener("changed", ev => {
|
||||
const alteredOpts = ev.detail;
|
||||
|
||||
if (alteredOpts.includes("userAgentWhitelist")
|
||||
|| alteredOpts.includes("userAgentWhitelistEnabled")) {
|
||||
if (
|
||||
alteredOpts.includes("userAgentWhitelist") ||
|
||||
alteredOpts.includes("userAgentWhitelistEnabled")
|
||||
) {
|
||||
unregisterUserAgentWhitelist();
|
||||
registerUserAgentWhitelist();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Web apps usually only load the sender library and
|
||||
* provide cast functionality if the browser is detected
|
||||
* as Chrome, so we should rewrite the User-Agent header
|
||||
* to reflect this on whitelisted sites.
|
||||
*/
|
||||
async function onWhitelistedBeforeSendHeaders(
|
||||
details: OnBeforeSendHeadersDetails) {
|
||||
|
||||
async function onWhitelistedBeforeSendHeaders(
|
||||
details: OnBeforeSendHeadersDetails
|
||||
) {
|
||||
if (!details.requestHeaders) {
|
||||
throw logger.error("OnBeforeSendHeaders handler details missing requestHeaders.");
|
||||
throw logger.error(
|
||||
"OnBeforeSendHeaders handler details missing requestHeaders."
|
||||
);
|
||||
}
|
||||
|
||||
if (details.originUrl && !originUrlCache.includes(details.originUrl)) {
|
||||
originUrlCache.push(details.originUrl);
|
||||
}
|
||||
|
||||
const host = details.requestHeaders.find(
|
||||
header => header.name === "Host");
|
||||
const host = details.requestHeaders.find(header => header.name === "Host");
|
||||
|
||||
for (const header of details.requestHeaders) {
|
||||
if (header.name === "User-Agent") {
|
||||
header.value = host?.value === "www.youtube.com"
|
||||
? chromeUserAgentHybrid
|
||||
: chromeUserAgent;
|
||||
header.value =
|
||||
host?.value === "www.youtube.com"
|
||||
? chromeUserAgentHybrid
|
||||
: chromeUserAgent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -101,8 +106,8 @@ export async function initWhitelist() {
|
||||
* main site is whitelisted.
|
||||
*/
|
||||
function onWhitelistedChildBeforeSendHeaders(
|
||||
details: OnBeforeSendHeadersDetails) {
|
||||
|
||||
details: OnBeforeSendHeadersDetails
|
||||
) {
|
||||
if (!details.requestHeaders || !details.frameAncestors) {
|
||||
return;
|
||||
}
|
||||
@@ -110,13 +115,15 @@ function onWhitelistedChildBeforeSendHeaders(
|
||||
for (const ancestor of details.frameAncestors) {
|
||||
if (originUrlCache.includes(ancestor.url)) {
|
||||
const host = details.requestHeaders.find(
|
||||
header => header.name === "Host");
|
||||
header => header.name === "Host"
|
||||
);
|
||||
|
||||
for (const header of details.requestHeaders) {
|
||||
if (header.name === "User-Agent") {
|
||||
header.value = host?.value === "www.youtube.com"
|
||||
? chromeUserAgentHybrid
|
||||
: chromeUserAgent;
|
||||
header.value =
|
||||
host?.value === "www.youtube.com"
|
||||
? chromeUserAgentHybrid
|
||||
: chromeUserAgent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -128,7 +135,6 @@ function onWhitelistedChildBeforeSendHeaders(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sender applications load a cast_sender.js script that
|
||||
* functions as a loader for the internal chrome-extension:
|
||||
@@ -165,16 +171,17 @@ async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) {
|
||||
await browser.tabs.executeScript(details.tabId, {
|
||||
code: `
|
||||
window.isFramework = ${
|
||||
details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL};
|
||||
`
|
||||
, frameId: details.frameId
|
||||
, runAt: "document_start"
|
||||
details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL
|
||||
};
|
||||
`,
|
||||
frameId: details.frameId,
|
||||
runAt: "document_start"
|
||||
});
|
||||
|
||||
await browser.tabs.executeScript(details.tabId, {
|
||||
file: "shim/contentBridge.js"
|
||||
, frameId: details.frameId
|
||||
, runAt: "document_start"
|
||||
file: "shim/contentBridge.js",
|
||||
frameId: details.frameId,
|
||||
runAt: "document_start"
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -182,40 +189,41 @@ async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async function registerUserAgentWhitelist() {
|
||||
const { userAgentWhitelist
|
||||
, userAgentWhitelistEnabled } = await options.getAll();
|
||||
const { userAgentWhitelist, userAgentWhitelistEnabled } =
|
||||
await options.getAll();
|
||||
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
onBeforeCastSDKRequest
|
||||
, { urls: [
|
||||
CAST_LOADER_SCRIPT_URL
|
||||
, CAST_FRAMEWORK_LOADER_SCRIPT_URL ]}
|
||||
, [ "blocking" ]);
|
||||
onBeforeCastSDKRequest,
|
||||
{ urls: [CAST_LOADER_SCRIPT_URL, CAST_FRAMEWORK_LOADER_SCRIPT_URL] },
|
||||
["blocking"]
|
||||
);
|
||||
|
||||
if (!userAgentWhitelistEnabled || !userAgentWhitelist.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||
onWhitelistedBeforeSendHeaders
|
||||
, { urls: userAgentWhitelist }
|
||||
, [ "blocking", "requestHeaders" ]);
|
||||
onWhitelistedBeforeSendHeaders,
|
||||
{ urls: userAgentWhitelist },
|
||||
["blocking", "requestHeaders"]
|
||||
);
|
||||
|
||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||
onWhitelistedChildBeforeSendHeaders
|
||||
, { urls: [ "<all_urls>" ]}
|
||||
, [ "blocking", "requestHeaders" ]);
|
||||
onWhitelistedChildBeforeSendHeaders,
|
||||
{ urls: ["<all_urls>"] },
|
||||
["blocking", "requestHeaders"]
|
||||
);
|
||||
}
|
||||
|
||||
function unregisterUserAgentWhitelist() {
|
||||
originUrlCache.length = 0;
|
||||
|
||||
browser.webRequest.onBeforeSendHeaders
|
||||
.removeListener(onWhitelistedBeforeSendHeaders);
|
||||
browser.webRequest.onBeforeSendHeaders
|
||||
.removeListener(onWhitelistedChildBeforeSendHeaders);
|
||||
browser.webRequest.onBeforeRequest
|
||||
.removeListener(onBeforeCastSDKRequest);
|
||||
browser.webRequest.onBeforeSendHeaders.removeListener(
|
||||
onWhitelistedBeforeSendHeaders
|
||||
);
|
||||
browser.webRequest.onBeforeSendHeaders.removeListener(
|
||||
onWhitelistedChildBeforeSendHeaders
|
||||
);
|
||||
browser.webRequest.onBeforeRequest.removeListener(onBeforeCastSDKRequest);
|
||||
}
|
||||
|
||||
@@ -3,25 +3,22 @@
|
||||
import { ReceiverSelectorType } from "./background/receiverSelector";
|
||||
import { Options } from "./lib/options";
|
||||
|
||||
|
||||
export default {
|
||||
bridgeApplicationName: BRIDGE_NAME
|
||||
, bridgeBackupEnabled: false
|
||||
, bridgeBackupHost: "localhost"
|
||||
, bridgeBackupPort: 9556
|
||||
, mediaEnabled: true
|
||||
, mediaOverlayEnabled: false
|
||||
, mediaSyncElement: false
|
||||
, mediaStopOnUnload: false
|
||||
, localMediaEnabled: true
|
||||
, localMediaServerPort: 9555
|
||||
, mirroringEnabled: false
|
||||
, mirroringAppId: MIRRORING_APP_ID
|
||||
, receiverSelectorCloseIfFocusLost: true
|
||||
, receiverSelectorWaitForConnection: true
|
||||
, userAgentWhitelistEnabled: true
|
||||
, userAgentWhitelistRestrictedEnabled: true
|
||||
, userAgentWhitelist: [
|
||||
"https://www.netflix.com/*"
|
||||
]
|
||||
bridgeApplicationName: BRIDGE_NAME,
|
||||
bridgeBackupEnabled: false,
|
||||
bridgeBackupHost: "localhost",
|
||||
bridgeBackupPort: 9556,
|
||||
mediaEnabled: true,
|
||||
mediaOverlayEnabled: false,
|
||||
mediaSyncElement: false,
|
||||
mediaStopOnUnload: false,
|
||||
localMediaEnabled: true,
|
||||
localMediaServerPort: 9555,
|
||||
mirroringEnabled: false,
|
||||
mirroringAppId: MIRRORING_APP_ID,
|
||||
receiverSelectorCloseIfFocusLost: true,
|
||||
receiverSelectorWaitForConnection: true,
|
||||
userAgentWhitelistEnabled: true,
|
||||
userAgentWhitelistRestrictedEnabled: true,
|
||||
userAgentWhitelist: ["https://www.netflix.com/*"]
|
||||
} as Options;
|
||||
|
||||
61
ext/src/global.d.ts
vendored
61
ext/src/global.d.ts
vendored
@@ -4,11 +4,9 @@ declare const MIRRORING_APP_ID: string;
|
||||
|
||||
declare type Nullable<T> = T | null;
|
||||
|
||||
declare type DistributiveOmit<T, K extends keyof any> =
|
||||
T extends any
|
||||
? Omit<T, K>
|
||||
: never;
|
||||
|
||||
declare type DistributiveOmit<T, K extends keyof any> = T extends any
|
||||
? Omit<T, K>
|
||||
: never;
|
||||
|
||||
declare interface Object {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
@@ -22,16 +20,19 @@ declare interface CanvasRenderingContext2D {
|
||||
DRAWWINDOW_USE_WIDGET_LAYERS: 0x08;
|
||||
DRAWWINDOW_ASYNC_DECODE_IMAGES: 0x10;
|
||||
|
||||
drawWindow (
|
||||
window: Window
|
||||
, x: number, y: number
|
||||
, w: number, h: number
|
||||
, bgColor: string
|
||||
, flags: number): void;
|
||||
drawWindow(
|
||||
window: Window,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
bgColor: string,
|
||||
flags: number
|
||||
): void;
|
||||
}
|
||||
|
||||
declare interface HTMLCanvasElement {
|
||||
captureStream (frameRate?: number): MediaStream;
|
||||
captureStream(frameRate?: number): MediaStream;
|
||||
}
|
||||
|
||||
declare interface MediaTrackConstraints {
|
||||
@@ -39,25 +40,23 @@ declare interface MediaTrackConstraints {
|
||||
}
|
||||
|
||||
declare interface RTCPeerConnection {
|
||||
addStream (mediaStream: MediaStream): void;
|
||||
addStream(mediaStream: MediaStream): void;
|
||||
}
|
||||
|
||||
declare interface MediaDevices {
|
||||
getDisplayMedia (constraints: MediaStreamConstraints)
|
||||
: Promise<MediaStream>;
|
||||
getDisplayMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
||||
}
|
||||
|
||||
|
||||
interface CloneIntoOptions {
|
||||
cloneFunctions?: boolean;
|
||||
wrapReflectors?: boolean;
|
||||
}
|
||||
|
||||
declare function cloneInto<T> (
|
||||
obj: T
|
||||
, targetScope: Window
|
||||
, options?: CloneIntoOptions): T;
|
||||
|
||||
declare function cloneInto<T>(
|
||||
obj: T,
|
||||
targetScope: Window,
|
||||
options?: CloneIntoOptions
|
||||
): T;
|
||||
|
||||
interface ExportFunctionOptions {
|
||||
defineAs: string;
|
||||
@@ -67,11 +66,11 @@ interface ExportFunctionOptions {
|
||||
|
||||
type ExportFunctionFunc = (...args: any[]) => any;
|
||||
|
||||
declare function exportFunction (
|
||||
func: ExportFunctionFunc
|
||||
, targetScope: any
|
||||
, options?: ExportFunctionOptions): ExportFunctionFunc;
|
||||
|
||||
declare function exportFunction(
|
||||
func: ExportFunctionFunc,
|
||||
targetScope: any,
|
||||
options?: ExportFunctionOptions
|
||||
): ExportFunctionFunc;
|
||||
|
||||
// Fix issues with @types/firefox-webext-browser
|
||||
declare namespace browser.events {
|
||||
@@ -80,8 +79,8 @@ declare namespace browser.events {
|
||||
* event types.
|
||||
*/
|
||||
interface Event {
|
||||
addListener (...args: any[]): void | Promise<void>;
|
||||
removeListener (...args: any[]): void | Promise<void>;
|
||||
addListener(...args: any[]): void | Promise<void>;
|
||||
removeListener(...args: any[]): void | Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +96,7 @@ declare namespace browser.runtime {
|
||||
}
|
||||
|
||||
function connect(connectInfo: {
|
||||
name?: string
|
||||
, includeTlsChannelId?: boolean
|
||||
}): browser.runtime.Port;
|
||||
name?: string;
|
||||
includeTlsChannelId?: boolean;
|
||||
}): browser.runtime.Port;
|
||||
}
|
||||
|
||||
@@ -10,14 +10,18 @@ interface TypedEvents {
|
||||
export class TypedEventTarget<T extends TypedEvents> extends EventTarget {
|
||||
// @ts-ignore
|
||||
public addEventListener<K extends keyof T>(
|
||||
type: K, listener: (ev: CustomEvent<T[K]>) => void): void {
|
||||
type: K,
|
||||
listener: (ev: CustomEvent<T[K]>) => void
|
||||
): void {
|
||||
// @ts-ignore
|
||||
super.addEventListener(type as string, listener);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
public removeEventListener<K extends keyof T>(
|
||||
type: K, listener: (ev: CustomEvent<T[K]>) => void): void {
|
||||
type: K,
|
||||
listener: (ev: CustomEvent<T[K]>) => void
|
||||
): void {
|
||||
// @ts-ignore
|
||||
super.removeEventListener(type as string, listener);
|
||||
}
|
||||
|
||||
@@ -4,24 +4,21 @@
|
||||
* Provides a typed interface to runtime.Port objects.
|
||||
*/
|
||||
export interface TypedPort<T>
|
||||
extends Omit<browser.runtime.Port
|
||||
, "onDisconnect"
|
||||
| "onMessage"
|
||||
| "postMessage"> {
|
||||
|
||||
extends Omit<
|
||||
browser.runtime.Port,
|
||||
"onDisconnect" | "onMessage" | "postMessage"
|
||||
> {
|
||||
onDisconnect: {
|
||||
addListener (cb: (port: TypedPort<T>) => void): void | Promise<void>
|
||||
, removeListener (cb: (port: TypedPort<T>) => void): void | Promise<void>
|
||||
, hasListener (cb: (port: TypedPort<T>) => void): boolean
|
||||
, hasListeners (): boolean
|
||||
}
|
||||
|
||||
, onMessage: {
|
||||
addListener (cb: (message: T) => void): void | Promise<void>
|
||||
, removeListener (cb: (message: T) => void): void | Promise<void>
|
||||
, hasListener (cb: (message: T) => void): boolean
|
||||
, hasListeners (): boolean
|
||||
}
|
||||
|
||||
, postMessage (message: T): void
|
||||
addListener(cb: (port: TypedPort<T>) => void): void | Promise<void>;
|
||||
removeListener(cb: (port: TypedPort<T>) => void): void | Promise<void>;
|
||||
hasListener(cb: (port: TypedPort<T>) => void): boolean;
|
||||
hasListeners(): boolean;
|
||||
};
|
||||
onMessage: {
|
||||
addListener(cb: (message: T) => void): void | Promise<void>;
|
||||
removeListener(cb: (message: T) => void): void | Promise<void>;
|
||||
hasListener(cb: (message: T) => void): boolean;
|
||||
hasListeners(): boolean;
|
||||
};
|
||||
postMessage(message: T): void;
|
||||
}
|
||||
|
||||
@@ -13,21 +13,18 @@ export class TypedStorageArea<Schema extends { [key: string]: any }> {
|
||||
this.storageArea = storageArea;
|
||||
}
|
||||
|
||||
public async get<SchemaKey extends keyof Schema
|
||||
, SchemaPartial extends Partial<Schema>>(
|
||||
keys?: SchemaKey
|
||||
| SchemaKey[]
|
||||
| SchemaPartial
|
||||
| null | undefined)
|
||||
: Promise<Pick<Schema, Extract<
|
||||
keyof SchemaPartial, SchemaKey>>> {
|
||||
|
||||
public async get<
|
||||
SchemaKey extends keyof Schema,
|
||||
SchemaPartial extends Partial<Schema>
|
||||
>(
|
||||
keys?: SchemaKey | SchemaKey[] | SchemaPartial | null | undefined
|
||||
): Promise<Pick<Schema, Extract<keyof SchemaPartial, SchemaKey>>> {
|
||||
return await this.storageArea.get(keys);
|
||||
}
|
||||
|
||||
public async getBytesInUse<SchemaKey extends keyof Schema>(
|
||||
keys?: Schema | SchemaKey[]): Promise<number> {
|
||||
|
||||
keys?: Schema | SchemaKey[]
|
||||
): Promise<number> {
|
||||
return await this.storageArea.getBytesInUse(keys);
|
||||
}
|
||||
|
||||
@@ -36,8 +33,8 @@ export class TypedStorageArea<Schema extends { [key: string]: any }> {
|
||||
}
|
||||
|
||||
public async remove<SchemaKey extends keyof Schema>(
|
||||
keys: SchemaKey | SchemaKey[]): Promise<void> {
|
||||
|
||||
keys: SchemaKey | SchemaKey[]
|
||||
): Promise<void> {
|
||||
await this.storageArea.remove(keys);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Port } from "../messaging";
|
||||
import nativeMessaging from "./nativeMessaging";
|
||||
import options from "./options";
|
||||
|
||||
|
||||
export const BRIDGE_TIMEOUT = 5000;
|
||||
|
||||
/**
|
||||
@@ -15,13 +14,16 @@ export const BRIDGE_TIMEOUT = 5000;
|
||||
*/
|
||||
async function connect(): Promise<Port> {
|
||||
const applicationName = await options.get("bridgeApplicationName");
|
||||
const bridgePort = nativeMessaging.connectNative(applicationName) as
|
||||
unknown as Port;
|
||||
const bridgePort = nativeMessaging.connectNative(
|
||||
applicationName
|
||||
) as unknown as Port;
|
||||
|
||||
bridgePort.onDisconnect.addListener(() => {
|
||||
if (bridgePort.error) {
|
||||
console.error(`${applicationName} disconnected:`
|
||||
, bridgePort.error.message);
|
||||
console.error(
|
||||
`${applicationName} disconnected:`,
|
||||
bridgePort.error.message
|
||||
);
|
||||
} else {
|
||||
console.info(`${applicationName} disconnected`);
|
||||
}
|
||||
@@ -30,7 +32,6 @@ async function connect(): Promise<Port> {
|
||||
return bridgePort;
|
||||
}
|
||||
|
||||
|
||||
export interface BridgeInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
@@ -50,77 +51,81 @@ export class BridgeTimedOutError extends Error {}
|
||||
* rules to determine compatiblity, then returns a
|
||||
* BridgeInfo object.
|
||||
*/
|
||||
const getInfo = () => new Promise<BridgeInfo>(async (resolve, reject) => {
|
||||
const applicationName = await options.get("bridgeApplicationName");
|
||||
if (!applicationName) {
|
||||
reject(logger.error("Bridge application name not found."));
|
||||
return;
|
||||
}
|
||||
const getInfo = () =>
|
||||
new Promise<BridgeInfo>(async (resolve, reject) => {
|
||||
const applicationName = await options.get("bridgeApplicationName");
|
||||
if (!applicationName) {
|
||||
reject(logger.error("Bridge application name not found."));
|
||||
return;
|
||||
}
|
||||
|
||||
const bridgeTimeoutId = setTimeout(() => {
|
||||
logger.error("Bridge timed out.");
|
||||
reject(new BridgeTimedOutError());
|
||||
}, BRIDGE_TIMEOUT);
|
||||
const bridgeTimeoutId = setTimeout(() => {
|
||||
logger.error("Bridge timed out.");
|
||||
reject(new BridgeTimedOutError());
|
||||
}, BRIDGE_TIMEOUT);
|
||||
|
||||
let applicationVersion: string;
|
||||
try {
|
||||
const { version } = browser.runtime.getManifest();
|
||||
let applicationVersion: string;
|
||||
try {
|
||||
const { version } = browser.runtime.getManifest();
|
||||
|
||||
applicationVersion = await nativeMessaging.sendNativeMessage(
|
||||
applicationName,
|
||||
{ subject: "bridge:/getInfo", data: version }
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error("Bridge connection failed.");
|
||||
reject(new BridgeConnectionError());
|
||||
clearTimeout(bridgeTimeoutId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
applicationVersion = await nativeMessaging.sendNativeMessage(
|
||||
applicationName
|
||||
, { subject: "bridge:/getInfo"
|
||||
, data: version });
|
||||
} catch (err) {
|
||||
logger.error("Bridge connection failed.");
|
||||
reject(new BridgeConnectionError());
|
||||
clearTimeout(bridgeTimeoutId);
|
||||
|
||||
return;
|
||||
}
|
||||
const extensionVersion = browser.runtime.getManifest().version;
|
||||
const extensionVersionMajor = semver.major(extensionVersion);
|
||||
|
||||
clearTimeout(bridgeTimeoutId);
|
||||
const versionDiff = semver.diff(applicationVersion, extensionVersion);
|
||||
|
||||
const extensionVersion = browser.runtime.getManifest().version;
|
||||
const extensionVersionMajor = semver.major(extensionVersion);
|
||||
/**
|
||||
* If the target version is above 0.x.x range, API is stable
|
||||
* and versions with minor or patch level changes should be
|
||||
* compatible.
|
||||
*/
|
||||
const isVersionCompatible =
|
||||
semver.eq(applicationVersion, extensionVersion) ||
|
||||
(versionDiff !== "major" && extensionVersionMajor !== 0) ||
|
||||
(versionDiff === "patch" && extensionVersionMajor === 0);
|
||||
|
||||
const versionDiff = semver.diff(applicationVersion, extensionVersion);
|
||||
const isVersionExact = semver.eq(applicationVersion, extensionVersion);
|
||||
const isVersionOlder = semver.lt(applicationVersion, extensionVersion);
|
||||
const isVersionNewer = semver.gt(applicationVersion, extensionVersion);
|
||||
|
||||
/**
|
||||
* If the target version is above 0.x.x range, API is stable
|
||||
* and versions with minor or patch level changes should be
|
||||
* compatible.
|
||||
*/
|
||||
const isVersionCompatible =
|
||||
semver.eq(applicationVersion, extensionVersion)
|
||||
|| (versionDiff !== "major" && extensionVersionMajor !== 0)
|
||||
|| (versionDiff === "patch" && extensionVersionMajor === 0);
|
||||
// Print compatibility info to console
|
||||
if (!isVersionCompatible) {
|
||||
logger.error(
|
||||
`Expecting ${applicationName} v${BRIDGE_VERSION}, found v${applicationVersion}. ${
|
||||
isVersionOlder
|
||||
? "Try updating the native app to the latest version."
|
||||
: "Try updating the extension to the latest version"
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const isVersionExact = semver.eq(applicationVersion, extensionVersion);
|
||||
const isVersionOlder = semver.lt(applicationVersion, extensionVersion);
|
||||
const isVersionNewer = semver.gt(applicationVersion, extensionVersion);
|
||||
resolve({
|
||||
name: applicationName,
|
||||
version: applicationVersion,
|
||||
expectedVersion: BRIDGE_VERSION,
|
||||
|
||||
// Print compatibility info to console
|
||||
if (!isVersionCompatible) {
|
||||
logger.error(`Expecting ${applicationName} v${BRIDGE_VERSION}, found v${applicationVersion}. ${
|
||||
isVersionOlder
|
||||
? "Try updating the native app to the latest version."
|
||||
: "Try updating the extension to the latest version"}`);
|
||||
}
|
||||
|
||||
resolve({
|
||||
name: applicationName
|
||||
, version: applicationVersion
|
||||
, expectedVersion: BRIDGE_VERSION
|
||||
|
||||
// Version info
|
||||
, isVersionExact
|
||||
, isVersionCompatible
|
||||
, isVersionOlder
|
||||
, isVersionNewer
|
||||
// Version info
|
||||
isVersionExact,
|
||||
isVersionCompatible,
|
||||
isVersionOlder,
|
||||
isVersionNewer
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export default {
|
||||
connect
|
||||
, getInfo
|
||||
connect,
|
||||
getInfo
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* UA string checking.
|
||||
*/
|
||||
export const CAST_LOADER_SCRIPT_URL =
|
||||
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js";
|
||||
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js";
|
||||
|
||||
/**
|
||||
* Cast Chrome Sender Framework API loader script.
|
||||
@@ -18,8 +18,7 @@ export const CAST_LOADER_SCRIPT_URL =
|
||||
* the framework API script is conditionally loaded in
|
||||
* addition to the regular SDK script.
|
||||
*/
|
||||
export const CAST_FRAMEWORK_LOADER_SCRIPT_URL =
|
||||
`${CAST_LOADER_SCRIPT_URL}?loadCastFramework=1`;
|
||||
export const CAST_FRAMEWORK_LOADER_SCRIPT_URL = `${CAST_LOADER_SCRIPT_URL}?loadCastFramework=1`;
|
||||
|
||||
/**
|
||||
* Cast extension URLs.
|
||||
@@ -29,8 +28,8 @@ export const CAST_FRAMEWORK_LOADER_SCRIPT_URL =
|
||||
* chrome-extension: URLs for compatibility reasons (?).
|
||||
*/
|
||||
export const CAST_SCRIPT_URLS = [
|
||||
"chrome-extension://pkedcjkdefgpdelpbcmbmeomcjbeemfm/cast_sender.js"
|
||||
, "chrome-extension://enhhojjnijigcajfphajepfemndkmdlo/cast_sender.js"
|
||||
"chrome-extension://pkedcjkdefgpdelpbcmbmeomcjbeemfm/cast_sender.js",
|
||||
"chrome-extension://enhhojjnijigcajfphajepfemndkmdlo/cast_sender.js"
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -41,4 +40,4 @@ export const CAST_SCRIPT_URLS = [
|
||||
* opposed to within the cast extension.
|
||||
*/
|
||||
export const CAST_FRAMEWORK_SCRIPT_URL =
|
||||
"https://www.gstatic.com/cast/sdk/libs/sender/1.0/cast_framework.js";
|
||||
"https://www.gstatic.com/cast/sdk/libs/sender/1.0/cast_framework.js";
|
||||
|
||||
@@ -15,21 +15,30 @@ interface KnownApp {
|
||||
*/
|
||||
export default {
|
||||
// Web-supported
|
||||
"CA5E8412": { name: "Netflix" , matches: "https://www.netflix.com/*" }
|
||||
, "233637DE": { name: "YouTube" , matches: "https://www.youtube.com/*" }
|
||||
, "CC32E753": { name: "Spotify" , matches: "https://open.spotify.com/*" }
|
||||
, "5E81F6DB": { name: "BBC iPlayer" , matches: "https://www.bbc.co.uk/iplayer/*" }
|
||||
, "03977A48": { name: "BBC Sounds" , matches: "https://www.bbc.co.uk/sounds/*" }
|
||||
, "AA666EDD": { name: "Crunchyroll" , matches: "https://crunchyroll.com/*" }
|
||||
, "10AAD887": { name: "All 4" , matches: "https://www.channel4.com/*" }
|
||||
, "B3DCF968": { name: "Twitch" , matches: "https://www.twitch.tv/*" }
|
||||
, "B88B034A": { name: "Dailymotion" , matches: "https://www.dailymotion.com/*" }
|
||||
, "C3DE6BC2": { name: "Disney+" , matches: "https://www.disneyplus.com/*" }
|
||||
"CA5E8412": { name: "Netflix", matches: "https://www.netflix.com/*" },
|
||||
"233637DE": { name: "YouTube", matches: "https://www.youtube.com/*" },
|
||||
"CC32E753": { name: "Spotify", matches: "https://open.spotify.com/*" },
|
||||
"5E81F6DB": {
|
||||
name: "BBC iPlayer",
|
||||
matches: "https://www.bbc.co.uk/iplayer/*"
|
||||
},
|
||||
"03977A48": {
|
||||
name: "BBC Sounds",
|
||||
matches: "https://www.bbc.co.uk/sounds/*"
|
||||
},
|
||||
"AA666EDD": { name: "Crunchyroll", matches: "https://crunchyroll.com/*" },
|
||||
"10AAD887": { name: "All 4", matches: "https://www.channel4.com/*" },
|
||||
"B3DCF968": { name: "Twitch", matches: "https://www.twitch.tv/*" },
|
||||
"B88B034A": {
|
||||
name: "Dailymotion",
|
||||
matches: "https://www.dailymotion.com/*"
|
||||
},
|
||||
"C3DE6BC2": { name: "Disney+", matches: "https://www.disneyplus.com/*" },
|
||||
|
||||
// Misc
|
||||
, "17608BC8": { name: "Prime Video" }
|
||||
, "9AC194DC": { name: "Plex" }
|
||||
, "CD7B9F59": { name: "Global Player Live" }
|
||||
"17608BC8": { name: "Prime Video" },
|
||||
"9AC194DC": { name: "Plex" },
|
||||
"CD7B9F59": { name: "Global Player Live" },
|
||||
|
||||
, "CC1AD845": { name: _("popupMediaTypeAppMedia") }
|
||||
"CC1AD845": { name: _("popupMediaTypeAppMedia") }
|
||||
} as Record<string, KnownApp>;
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
import logger from "./logger";
|
||||
import { stringify } from "./utils";
|
||||
|
||||
import { ReceiverSelection
|
||||
, ReceiverSelectionActionType
|
||||
, ReceiverSelectorMediaType } from "../background/receiverSelector";
|
||||
import {
|
||||
ReceiverSelection,
|
||||
ReceiverSelectionActionType,
|
||||
ReceiverSelectorMediaType
|
||||
} from "../background/receiverSelector";
|
||||
|
||||
import ShimManager from "../background/ShimManager";
|
||||
|
||||
|
||||
interface LoadSenderOptions {
|
||||
tabId: number;
|
||||
frameId?: number;
|
||||
@@ -34,13 +35,14 @@ export default async function loadSender(opts: LoadSenderOptions) {
|
||||
case ReceiverSelectorMediaType.App: {
|
||||
const shim = ShimManager.getShim(opts.tabId, opts.frameId);
|
||||
if (!shim) {
|
||||
throw logger.error(`Shim not found at tabId ${
|
||||
opts.tabId} / frameId ${opts.frameId}`);
|
||||
throw logger.error(
|
||||
`Shim not found at tabId ${opts.tabId} / frameId ${opts.frameId}`
|
||||
);
|
||||
}
|
||||
|
||||
shim.contentPort.postMessage({
|
||||
subject: "shim:launchApp"
|
||||
, data: { receiver: opts.selection.receiver }
|
||||
subject: "shim:launchApp",
|
||||
data: { receiver: opts.selection.receiver }
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -52,13 +54,13 @@ export default async function loadSender(opts: LoadSenderOptions) {
|
||||
code: stringify`
|
||||
window.selectedMedia = ${opts.selection.mediaType};
|
||||
window.selectedReceiver = ${opts.selection.receiver};
|
||||
`
|
||||
, frameId: opts.frameId
|
||||
`,
|
||||
frameId: opts.frameId
|
||||
});
|
||||
|
||||
await browser.tabs.executeScript(opts.tabId, {
|
||||
file: "senders/mirroring.js"
|
||||
, frameId: opts.frameId
|
||||
file: "senders/mirroring.js",
|
||||
frameId: opts.frameId
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -69,8 +71,8 @@ export default async function loadSender(opts: LoadSenderOptions) {
|
||||
const { init } = await import("../senders/media");
|
||||
|
||||
init({
|
||||
mediaUrl: fileUrl.href
|
||||
, receiver: opts.selection.receiver
|
||||
mediaUrl: fileUrl.href,
|
||||
receiver: opts.selection.receiver
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
@@ -5,7 +5,6 @@ import options from "./options";
|
||||
|
||||
import { Message, Port } from "../messaging";
|
||||
|
||||
|
||||
type DisconnectListener = (port: Port) => void;
|
||||
type MessageListener = (message: Message) => void;
|
||||
|
||||
@@ -26,7 +25,6 @@ function connectNative(application: string): Port {
|
||||
|
||||
const port = browser.runtime.connectNative(application);
|
||||
|
||||
|
||||
let socket: WebSocket;
|
||||
|
||||
const onDisconnectListeners = new Set<DisconnectListener>();
|
||||
@@ -34,47 +32,47 @@ function connectNative(application: string): Port {
|
||||
|
||||
// Port proxy API
|
||||
const portObject: Port = {
|
||||
error: null as any
|
||||
, name: ""
|
||||
error: null as any,
|
||||
name: "",
|
||||
|
||||
, onDisconnect: {
|
||||
onDisconnect: {
|
||||
addListener(cb: DisconnectListener) {
|
||||
onDisconnectListeners.add(cb);
|
||||
}
|
||||
, removeListener(cb: DisconnectListener) {
|
||||
},
|
||||
removeListener(cb: DisconnectListener) {
|
||||
onDisconnectListeners.delete(cb);
|
||||
}
|
||||
, hasListener(cb: DisconnectListener) {
|
||||
},
|
||||
hasListener(cb: DisconnectListener) {
|
||||
return onDisconnectListeners.has(cb);
|
||||
}
|
||||
, hasListeners() {
|
||||
},
|
||||
hasListeners() {
|
||||
return onDisconnectListeners.size > 0;
|
||||
}
|
||||
}
|
||||
, onMessage: {
|
||||
},
|
||||
onMessage: {
|
||||
addListener(cb: MessageListener) {
|
||||
onMessageListeners.add(cb);
|
||||
}
|
||||
, removeListener(cb: MessageListener) {
|
||||
},
|
||||
removeListener(cb: MessageListener) {
|
||||
onMessageListeners.delete(cb);
|
||||
}
|
||||
, hasListener(cb: MessageListener) {
|
||||
},
|
||||
hasListener(cb: MessageListener) {
|
||||
return onMessageListeners.has(cb);
|
||||
}
|
||||
, hasListeners() {
|
||||
},
|
||||
hasListeners() {
|
||||
return onMessageListeners.size > 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
, disconnect() {
|
||||
disconnect() {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
} else {
|
||||
port.disconnect();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
, postMessage(message) {
|
||||
postMessage(message) {
|
||||
if (socket) {
|
||||
switch (socket.readyState) {
|
||||
case WebSocket.CONNECTING: {
|
||||
@@ -99,11 +97,9 @@ function connectNative(application: string): Port {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
port.onDisconnect.addListener(async () => {
|
||||
const { bridgeBackupEnabled
|
||||
, bridgeBackupHost
|
||||
, bridgeBackupPort } = await options.getAll();
|
||||
const { bridgeBackupEnabled, bridgeBackupHost, bridgeBackupPort } =
|
||||
await options.getAll();
|
||||
|
||||
if (!bridgeBackupEnabled) {
|
||||
portObject.error = {
|
||||
@@ -114,14 +110,17 @@ function connectNative(application: string): Port {
|
||||
listener(portObject);
|
||||
}
|
||||
|
||||
throw logger.error("Bridge connection failed and backup not enabled.");
|
||||
throw logger.error(
|
||||
"Bridge connection failed and backup not enabled."
|
||||
);
|
||||
}
|
||||
|
||||
if (port.error && !isNativeHostStatusKnown) {
|
||||
isNativeHostStatusKnown = true;
|
||||
|
||||
socket = new WebSocket(
|
||||
`ws://${bridgeBackupHost}:${bridgeBackupPort}`);
|
||||
`ws://${bridgeBackupHost}:${bridgeBackupPort}`
|
||||
);
|
||||
|
||||
socket.addEventListener("open", () => {
|
||||
// Send all messages in queue
|
||||
@@ -163,30 +162,28 @@ function connectNative(application: string): Port {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return portObject;
|
||||
}
|
||||
|
||||
async function sendNativeMessage(
|
||||
application: string
|
||||
, message: Message) {
|
||||
|
||||
async function sendNativeMessage(application: string, message: Message) {
|
||||
try {
|
||||
return await browser.runtime.sendNativeMessage(application, message);
|
||||
} catch {
|
||||
const { bridgeBackupEnabled
|
||||
, bridgeBackupHost
|
||||
, bridgeBackupPort } = await options.getAll();
|
||||
const { bridgeBackupEnabled, bridgeBackupHost, bridgeBackupPort } =
|
||||
await options.getAll();
|
||||
|
||||
if (!bridgeBackupEnabled) {
|
||||
throw logger.error("Bridge connection failed and backup not enabled.");
|
||||
throw logger.error(
|
||||
"Bridge connection failed and backup not enabled."
|
||||
);
|
||||
}
|
||||
|
||||
const port = await options.get("bridgeBackupPort");
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(
|
||||
`ws://${bridgeBackupHost}:${bridgeBackupPort}`);
|
||||
`ws://${bridgeBackupHost}:${bridgeBackupPort}`
|
||||
);
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
ws.send(JSON.stringify(message));
|
||||
@@ -205,8 +202,7 @@ async function sendNativeMessage(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
connectNative
|
||||
, sendNativeMessage
|
||||
connectNative,
|
||||
sendNativeMessage
|
||||
};
|
||||
|
||||
@@ -8,9 +8,8 @@ import { ReceiverSelectorType } from "../background/receiverSelector";
|
||||
import { TypedEventTarget } from "./TypedEventTarget";
|
||||
import { TypedStorageArea } from "./TypedStorageArea";
|
||||
|
||||
|
||||
const storageArea = new TypedStorageArea<{
|
||||
options: Options
|
||||
options: Options;
|
||||
}>(browser.storage.sync);
|
||||
|
||||
export interface Options {
|
||||
@@ -35,12 +34,11 @@ export interface Options {
|
||||
[key: string]: Options[keyof Options];
|
||||
}
|
||||
|
||||
|
||||
interface EventMap {
|
||||
"changed": Array<keyof Options>;
|
||||
changed: Array<keyof Options>;
|
||||
}
|
||||
|
||||
export default new class extends TypedEventTarget<EventMap> {
|
||||
export default new (class extends TypedEventTarget<EventMap> {
|
||||
constructor() {
|
||||
super();
|
||||
this.onStorageChanged = this.onStorageChanged.bind(this);
|
||||
@@ -53,9 +51,9 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
}
|
||||
|
||||
private onStorageChanged(
|
||||
changes: { [key: string]: browser.storage.StorageChange }
|
||||
, areaName: string) {
|
||||
|
||||
changes: { [key: string]: browser.storage.StorageChange },
|
||||
areaName: string
|
||||
) {
|
||||
if (areaName !== "sync") {
|
||||
return;
|
||||
}
|
||||
@@ -80,11 +78,16 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
}
|
||||
|
||||
// Array comparison
|
||||
if (oldKeyValue instanceof Array
|
||||
&& newKeyValue instanceof Array) {
|
||||
if (oldKeyValue.length === newKeyValue.length
|
||||
&& oldKeyValue.every((value, index) =>
|
||||
value === newKeyValue[index])) {
|
||||
if (
|
||||
oldKeyValue instanceof Array &&
|
||||
newKeyValue instanceof Array
|
||||
) {
|
||||
if (
|
||||
oldKeyValue.length === newKeyValue.length &&
|
||||
oldKeyValue.every(
|
||||
(value, index) => value === newKeyValue[index]
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -93,9 +96,11 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
changedKeys.push(key);
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent("changed", {
|
||||
detail: changedKeys as Array<keyof Options>
|
||||
}));
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("changed", {
|
||||
detail: changedKeys as Array<keyof Options>
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,15 +140,14 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
* promise.
|
||||
*/
|
||||
public async set<T extends keyof Options>(
|
||||
name: T
|
||||
, value: Options[T]): Promise<void> {
|
||||
|
||||
name: T,
|
||||
value: Options[T]
|
||||
): Promise<void> {
|
||||
const options = await this.getAll();
|
||||
options[name] = value;
|
||||
return this.setAll(options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets existing options from storage and compares it
|
||||
* against defaults. Any options in defaults and not in
|
||||
@@ -153,7 +157,7 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
const newOpts = await this.getAll();
|
||||
|
||||
// Find options not already in storage
|
||||
for (const [ optName, optVal ] of Object.entries(defaults)) {
|
||||
for (const [optName, optVal] of Object.entries(defaults)) {
|
||||
if (!newOpts.hasOwnProperty(optName)) {
|
||||
newOpts[optName] = optVal;
|
||||
}
|
||||
@@ -162,4 +166,4 @@ export default new class extends TypedEventTarget<EventMap> {
|
||||
// Update storage with default values of new options
|
||||
return this.setAll(newOpts);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -5,15 +5,14 @@ const PLATFORM_MAC_HYBRID = "Macintosh; Intel Mac OS X 10.15; rv:72.0";
|
||||
const PLATFORM_WIN = "Windows NT 10.0; Win64; x64";
|
||||
const PLATFORM_LINUX = "X11; Linux x86_64";
|
||||
|
||||
const UA_CHROME = "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36";
|
||||
const UA_CHROME =
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36";
|
||||
const UA_HYBRID = "Chrome/80.0.3987.87 Gecko/20100101 Firefox/72.0";
|
||||
|
||||
export function getChromeUserAgent(platform: string, hybrid = false) {
|
||||
let platformComponent: string;
|
||||
if (platform === "mac") {
|
||||
platformComponent = hybrid
|
||||
? PLATFORM_MAC_HYBRID
|
||||
: PLATFORM_MAC;
|
||||
platformComponent = hybrid ? PLATFORM_MAC_HYBRID : PLATFORM_MAC;
|
||||
} else if (platform === "win") {
|
||||
platformComponent = PLATFORM_WIN;
|
||||
} else if (platform === "linux") {
|
||||
@@ -22,9 +21,7 @@ export function getChromeUserAgent(platform: string, hybrid = false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browserComponent = hybrid
|
||||
? UA_HYBRID
|
||||
: UA_CHROME;
|
||||
const browserComponent = hybrid ? UA_HYBRID : UA_CHROME;
|
||||
|
||||
return `Mozilla/5.0 (${platformComponent}) ${browserComponent}`;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import logger from "./logger";
|
||||
|
||||
import { ReceiverSelectorMediaType } from "../background/receiverSelector";
|
||||
|
||||
|
||||
export function getNextEllipsis(ellipsis: string): string {
|
||||
if (ellipsis === "") return ".";
|
||||
if (ellipsis === ".") return "..";
|
||||
@@ -18,9 +17,9 @@ export function getNextEllipsis(ellipsis: string): string {
|
||||
* Template literal tag function, JSON-encodes substitutions.
|
||||
*/
|
||||
export function stringify(
|
||||
templateStrings: TemplateStringsArray
|
||||
, ...substitutions: any[]) {
|
||||
|
||||
templateStrings: TemplateStringsArray,
|
||||
...substitutions: any[]
|
||||
) {
|
||||
let formattedString = "";
|
||||
|
||||
for (const templateString of templateStrings) {
|
||||
@@ -35,8 +34,8 @@ export function stringify(
|
||||
}
|
||||
|
||||
export function getMediaTypesForPageUrl(
|
||||
pageUrl: string): ReceiverSelectorMediaType {
|
||||
|
||||
pageUrl: string
|
||||
): ReceiverSelectorMediaType {
|
||||
const url = new URL(pageUrl);
|
||||
let availableMediaTypes = ReceiverSelectorMediaType.File;
|
||||
|
||||
@@ -45,18 +44,18 @@ export function getMediaTypesForPageUrl(
|
||||
* Mozilla domains.
|
||||
*/
|
||||
const blockedHosts = [
|
||||
"accounts-static.cdn.mozilla.net"
|
||||
, "accounts.firefox.com"
|
||||
, "addons.cdn.mozilla.net"
|
||||
, "addons.mozilla.org"
|
||||
, "api.accounts.firefox.com"
|
||||
, "content.cdn.mozilla.net"
|
||||
, "discovery.addons.mozilla.org"
|
||||
, "install.mozilla.org"
|
||||
, "oauth.accounts.firefox.com"
|
||||
, "profile.accounts.firefox.com"
|
||||
, "support.mozilla.org"
|
||||
, "sync.services.mozilla.com"
|
||||
"accounts-static.cdn.mozilla.net",
|
||||
"accounts.firefox.com",
|
||||
"addons.cdn.mozilla.net",
|
||||
"addons.mozilla.org",
|
||||
"api.accounts.firefox.com",
|
||||
"content.cdn.mozilla.net",
|
||||
"discovery.addons.mozilla.org",
|
||||
"install.mozilla.org",
|
||||
"oauth.accounts.firefox.com",
|
||||
"profile.accounts.firefox.com",
|
||||
"support.mozilla.org",
|
||||
"sync.services.mozilla.com"
|
||||
];
|
||||
|
||||
if (blockedHosts.includes(url.host)) {
|
||||
@@ -80,7 +79,6 @@ export function getMediaTypesForPageUrl(
|
||||
return availableMediaTypes;
|
||||
}
|
||||
|
||||
|
||||
export interface WindowCenteredProps {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -89,33 +87,37 @@ export interface WindowCenteredProps {
|
||||
}
|
||||
|
||||
export function getWindowCenteredProps(
|
||||
refWin: browser.windows.Window
|
||||
, width: number
|
||||
, height: number): WindowCenteredProps {
|
||||
|
||||
if (refWin.left === undefined || refWin.width === undefined
|
||||
|| refWin.top === undefined || refWin.height === undefined) {
|
||||
refWin: browser.windows.Window,
|
||||
width: number,
|
||||
height: number
|
||||
): WindowCenteredProps {
|
||||
if (
|
||||
refWin.left === undefined ||
|
||||
refWin.width === undefined ||
|
||||
refWin.top === undefined ||
|
||||
refWin.height === undefined
|
||||
) {
|
||||
throw logger.error("refWin missing positional attributes.");
|
||||
}
|
||||
|
||||
const centerX = refWin.left + (refWin.width / 2);
|
||||
const centerY = refWin.top + (refWin.height / 3);
|
||||
const centerX = refWin.left + refWin.width / 2;
|
||||
const centerY = refWin.top + refWin.height / 3;
|
||||
|
||||
return {
|
||||
width, height
|
||||
, left: Math.floor(centerX - width / 2)
|
||||
, top: Math.floor(centerY - height / 2)
|
||||
width,
|
||||
height,
|
||||
left: Math.floor(centerX - width / 2),
|
||||
top: Math.floor(centerY - height / 2)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const REMOTE_MATCH_PATTERN_REGEX = /^(?:(?:(\*|https?|ftp):\/\/(\*|(?:\*\.(?:[^\/\*:]\.?)+(?:[^\.])|[^\/\*:]*))?)(\/.*)|<all_urls>)$/;
|
||||
|
||||
export const REMOTE_MATCH_PATTERN_REGEX =
|
||||
/^(?:(?:(\*|https?|ftp):\/\/(\*|(?:\*\.(?:[^\/\*:]\.?)+(?:[^\.])|[^\/\*:]*))?)(\/.*)|<all_urls>)$/;
|
||||
|
||||
export function loadScript(
|
||||
scriptUrl: string
|
||||
, doc: Document = document): HTMLScriptElement {
|
||||
|
||||
scriptUrl: string,
|
||||
doc: Document = document
|
||||
): HTMLScriptElement {
|
||||
const scriptElement = doc.createElement("script");
|
||||
scriptElement.src = scriptUrl;
|
||||
(doc.head || doc.documentElement).append(scriptElement);
|
||||
|
||||
@@ -4,18 +4,21 @@ import { TypedPort } from "./lib/TypedPort";
|
||||
import { BridgeInfo } from "./lib/bridge";
|
||||
|
||||
import { ReceiverSelectorMediaType } from "./background/receiverSelector";
|
||||
import { ReceiverSelection
|
||||
, ReceiverSelectionCast
|
||||
, ReceiverSelectionStop } from "./background/receiverSelector";
|
||||
import {
|
||||
ReceiverSelection,
|
||||
ReceiverSelectionCast,
|
||||
ReceiverSelectionStop
|
||||
} from "./background/receiverSelector";
|
||||
|
||||
import { CastSessionCreated
|
||||
, CastSessionUpdated
|
||||
, ReceiverStatus
|
||||
, SenderMessage } from "./shim/cast/types";
|
||||
import {
|
||||
CastSessionCreated,
|
||||
CastSessionUpdated,
|
||||
ReceiverStatus,
|
||||
SenderMessage
|
||||
} from "./shim/cast/types";
|
||||
|
||||
import { ReceiverDevice } from "./types";
|
||||
|
||||
|
||||
/**
|
||||
* Messages are JSON objects with a `subject` string key and a
|
||||
* generic `data` key:
|
||||
@@ -29,38 +32,31 @@ import { ReceiverDevice } from "./types";
|
||||
* as the value in the message tables.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Messages exclusively used internally between extension
|
||||
* components.
|
||||
*/
|
||||
type ExtMessageDefinitions = {
|
||||
"popup:init": { appId?: string }
|
||||
, "popup:update": {
|
||||
receivers: ReceiverDevice[]
|
||||
, defaultMediaType?: ReceiverSelectorMediaType
|
||||
, availableMediaTypes?: ReceiverSelectorMediaType
|
||||
}
|
||||
, "popup:close": {}
|
||||
|
||||
, "receiverSelector:selected": ReceiverSelection
|
||||
, "receiverSelector:stop": ReceiverSelection
|
||||
|
||||
, "main:shimReady": { appId: string }
|
||||
|
||||
, "main:selectReceiver": {}
|
||||
, "shim:selectReceiver/selected": ReceiverSelectionCast
|
||||
, "shim:selectReceiver/stopped": ReceiverSelectionStop
|
||||
, "shim:selectReceiver/cancelled": {}
|
||||
|
||||
, "main:sessionCreated": {}
|
||||
|
||||
, "shim:initialized": BridgeInfo
|
||||
, "shim:serviceUp": { receiverDevice: ReceiverDevice }
|
||||
, "shim:serviceDown": { receiverDeviceId: ReceiverDevice["id"] }
|
||||
|
||||
, "shim:launchApp": { receiver: ReceiverDevice }
|
||||
}
|
||||
"popup:init": { appId?: string };
|
||||
"popup:update": {
|
||||
receivers: ReceiverDevice[];
|
||||
defaultMediaType?: ReceiverSelectorMediaType;
|
||||
availableMediaTypes?: ReceiverSelectorMediaType;
|
||||
};
|
||||
"popup:close": {};
|
||||
"receiverSelector:selected": ReceiverSelection;
|
||||
"receiverSelector:stop": ReceiverSelection;
|
||||
"main:shimReady": { appId: string };
|
||||
"main:selectReceiver": {};
|
||||
"shim:selectReceiver/selected": ReceiverSelectionCast;
|
||||
"shim:selectReceiver/stopped": ReceiverSelectionStop;
|
||||
"shim:selectReceiver/cancelled": {};
|
||||
"main:sessionCreated": {};
|
||||
"shim:initialized": BridgeInfo;
|
||||
"shim:serviceUp": { receiverDevice: ReceiverDevice };
|
||||
"shim:serviceDown": { receiverDeviceId: ReceiverDevice["id"] };
|
||||
"shim:launchApp": { receiver: ReceiverDevice };
|
||||
};
|
||||
|
||||
/**
|
||||
* Messages that cross the native messaging channel. MUST keep
|
||||
@@ -68,89 +64,76 @@ type ExtMessageDefinitions = {
|
||||
* app/bridge/messaging.ts > MessagesBase
|
||||
*/
|
||||
type AppMessageDefinitions = {
|
||||
"shim:castSessionCreated": CastSessionCreated
|
||||
, "shim:castSessionUpdated": CastSessionUpdated
|
||||
, "shim:castSessionStopped": {
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
, "shim:receivedCastSessionMessage": {
|
||||
sessionId: string
|
||||
, namespace: string
|
||||
, messageData: string
|
||||
}
|
||||
|
||||
, "shim:impl_sendCastMessage": {
|
||||
sessionId: string
|
||||
, messageId: string
|
||||
, error?: string
|
||||
}
|
||||
|
||||
, "bridge:createCastSession": {
|
||||
appId: string
|
||||
, receiverDevice: ReceiverDevice
|
||||
}
|
||||
, "bridge:sendCastReceiverMessage": {
|
||||
sessionId: string
|
||||
, messageData: SenderMessage
|
||||
, messageId: string
|
||||
}
|
||||
, "bridge:sendCastSessionMessage": {
|
||||
sessionId: string
|
||||
, namespace: string
|
||||
, messageData: object | string
|
||||
, messageId: string
|
||||
}
|
||||
|
||||
, "bridge:stopCastApp": { receiverDevice: ReceiverDevice }
|
||||
"shim:castSessionCreated": CastSessionCreated;
|
||||
"shim:castSessionUpdated": CastSessionUpdated;
|
||||
"shim:castSessionStopped": {
|
||||
sessionId: string;
|
||||
};
|
||||
"shim:receivedCastSessionMessage": {
|
||||
sessionId: string;
|
||||
namespace: string;
|
||||
messageData: string;
|
||||
};
|
||||
"shim:impl_sendCastMessage": {
|
||||
sessionId: string;
|
||||
messageId: string;
|
||||
error?: string;
|
||||
};
|
||||
"bridge:createCastSession": {
|
||||
appId: string;
|
||||
receiverDevice: ReceiverDevice;
|
||||
};
|
||||
"bridge:sendCastReceiverMessage": {
|
||||
sessionId: string;
|
||||
messageData: SenderMessage;
|
||||
messageId: string;
|
||||
};
|
||||
"bridge:sendCastSessionMessage": {
|
||||
sessionId: string;
|
||||
namespace: string;
|
||||
messageData: object | string;
|
||||
messageId: string;
|
||||
};
|
||||
"bridge:stopCastApp": { receiverDevice: ReceiverDevice };
|
||||
|
||||
// Bridge messages
|
||||
, "main:receiverSelector/selected": ReceiverSelectionCast
|
||||
, "main:receiverSelector/stopped": ReceiverSelectionStop
|
||||
, "main:receiverSelector/cancelled": {}
|
||||
, "main:receiverSelector/error": string
|
||||
"main:receiverSelector/selected": ReceiverSelectionCast;
|
||||
"main:receiverSelector/stopped": ReceiverSelectionStop;
|
||||
"main:receiverSelector/cancelled": {};
|
||||
"main:receiverSelector/error": string;
|
||||
|
||||
/**
|
||||
* getInfo uses the old :/ form for compat with old bridge
|
||||
* versions.
|
||||
*/
|
||||
, "bridge:getInfo": string
|
||||
, "bridge:/getInfo": string
|
||||
|
||||
, "bridge:startDiscovery": {
|
||||
shouldWatchStatus: boolean
|
||||
}
|
||||
|
||||
, "bridge:openReceiverSelector": string
|
||||
, "bridge:closeReceiverSelector": {}
|
||||
|
||||
, "bridge:startMediaServer": {
|
||||
filePath: string
|
||||
, port: number
|
||||
}
|
||||
, "bridge:stopMediaServer": {}
|
||||
|
||||
, "mediaCast:mediaServerStarted": {
|
||||
mediaPath: string
|
||||
, subtitlePaths: string[]
|
||||
, localAddress: string
|
||||
}
|
||||
, "mediaCast:mediaServerStopped": {}
|
||||
, "mediaCast:mediaServerError": {}
|
||||
|
||||
|
||||
, "main:receiverDeviceUp": { receiverDevice: ReceiverDevice }
|
||||
, "main:receiverDeviceDown": { receiverDeviceId: string }
|
||||
, "main:receiverDeviceUpdated": {
|
||||
receiverDeviceId: string
|
||||
, status: ReceiverStatus
|
||||
}
|
||||
}
|
||||
|
||||
type MessageDefinitions =
|
||||
ExtMessageDefinitions
|
||||
& AppMessageDefinitions;
|
||||
"bridge:getInfo": string;
|
||||
"bridge:/getInfo": string;
|
||||
"bridge:startDiscovery": {
|
||||
shouldWatchStatus: boolean;
|
||||
};
|
||||
"bridge:openReceiverSelector": string;
|
||||
"bridge:closeReceiverSelector": {};
|
||||
"bridge:startMediaServer": {
|
||||
filePath: string;
|
||||
port: number;
|
||||
};
|
||||
"bridge:stopMediaServer": {};
|
||||
"mediaCast:mediaServerStarted": {
|
||||
mediaPath: string;
|
||||
subtitlePaths: string[];
|
||||
localAddress: string;
|
||||
};
|
||||
"mediaCast:mediaServerStopped": {};
|
||||
"mediaCast:mediaServerError": {};
|
||||
"main:receiverDeviceUp": { receiverDevice: ReceiverDevice };
|
||||
"main:receiverDeviceDown": { receiverDeviceId: string };
|
||||
"main:receiverDeviceUpdated": {
|
||||
receiverDeviceId: string;
|
||||
status: ReceiverStatus;
|
||||
};
|
||||
};
|
||||
|
||||
type MessageDefinitions = ExtMessageDefinitions & AppMessageDefinitions;
|
||||
|
||||
interface MessageBase<K extends keyof MessageDefinitions> {
|
||||
subject: K;
|
||||
@@ -159,7 +142,7 @@ interface MessageBase<K extends keyof MessageDefinitions> {
|
||||
|
||||
type Messages = {
|
||||
[K in keyof MessageDefinitions]: MessageBase<K>;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* For better call semantics, make message data key optional if
|
||||
@@ -172,39 +155,35 @@ type NarrowedMessage<L extends MessageBase<keyof MessageDefinitions>> =
|
||||
: L
|
||||
: never;
|
||||
|
||||
|
||||
export type Port = TypedPort<Message>;
|
||||
export type Message = NarrowedMessage<Messages[keyof Messages]>;
|
||||
|
||||
|
||||
/**
|
||||
* Typed WebExtension-style messaging utility class.
|
||||
*/
|
||||
class Messenger<T> {
|
||||
connect(connectInfo: { name: string; }) {
|
||||
return browser.runtime.connect(connectInfo) as
|
||||
unknown as TypedPort<T>;
|
||||
connect(connectInfo: { name: string }) {
|
||||
return browser.runtime.connect(connectInfo) as unknown as TypedPort<T>;
|
||||
}
|
||||
|
||||
connectTab(tabId: number
|
||||
, connectInfo: { name: string
|
||||
, frameId: number }) {
|
||||
|
||||
return browser.tabs.connect(tabId, connectInfo) as
|
||||
unknown as TypedPort<T>;
|
||||
connectTab(tabId: number, connectInfo: { name: string; frameId: number }) {
|
||||
return browser.tabs.connect(
|
||||
tabId,
|
||||
connectInfo
|
||||
) as unknown as TypedPort<T>;
|
||||
}
|
||||
|
||||
onConnect = {
|
||||
addListener(cb: (port: TypedPort<T>) => void) {
|
||||
browser.runtime.onConnect.addListener(cb as any);
|
||||
}
|
||||
, removeListener(cb: (port: TypedPort<T>) => void) {
|
||||
},
|
||||
removeListener(cb: (port: TypedPort<T>) => void) {
|
||||
browser.runtime.onConnect.removeListener(cb as any);
|
||||
}
|
||||
, hasListener(cb: (port: TypedPort<T>) => void) {
|
||||
},
|
||||
hasListener(cb: (port: TypedPort<T>) => void) {
|
||||
return browser.runtime.onConnect.hasListener(cb as any);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new Messenger<Message>();
|
||||
|
||||
@@ -7,18 +7,20 @@ import cast, { ensureInit } from "../../shim/export";
|
||||
import { Message } from "../../messaging";
|
||||
import { ReceiverDevice } from "../../types";
|
||||
|
||||
|
||||
function startMediaServer(filePath: string, port: number)
|
||||
: Promise<{ mediaPath: string
|
||||
, subtitlePaths: string[]
|
||||
, localAddress: string }> {
|
||||
|
||||
function startMediaServer(
|
||||
filePath: string,
|
||||
port: number
|
||||
): Promise<{
|
||||
mediaPath: string;
|
||||
subtitlePaths: string[];
|
||||
localAddress: string;
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
backgroundPort.postMessage({
|
||||
subject: "bridge:startMediaServer"
|
||||
, data: {
|
||||
filePath: decodeURI(filePath)
|
||||
, port
|
||||
subject: "bridge:startMediaServer",
|
||||
data: {
|
||||
filePath: decodeURI(filePath),
|
||||
port
|
||||
}
|
||||
} as Message);
|
||||
|
||||
@@ -45,7 +47,6 @@ function startMediaServer(filePath: string, port: number)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let backgroundPort: MessagePort;
|
||||
|
||||
let currentSession: cast.Session;
|
||||
@@ -53,7 +54,6 @@ let currentMedia: cast.media.Media;
|
||||
|
||||
let targetElement: HTMLElement;
|
||||
|
||||
|
||||
function getSession(opts: InitOptions): Promise<cast.Session> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
/**
|
||||
@@ -64,10 +64,11 @@ function getSession(opts: InitOptions): Promise<cast.Session> {
|
||||
function receiverListener(availability: string) {
|
||||
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
||||
cast.requestSession(
|
||||
onRequestSessionSuccess
|
||||
, onRequestSessionError
|
||||
, undefined
|
||||
, opts.receiver);
|
||||
onRequestSessionSuccess,
|
||||
onRequestSessionError,
|
||||
undefined,
|
||||
opts.receiver
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,15 +83,15 @@ function getSession(opts: InitOptions): Promise<cast.Session> {
|
||||
reject(err.description);
|
||||
}
|
||||
|
||||
|
||||
const sessionRequest = new cast.SessionRequest(
|
||||
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
|
||||
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID
|
||||
);
|
||||
|
||||
const apiConfig = new cast.ApiConfig(
|
||||
sessionRequest
|
||||
, sessionListener // sessionListener
|
||||
, receiverListener); // receiverListener
|
||||
|
||||
sessionRequest,
|
||||
sessionListener, // sessionListener
|
||||
receiverListener
|
||||
); // receiverListener
|
||||
|
||||
cast.initialize(apiConfig);
|
||||
});
|
||||
@@ -113,19 +114,18 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
|
||||
try {
|
||||
// Wait until media server is listening
|
||||
const { localAddress, mediaPath, subtitlePaths } =
|
||||
await startMediaServer(mediaTitle, port);
|
||||
await startMediaServer(mediaTitle, port);
|
||||
|
||||
const baseUrl = new URL(`http://${localAddress}:${port}/`);
|
||||
mediaUrl = new URL(mediaPath, baseUrl);
|
||||
subtitleUrls = subtitlePaths.map(
|
||||
path => new URL(path, baseUrl));
|
||||
|
||||
path => new URL(path, baseUrl)
|
||||
);
|
||||
} catch (err) {
|
||||
throw logger.error("Failed to start media server");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const activeTrackIds: number[] = [];
|
||||
const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, "");
|
||||
|
||||
@@ -137,7 +137,9 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
|
||||
let trackIndex = 0;
|
||||
for (const subtitleUrl of subtitleUrls) {
|
||||
const castTrack = new cast.media.Track(
|
||||
trackIndex, cast.media.TrackType.TEXT);
|
||||
trackIndex,
|
||||
cast.media.TrackType.TEXT
|
||||
);
|
||||
|
||||
castTrack.name = subtitleUrl.pathname;
|
||||
castTrack.trackContentId = subtitleUrl.href;
|
||||
@@ -168,7 +170,9 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
|
||||
* and type as TrackType.TEXT.
|
||||
*/
|
||||
const castTrack = new cast.media.Track(
|
||||
trackIndex, cast.media.TrackType.TEXT);
|
||||
trackIndex,
|
||||
cast.media.TrackType.TEXT
|
||||
);
|
||||
|
||||
// Copy TextTrack properties
|
||||
castTrack.name = track.label || `track-${trackIndex}`;
|
||||
@@ -179,29 +183,29 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
|
||||
switch (track.kind) {
|
||||
case "subtitles":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.SUBTITLES;
|
||||
cast.media.TextTrackType.SUBTITLES;
|
||||
break;
|
||||
case "captions":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.CAPTIONS;
|
||||
cast.media.TextTrackType.CAPTIONS;
|
||||
break;
|
||||
case "descriptions":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.DESCRIPTIONS;
|
||||
cast.media.TextTrackType.DESCRIPTIONS;
|
||||
break;
|
||||
case "chapters":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.CHAPTERS;
|
||||
cast.media.TextTrackType.CHAPTERS;
|
||||
break;
|
||||
case "metadata":
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.METADATA;
|
||||
cast.media.TextTrackType.METADATA;
|
||||
break;
|
||||
|
||||
// Default to subtitles
|
||||
default:
|
||||
castTrack.subtype =
|
||||
cast.media.TextTrackType.SUBTITLES;
|
||||
cast.media.TextTrackType.SUBTITLES;
|
||||
}
|
||||
|
||||
// Add track to mediaInfo
|
||||
@@ -225,7 +229,6 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let ignoreMediaEvents = false;
|
||||
|
||||
async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
||||
@@ -244,7 +247,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
||||
mediaElement.addEventListener("ratechange", checkIgnore, true);
|
||||
mediaElement.addEventListener("volumechange", checkIgnore, true);
|
||||
|
||||
|
||||
mediaElement.addEventListener("play", () => {
|
||||
currentMedia.play();
|
||||
});
|
||||
@@ -270,15 +272,15 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
||||
|
||||
mediaElement.addEventListener("volumechange", () => {
|
||||
const newVolume = new cast.Volume(
|
||||
currentMedia.volume.level
|
||||
, currentMedia.volume.muted);
|
||||
currentMedia.volume.level,
|
||||
currentMedia.volume.muted
|
||||
);
|
||||
|
||||
const volumeRequest = new cast.media.VolumeRequest(newVolume);
|
||||
|
||||
currentMedia.setVolume(volumeRequest);
|
||||
});
|
||||
|
||||
|
||||
currentMedia.addUpdateListener(isAlive => {
|
||||
if (!isAlive) {
|
||||
return;
|
||||
@@ -303,7 +305,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const localRepeatMode = mediaElement.loop
|
||||
? cast.media.RepeatMode.SINGLE
|
||||
: cast.media.RepeatMode.OFF;
|
||||
@@ -323,7 +324,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (currentMedia.currentTime !== mediaElement.currentTime) {
|
||||
ignoreMediaEvents = true;
|
||||
mediaElement.currentTime = currentMedia.currentTime;
|
||||
@@ -332,7 +332,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface InitOptions {
|
||||
mediaUrl: string;
|
||||
receiver?: ReceiverDevice;
|
||||
@@ -355,7 +354,8 @@ export async function init(opts: InitOptions) {
|
||||
}
|
||||
|
||||
targetElement = browser.menus.getTargetElement(
|
||||
opts.targetElementId) as HTMLMediaElement;
|
||||
opts.targetElementId
|
||||
) as HTMLMediaElement;
|
||||
|
||||
currentSession = await getSession(opts);
|
||||
currentMedia = await getMedia(opts);
|
||||
@@ -384,11 +384,11 @@ export async function init(opts: InitOptions) {
|
||||
* provided on the window object.
|
||||
*/
|
||||
if (window.location.protocol !== "moz-extension:") {
|
||||
const _window = (window as any);
|
||||
const _window = window as any;
|
||||
|
||||
init({
|
||||
mediaUrl: _window.mediaUrl
|
||||
, receiver: _window.receiver
|
||||
, targetElementId: _window.targetElementId
|
||||
mediaUrl: _window.mediaUrl,
|
||||
receiver: _window.receiver,
|
||||
targetElementId: _window.targetElementId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,15 +5,13 @@
|
||||
* descriptor is found, otherwise return undefined.
|
||||
*/
|
||||
export function getPropertyDescriptor(
|
||||
target: any, prop: string | number | symbol)
|
||||
: PropertyDescriptor | undefined {
|
||||
|
||||
target: any,
|
||||
prop: string | number | symbol
|
||||
): PropertyDescriptor | undefined {
|
||||
let desc: PropertyDescriptor | undefined;
|
||||
while (!desc && target !== null) {
|
||||
desc = Object.getOwnPropertyDescriptor(target, prop);
|
||||
if (!desc) {
|
||||
target = Object.getPrototypeOf(target);
|
||||
}
|
||||
if (!desc) target = Object.getPrototypeOf(target);
|
||||
}
|
||||
|
||||
return desc;
|
||||
@@ -24,14 +22,14 @@ export function getPropertyDescriptor(
|
||||
* to a target object.
|
||||
*/
|
||||
export function bindPropertyDescriptor(
|
||||
desc: PropertyDescriptor, target: any)
|
||||
: PropertyDescriptor {
|
||||
|
||||
desc: PropertyDescriptor,
|
||||
target: any
|
||||
): PropertyDescriptor {
|
||||
if (typeof desc.value === "function") {
|
||||
desc.value = desc.value.bind(target);
|
||||
} else {
|
||||
if (desc.get) { desc.get = desc.get.bind(target); }
|
||||
if (desc.set) { desc.set = desc.set.bind(target); }
|
||||
if (desc.get) desc.get = desc.get.bind(target);
|
||||
if (desc.set) desc.set = desc.set.bind(target);
|
||||
}
|
||||
|
||||
return desc;
|
||||
@@ -43,9 +41,9 @@ export function bindPropertyDescriptor(
|
||||
* element and collect them into a property descriptor map.
|
||||
*/
|
||||
export function clonePropsDescriptor<T>(
|
||||
target: T, props: any[])
|
||||
: PropertyDescriptorMap {
|
||||
|
||||
target: T,
|
||||
props: any[]
|
||||
): PropertyDescriptorMap {
|
||||
return props.reduce<PropertyDescriptorMap>((descriptorMap, prop) => {
|
||||
const desc = getPropertyDescriptor(target, prop);
|
||||
if (desc) {
|
||||
@@ -59,17 +57,19 @@ export function clonePropsDescriptor<T>(
|
||||
|
||||
export function makeGetterDescriptor(val: any): PropertyDescriptor {
|
||||
return {
|
||||
enumerable: true
|
||||
, configurable: true
|
||||
, get() { return val; }
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get() {
|
||||
return val;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function makeValueDescriptor(val: any): PropertyDescriptor {
|
||||
return {
|
||||
enumerable: true
|
||||
, configurable: true
|
||||
, writable: true
|
||||
, value: val
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: val
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
|
||||
import logger from "../../../lib/logger";
|
||||
|
||||
import { bindPropertyDescriptor
|
||||
, clonePropsDescriptor
|
||||
, getPropertyDescriptor
|
||||
, makeGetterDescriptor
|
||||
, makeValueDescriptor } from "./descriptorUtils";
|
||||
import {
|
||||
bindPropertyDescriptor,
|
||||
clonePropsDescriptor,
|
||||
getPropertyDescriptor,
|
||||
makeGetterDescriptor,
|
||||
makeValueDescriptor
|
||||
} from "./descriptorUtils";
|
||||
|
||||
// Injected by content loader
|
||||
declare const iconAirPlayAudio: string;
|
||||
declare const iconAirPlayVideo: string;
|
||||
declare const mediaOverlayTitle: string;
|
||||
|
||||
|
||||
/**
|
||||
* Intercept and store references to shadow root nodes created by
|
||||
* calls to `attachShadow`. Used to reference shadow roots, even when
|
||||
@@ -27,7 +28,6 @@ Element.prototype.attachShadow = function (init) {
|
||||
return shadowRoot;
|
||||
};
|
||||
|
||||
|
||||
function getShadowRootFromNode(node: Node): ShadowRoot | undefined {
|
||||
// Don't touch our custom element
|
||||
if (node instanceof PlayerElement) {
|
||||
@@ -37,7 +37,6 @@ function getShadowRootFromNode(node: Node): ShadowRoot | undefined {
|
||||
return internalShadowRoots.get(node as Element);
|
||||
}
|
||||
|
||||
|
||||
const DQS_XPATH_EXPRESSION = `//*[contains(name(), "-")]`;
|
||||
|
||||
/**
|
||||
@@ -46,12 +45,15 @@ const DQS_XPATH_EXPRESSION = `//*[contains(name(), "-")]`;
|
||||
*/
|
||||
function deepQuerySelector(selector: string): Element | null {
|
||||
const result = document.evaluate(
|
||||
DQS_XPATH_EXPRESSION, document, null
|
||||
, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
|
||||
DQS_XPATH_EXPRESSION,
|
||||
document,
|
||||
null,
|
||||
XPathResult.ORDERED_NODE_ITERATOR_TYPE
|
||||
);
|
||||
|
||||
let node: Node | null;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (node = result.iterateNext()) {
|
||||
while ((node = result.iterateNext())) {
|
||||
const shadowRoot = getShadowRootFromNode(node);
|
||||
if (!shadowRoot) {
|
||||
continue;
|
||||
@@ -72,14 +74,17 @@ function deepQuerySelector(selector: string): Element | null {
|
||||
*/
|
||||
function deepQuerySelectorAll(selector: string): Node[] {
|
||||
const result = document.evaluate(
|
||||
DQS_XPATH_EXPRESSION, document, null
|
||||
, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
|
||||
DQS_XPATH_EXPRESSION,
|
||||
document,
|
||||
null,
|
||||
XPathResult.ORDERED_NODE_ITERATOR_TYPE
|
||||
);
|
||||
|
||||
const nodes: Node[] = [];
|
||||
|
||||
let node: Node | null;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (node = result.iterateNext()) {
|
||||
while ((node = result.iterateNext())) {
|
||||
const shadowRoot = getShadowRootFromNode(node);
|
||||
if (shadowRoot) {
|
||||
nodes.push(...shadowRoot.querySelectorAll(selector));
|
||||
@@ -89,26 +94,45 @@ function deepQuerySelectorAll(selector: string): Node[] {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
|
||||
const mediaElementTypes = [
|
||||
HTMLMediaElement
|
||||
, HTMLVideoElement
|
||||
, HTMLAudioElement
|
||||
HTMLMediaElement,
|
||||
HTMLVideoElement,
|
||||
HTMLAudioElement
|
||||
];
|
||||
|
||||
const mediaElementEvents = [
|
||||
"abort", "canplay", "canplaythrough", "durationchange", "emptied"
|
||||
, "encrypted", "ended", "error", "interruptbegin", "interruptend"
|
||||
, "loadeddata", "loadedmetadata", "loadstart", "mozaudioavailable", "pause"
|
||||
, "play", "playing", "progress", "ratechange", "seeked", "seeking", "stalled"
|
||||
, "suspend", "timeupdate", "volumechange", "waiting"
|
||||
"abort",
|
||||
"canplay",
|
||||
"canplaythrough",
|
||||
"durationchange",
|
||||
"emptied",
|
||||
"encrypted",
|
||||
"ended",
|
||||
"error",
|
||||
"interruptbegin",
|
||||
"interruptend",
|
||||
"loadeddata",
|
||||
"loadedmetadata",
|
||||
"loadstart",
|
||||
"mozaudioavailable",
|
||||
"pause",
|
||||
"play",
|
||||
"playing",
|
||||
"progress",
|
||||
"ratechange",
|
||||
"seeked",
|
||||
"seeking",
|
||||
"stalled",
|
||||
"suspend",
|
||||
"timeupdate",
|
||||
"volumechange",
|
||||
"waiting"
|
||||
];
|
||||
|
||||
const mediaElementAttributes = mediaElementTypes
|
||||
.flatMap(type => Object.getOwnPropertyNames(type.prototype))
|
||||
.concat(mediaElementEvents.map(ev => `on${ev}`));
|
||||
|
||||
|
||||
/**
|
||||
* Opaque wrapper around the media element to provide an overlay without
|
||||
* author interference. Relevant properties, attributes, events and
|
||||
@@ -125,8 +149,14 @@ class PlayerElement extends HTMLElement {
|
||||
switch (this.constructor) {
|
||||
// URL variables injected ahead of current script
|
||||
|
||||
case AudioPlayerElement: { iconUrl = iconAirPlayAudio; break; }
|
||||
case VideoPlayerElement: { iconUrl = iconAirPlayVideo; break; }
|
||||
case AudioPlayerElement: {
|
||||
iconUrl = iconAirPlayAudio;
|
||||
break;
|
||||
}
|
||||
case VideoPlayerElement: {
|
||||
iconUrl = iconAirPlayVideo;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
shadowRoot.innerHTML = `
|
||||
@@ -192,10 +222,19 @@ class PlayerElement extends HTMLElement {
|
||||
* listeners, etc... on the media element, but since it's hidden
|
||||
* within the shadow DOM, these properties must be proxied.
|
||||
*/
|
||||
Object.defineProperties(host, clonePropsDescriptor(videoElement, [
|
||||
"attributes", "setAttribute", "removeAttribute", "setAttribute"
|
||||
, "addEventListener", "removeEventListener", "hasEventListener"
|
||||
, ...mediaElementAttributes as any ]));
|
||||
Object.defineProperties(
|
||||
host,
|
||||
clonePropsDescriptor(videoElement, [
|
||||
"attributes",
|
||||
"setAttribute",
|
||||
"removeAttribute",
|
||||
"setAttribute",
|
||||
"addEventListener",
|
||||
"removeEventListener",
|
||||
"hasEventListener",
|
||||
...(mediaElementAttributes as any)
|
||||
])
|
||||
);
|
||||
|
||||
shadowRoot.prepend(videoElement);
|
||||
}
|
||||
@@ -217,13 +256,14 @@ try {
|
||||
customElements.define("audio-player-element", AudioPlayerElement);
|
||||
customElements.define("video-player-element", VideoPlayerElement);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException
|
||||
&& err.code === DOMException.NOT_SUPPORTED_ERR) {
|
||||
if (
|
||||
err instanceof DOMException &&
|
||||
err.code === DOMException.NOT_SUPPORTED_ERR
|
||||
) {
|
||||
// Script already injected
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Original functions
|
||||
const _createElement = document.createElement;
|
||||
const _createElementNS = document.createElementNS;
|
||||
@@ -233,23 +273,22 @@ const _createElementNS = document.createElementNS;
|
||||
* custom element version that imitates the original. Otherwise, returns
|
||||
* the result of the original.
|
||||
*/
|
||||
function createElement(
|
||||
tagName: string
|
||||
, options?: ElementCreationOptions) {
|
||||
|
||||
function createElement(tagName: string, options?: ElementCreationOptions) {
|
||||
// Normalize formatting
|
||||
const lowerTagName = tagName.toLowerCase();
|
||||
const upperTagName = tagName.toUpperCase();
|
||||
|
||||
if (lowerTagName === "audio" || lowerTagName === "video") {
|
||||
const fakeElement = _createElement.call(document
|
||||
, `${lowerTagName}-player-element`) as HTMLMediaElement;
|
||||
const fakeElement = _createElement.call(
|
||||
document,
|
||||
`${lowerTagName}-player-element`
|
||||
) as HTMLMediaElement;
|
||||
|
||||
// Ensure all references to the element name match tagName
|
||||
Object.defineProperties(fakeElement, {
|
||||
tagName: makeGetterDescriptor(upperTagName)
|
||||
, nodeName: makeGetterDescriptor(upperTagName)
|
||||
, localName: makeGetterDescriptor(lowerTagName)
|
||||
tagName: makeGetterDescriptor(upperTagName),
|
||||
nodeName: makeGetterDescriptor(upperTagName),
|
||||
localName: makeGetterDescriptor(lowerTagName)
|
||||
});
|
||||
|
||||
return fakeElement;
|
||||
@@ -264,34 +303,41 @@ function createElement(
|
||||
* original.
|
||||
*/
|
||||
function createElementNS(
|
||||
namespaceURI: string
|
||||
, qualifiedName: string
|
||||
, options?: ElementCreationOptions) {
|
||||
|
||||
namespaceURI: string,
|
||||
qualifiedName: string,
|
||||
options?: ElementCreationOptions
|
||||
) {
|
||||
if (namespaceURI === document.namespaceURI) {
|
||||
return createElement(qualifiedName, options);
|
||||
}
|
||||
|
||||
return _createElementNS.call(document
|
||||
, namespaceURI, qualifiedName, options);
|
||||
return _createElementNS.call(
|
||||
document,
|
||||
namespaceURI,
|
||||
qualifiedName,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to hide function source from page scripts by returning the
|
||||
* toString/toSource values of the native function.
|
||||
*/
|
||||
Object.defineProperties(createElement, clonePropsDescriptor(
|
||||
_createElement, [ "toString", "toSource" ]));
|
||||
Object.defineProperties(createElementNS, clonePropsDescriptor(
|
||||
_createElementNS, [ "toString", "toSource" ]));
|
||||
Object.defineProperties(
|
||||
createElement,
|
||||
clonePropsDescriptor(_createElement, ["toString", "toSource"])
|
||||
);
|
||||
Object.defineProperties(
|
||||
createElementNS,
|
||||
clonePropsDescriptor(_createElementNS, ["toString", "toSource"])
|
||||
);
|
||||
|
||||
// Re-define element creation functions
|
||||
Object.defineProperties(document, {
|
||||
createElement: makeValueDescriptor(createElement)
|
||||
, createElementNS: makeValueDescriptor(createElementNS)
|
||||
createElement: makeValueDescriptor(createElement),
|
||||
createElementNS: makeValueDescriptor(createElementNS)
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Takes a media element, creates a `PlayerElement` via the patched
|
||||
* `createElement` function, fetches the shadow root and copies any
|
||||
@@ -321,7 +367,10 @@ function wrapMediaElement(mediaElement: HTMLMediaElement) {
|
||||
* internal media element instead.
|
||||
*/
|
||||
HTMLElement.prototype.setAttribute.call(
|
||||
wrappedMedia, attr.name, attr.value);
|
||||
wrappedMedia,
|
||||
attr.name,
|
||||
attr.value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +411,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const mediaElements = document.querySelectorAll(mediaSelector);
|
||||
const deepMediaElements = deepQuerySelectorAll(mediaSelector);
|
||||
|
||||
for (const mediaElement of [ ...mediaElements, ...deepMediaElements ]) {
|
||||
for (const mediaElement of [...mediaElements, ...deepMediaElements]) {
|
||||
wrapMediaElement(mediaElement as HTMLMediaElement);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,17 +7,22 @@ const _ = browser.i18n.getMessage;
|
||||
* scripts from loading before its execution.
|
||||
*/
|
||||
const req = new XMLHttpRequest();
|
||||
req.open("GET", browser.runtime.getURL(
|
||||
"senders/media/overlay/overlayContent.js"), false);
|
||||
req.open(
|
||||
"GET",
|
||||
browser.runtime.getURL("senders/media/overlay/overlayContent.js"),
|
||||
false
|
||||
);
|
||||
|
||||
req.send();
|
||||
|
||||
if (req.status === 200) {
|
||||
// TODO: Replace with cast icons until AirPlay support is ready
|
||||
const iconAirPlayAudio = browser.runtime.getURL(
|
||||
"senders/media/overlay/AirPlay_Audio.svg");
|
||||
"senders/media/overlay/AirPlay_Audio.svg"
|
||||
);
|
||||
const iconAirPlayVideo = browser.runtime.getURL(
|
||||
"senders/media/overlay/AirPlay_Audio.svg");
|
||||
"senders/media/overlay/AirPlay_Audio.svg"
|
||||
);
|
||||
|
||||
const scriptElement = document.createElement("script");
|
||||
scriptElement.textContent = `(function(){
|
||||
|
||||
@@ -6,23 +6,22 @@ import cast, { ensureInit } from "../shim/export";
|
||||
import { ReceiverSelectorMediaType } from "../background/receiverSelector";
|
||||
import { ReceiverDevice } from "../types";
|
||||
|
||||
|
||||
// Variables passed from background
|
||||
const { selectedMedia
|
||||
, selectedReceiver }
|
||||
: { selectedMedia: ReceiverSelectorMediaType
|
||||
, selectedReceiver: ReceiverDevice } = (window as any);
|
||||
|
||||
const {
|
||||
selectedMedia,
|
||||
selectedReceiver
|
||||
}: {
|
||||
selectedMedia: ReceiverSelectorMediaType;
|
||||
selectedReceiver: ReceiverDevice;
|
||||
} = window as any;
|
||||
|
||||
const FX_CAST_RECEIVER_APP_NAMESPACE = "urn:x-cast:fx_cast";
|
||||
|
||||
|
||||
let session: cast.Session;
|
||||
let wasSessionRequested = false;
|
||||
|
||||
let peerConnection: RTCPeerConnection;
|
||||
|
||||
|
||||
/**
|
||||
* Sends a message to the fx_cast app running on the
|
||||
* receiver device.
|
||||
@@ -33,40 +32,39 @@ function sendAppMessage(subject: string, data: any) {
|
||||
}
|
||||
|
||||
session.sendMessage(FX_CAST_RECEIVER_APP_NAMESPACE, {
|
||||
subject
|
||||
, data
|
||||
subject,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
sendAppMessage("close", null);
|
||||
});
|
||||
|
||||
|
||||
async function onRequestSessionSuccess(newSession: cast.Session) {
|
||||
cast.logMessage("onRequestSessionSuccess");
|
||||
|
||||
session = newSession;
|
||||
session.addMessageListener(FX_CAST_RECEIVER_APP_NAMESPACE
|
||||
, async (_namespace, message) => {
|
||||
session.addMessageListener(
|
||||
FX_CAST_RECEIVER_APP_NAMESPACE,
|
||||
async (_namespace, message) => {
|
||||
const { subject, data } = JSON.parse(message);
|
||||
|
||||
const { subject, data } = JSON.parse(message);
|
||||
|
||||
switch (subject) {
|
||||
case "peerConnectionAnswer": {
|
||||
peerConnection.setRemoteDescription(data);
|
||||
break;
|
||||
}
|
||||
case "iceCandidate": {
|
||||
peerConnection.addIceCandidate(data);
|
||||
break;
|
||||
switch (subject) {
|
||||
case "peerConnectionAnswer": {
|
||||
peerConnection.setRemoteDescription(data);
|
||||
break;
|
||||
}
|
||||
case "iceCandidate": {
|
||||
peerConnection.addIceCandidate(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
peerConnection = new RTCPeerConnection();
|
||||
peerConnection.addEventListener("icecandidate", (ev) => {
|
||||
peerConnection.addEventListener("icecandidate", ev => {
|
||||
sendAppMessage("iceCandidate", ev.candidate);
|
||||
});
|
||||
|
||||
@@ -95,27 +93,29 @@ async function onRequestSessionSuccess(newSession: cast.Session) {
|
||||
|
||||
// TODO: Test performance
|
||||
const drawFlags =
|
||||
ctx.DRAWWINDOW_DRAW_CARET
|
||||
| ctx.DRAWWINDOW_DRAW_VIEW
|
||||
| ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES
|
||||
| ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
|
||||
ctx.DRAWWINDOW_DRAW_CARET |
|
||||
ctx.DRAWWINDOW_DRAW_VIEW |
|
||||
ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
|
||||
ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
|
||||
|
||||
let lastFrame: DOMHighResTimeStamp;
|
||||
window.requestAnimationFrame(
|
||||
function draw(now: DOMHighResTimeStamp) {
|
||||
|
||||
window.requestAnimationFrame(function draw(
|
||||
now: DOMHighResTimeStamp
|
||||
) {
|
||||
if (!lastFrame) {
|
||||
lastFrame = now;
|
||||
}
|
||||
|
||||
if ((now - lastFrame) > (1000 / 30)) {
|
||||
if (now - lastFrame > 1000 / 30) {
|
||||
ctx.drawWindow(
|
||||
window // window
|
||||
, 0, 0 // x, y
|
||||
, canvas.width // w
|
||||
, canvas.height // h
|
||||
, "white" // bgColor
|
||||
, drawFlags); // flags
|
||||
window, // window
|
||||
0,
|
||||
0, // x, y
|
||||
canvas.width, // w
|
||||
canvas.height, // h
|
||||
"white", // bgColor
|
||||
drawFlags
|
||||
); // flags
|
||||
|
||||
lastFrame = now;
|
||||
}
|
||||
@@ -134,8 +134,8 @@ async function onRequestSessionSuccess(newSession: cast.Session) {
|
||||
|
||||
case ReceiverSelectorMediaType.Screen: {
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: { cursor: "motion" }
|
||||
, audio: false
|
||||
video: { cursor: "motion" },
|
||||
audio: false
|
||||
});
|
||||
|
||||
peerConnection.addStream(stream);
|
||||
@@ -152,7 +152,6 @@ async function onRequestSessionSuccess(newSession: cast.Session) {
|
||||
sendAppMessage("peerConnectionOffer", offer);
|
||||
}
|
||||
|
||||
|
||||
function receiverListener(availability: string) {
|
||||
cast.logMessage("receiverListener");
|
||||
|
||||
@@ -162,14 +161,15 @@ function receiverListener(availability: string) {
|
||||
|
||||
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
||||
wasSessionRequested = true;
|
||||
cast.requestSession(onRequestSessionSuccess
|
||||
, onRequestSessionError
|
||||
, undefined
|
||||
, selectedReceiver);
|
||||
cast.requestSession(
|
||||
onRequestSessionSuccess,
|
||||
onRequestSessionError,
|
||||
undefined,
|
||||
selectedReceiver
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onRequestSessionError() {
|
||||
cast.logMessage("onRequestSessionError");
|
||||
}
|
||||
@@ -183,18 +183,17 @@ function onInitializeError() {
|
||||
cast.logMessage("onInitializeError");
|
||||
}
|
||||
|
||||
|
||||
ensureInit().then(async () => {
|
||||
const mirroringAppId = await options.get("mirroringAppId");
|
||||
const sessionRequest = new cast.SessionRequest(mirroringAppId);
|
||||
|
||||
const apiConfig = new cast.ApiConfig(
|
||||
sessionRequest
|
||||
, sessionListener
|
||||
, receiverListener
|
||||
, undefined, undefined);
|
||||
sessionRequest,
|
||||
sessionListener,
|
||||
receiverListener,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
cast.initialize(apiConfig
|
||||
, onInitializeSuccess
|
||||
, onInitializeError);
|
||||
cast.initialize(apiConfig, onInitializeSuccess, onInitializeError);
|
||||
});
|
||||
|
||||
@@ -6,32 +6,34 @@ import logger from "../../lib/logger";
|
||||
|
||||
import { sendMessageResponse } from "../eventMessageChannel";
|
||||
|
||||
import { ErrorCallback
|
||||
, LoadSuccessCallback
|
||||
, MediaListener
|
||||
, MessageListener
|
||||
, SuccessCallback
|
||||
, UpdateListener } from "../types";
|
||||
import {
|
||||
ErrorCallback,
|
||||
LoadSuccessCallback,
|
||||
MediaListener,
|
||||
MessageListener,
|
||||
SuccessCallback,
|
||||
UpdateListener
|
||||
} from "../types";
|
||||
|
||||
import { MediaStatus
|
||||
, ReceiverMediaMessage
|
||||
, SenderMediaMessage
|
||||
, SenderMessage } from "./types";
|
||||
import {
|
||||
MediaStatus,
|
||||
ReceiverMediaMessage,
|
||||
SenderMediaMessage,
|
||||
SenderMessage
|
||||
} from "./types";
|
||||
|
||||
import { Image, Receiver, SenderApplication } from "./dataClasses";
|
||||
import { SessionStatus } from "./enums";
|
||||
import { Media, LoadRequest, QueueLoadRequest, QueueItem } from "./media";
|
||||
|
||||
|
||||
const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
||||
|
||||
|
||||
/**
|
||||
* Takes a media object and a media status object and merges
|
||||
* the status with the existing media object, updating it with
|
||||
* new properties.
|
||||
*/
|
||||
function updateMedia(media: Media, status: MediaStatus) {
|
||||
function updateMedia(media: Media, status: MediaStatus) {
|
||||
if (status.currentTime) {
|
||||
media._lastUpdateTime = Date.now();
|
||||
}
|
||||
@@ -51,7 +53,8 @@ const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
||||
if (!newItem.media) {
|
||||
// Existing queue item with the same ID
|
||||
const existingItem = media.items?.find(
|
||||
item => item.itemId === newItem.itemId);
|
||||
item => item.itemId === newItem.itemId
|
||||
);
|
||||
|
||||
/**
|
||||
* Use existing queue item's media info if available
|
||||
@@ -60,8 +63,10 @@ const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
||||
*/
|
||||
if (existingItem?.media) {
|
||||
newItem.media = existingItem.media;
|
||||
} else if (media.media
|
||||
&& newItem.itemId === media.currentItemId) {
|
||||
} else if (
|
||||
media.media &&
|
||||
newItem.itemId === media.currentItemId
|
||||
) {
|
||||
newItem.media = media.media;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +78,6 @@ const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class Session {
|
||||
#id = uuid();
|
||||
|
||||
@@ -86,14 +90,15 @@ export default class Session {
|
||||
_messageListeners = new Map<string, Set<MessageListener>>();
|
||||
_updateListeners = new Set<UpdateListener>();
|
||||
|
||||
|
||||
_sendMessageCallbacks =
|
||||
new Map<string, [ SuccessCallback?, ErrorCallback? ]>();
|
||||
_sendMessageCallbacks = new Map<
|
||||
string,
|
||||
[SuccessCallback?, ErrorCallback?]
|
||||
>();
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
#mediaMessageListener = (namespace: string, messageString: string) => {
|
||||
#mediaMessageListener = (namespace: string, messageString: string) => {
|
||||
if (namespace !== NS_MEDIA) return;
|
||||
|
||||
const message: ReceiverMediaMessage = JSON.parse(messageString);
|
||||
@@ -102,17 +107,19 @@ export default class Session {
|
||||
// Update media
|
||||
for (const mediaStatus of message.status) {
|
||||
let media = this.media.find(
|
||||
media => media.mediaSessionId ===
|
||||
mediaStatus.mediaSessionId);
|
||||
media =>
|
||||
media.mediaSessionId === mediaStatus.mediaSessionId
|
||||
);
|
||||
|
||||
console.info(media);
|
||||
|
||||
// Handle Media creation
|
||||
if (!media) {
|
||||
media = new Media(
|
||||
this.sessionId
|
||||
, mediaStatus.mediaSessionId
|
||||
, this.#sendMediaMessage);
|
||||
this.sessionId,
|
||||
mediaStatus.mediaSessionId,
|
||||
this.#sendMediaMessage
|
||||
);
|
||||
|
||||
this.media.push(media);
|
||||
this.#loadMediaSuccessCallback?.(media);
|
||||
@@ -128,44 +135,43 @@ export default class Session {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a media message to the app receiver.
|
||||
* urn:x-cast:com.google.cast.media
|
||||
*/
|
||||
#sendMediaMessage = (message: DistributiveOmit<
|
||||
SenderMediaMessage, "requestId">) => {
|
||||
|
||||
#sendMediaMessage = (
|
||||
message: DistributiveOmit<SenderMediaMessage, "requestId">
|
||||
) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.sendMessage(
|
||||
"urn:x-cast:com.google.cast.media"
|
||||
, { ...message, requestId: 0 }
|
||||
, resolve, reject);
|
||||
|
||||
"urn:x-cast:com.google.cast.media",
|
||||
{ ...message, requestId: 0 },
|
||||
resolve,
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#sendReceiverMessage = (message: DistributiveOmit<
|
||||
SenderMessage, "requestId">) => {
|
||||
};
|
||||
|
||||
#sendReceiverMessage = (
|
||||
message: DistributiveOmit<SenderMessage, "requestId">
|
||||
) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const messageId = uuid();
|
||||
|
||||
sendMessageResponse({
|
||||
subject: "bridge:sendCastReceiverMessage"
|
||||
, data: {
|
||||
sessionId: this.sessionId
|
||||
, messageData: message as SenderMessage
|
||||
, messageId
|
||||
subject: "bridge:sendCastReceiverMessage",
|
||||
data: {
|
||||
sessionId: this.sessionId,
|
||||
messageData: message as SenderMessage,
|
||||
messageId
|
||||
}
|
||||
});
|
||||
|
||||
this._sendMessageCallbacks.set(
|
||||
messageId, [ resolve, reject ]);
|
||||
this._sendMessageCallbacks.set(messageId, [resolve, reject]);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
media: Media[] = [];
|
||||
namespaces: Array<{ name: string }> = [];
|
||||
@@ -174,18 +180,18 @@ export default class Session {
|
||||
statusText: Nullable<string> = null;
|
||||
transportId: string;
|
||||
|
||||
constructor(public sessionId: string
|
||||
, public appId: string
|
||||
, public displayName: string
|
||||
, public appImages: Image[]
|
||||
, public receiver: Receiver) {
|
||||
|
||||
constructor(
|
||||
public sessionId: string,
|
||||
public appId: string,
|
||||
public displayName: string,
|
||||
public appImages: Image[],
|
||||
public receiver: Receiver
|
||||
) {
|
||||
this.transportId = sessionId || "";
|
||||
|
||||
this.addMessageListener(NS_MEDIA, this.#mediaMessageListener);
|
||||
}
|
||||
|
||||
|
||||
addMediaListener(_mediaListener: MediaListener) {
|
||||
logger.info("STUB :: Session#addMediaListener");
|
||||
}
|
||||
@@ -211,83 +217,80 @@ export default class Session {
|
||||
this._updateListeners.delete(listener);
|
||||
}
|
||||
|
||||
leave(_successCallback?: SuccessCallback
|
||||
, _errorCallback?: ErrorCallback) {
|
||||
|
||||
leave(_successCallback?: SuccessCallback, _errorCallback?: ErrorCallback) {
|
||||
logger.info("STUB :: Session#leave");
|
||||
}
|
||||
|
||||
loadMedia(loadRequest: LoadRequest
|
||||
, successCallback?: LoadSuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
loadMedia(
|
||||
loadRequest: LoadRequest,
|
||||
successCallback?: LoadSuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this.#loadMediaSuccessCallback = successCallback;
|
||||
this.#loadMediaErrorCallback = errorCallback;
|
||||
this.#loadMediaRequest = loadRequest;
|
||||
|
||||
loadRequest.sessionId = this.sessionId;
|
||||
this.#sendMediaMessage(loadRequest)
|
||||
.catch(errorCallback);
|
||||
this.#sendMediaMessage(loadRequest).catch(errorCallback);
|
||||
}
|
||||
|
||||
queueLoad(_queueLoadRequest: QueueLoadRequest
|
||||
, _successCallback?: LoadSuccessCallback
|
||||
, _errorCallback?: ErrorCallback) {
|
||||
|
||||
queueLoad(
|
||||
_queueLoadRequest: QueueLoadRequest,
|
||||
_successCallback?: LoadSuccessCallback,
|
||||
_errorCallback?: ErrorCallback
|
||||
) {
|
||||
logger.info("STUB :: Session#queueLoad");
|
||||
}
|
||||
|
||||
sendMessage(namespace: string
|
||||
, message: object | string
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
sendMessage(
|
||||
namespace: string,
|
||||
message: object | string,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
const messageId = uuid();
|
||||
|
||||
sendMessageResponse({
|
||||
subject: "bridge:sendCastSessionMessage"
|
||||
, data: {
|
||||
sessionId: this.sessionId
|
||||
, namespace
|
||||
, messageData: message
|
||||
, messageId
|
||||
subject: "bridge:sendCastSessionMessage",
|
||||
data: {
|
||||
sessionId: this.sessionId,
|
||||
namespace,
|
||||
messageData: message,
|
||||
messageId
|
||||
}
|
||||
});
|
||||
|
||||
this._sendMessageCallbacks.set(messageId, [
|
||||
successCallback
|
||||
, errorCallback
|
||||
successCallback,
|
||||
errorCallback
|
||||
]);
|
||||
}
|
||||
|
||||
setReceiverMuted(muted: boolean
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this.#sendReceiverMessage(
|
||||
{ type: "SET_VOLUME"
|
||||
, volume: { muted }})
|
||||
setReceiverMuted(
|
||||
muted: boolean,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this.#sendReceiverMessage({ type: "SET_VOLUME", volume: { muted } })
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
setReceiverVolumeLevel(newLevel: number
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this.#sendReceiverMessage(
|
||||
{ type: "SET_VOLUME"
|
||||
, volume: { level: newLevel }})
|
||||
setReceiverVolumeLevel(
|
||||
newLevel: number,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this.#sendReceiverMessage({
|
||||
type: "SET_VOLUME",
|
||||
volume: { level: newLevel }
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
stop(successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this.#sendReceiverMessage(
|
||||
{ type: "STOP"
|
||||
, sessionId: this.sessionId })
|
||||
stop(successCallback?: SuccessCallback, errorCallback?: ErrorCallback) {
|
||||
this.#sendReceiverMessage({ type: "STOP", sessionId: this.sessionId })
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
@@ -2,48 +2,44 @@
|
||||
|
||||
import Session from "./Session";
|
||||
|
||||
import { AutoJoinPolicy
|
||||
, Capability
|
||||
, DefaultActionPolicy
|
||||
, ReceiverType
|
||||
, VolumeControlType } from "./enums";
|
||||
|
||||
import {
|
||||
AutoJoinPolicy,
|
||||
Capability,
|
||||
DefaultActionPolicy,
|
||||
ReceiverType,
|
||||
VolumeControlType
|
||||
} from "./enums";
|
||||
|
||||
export class ApiConfig {
|
||||
constructor(
|
||||
public sessionRequest: SessionRequest
|
||||
, public sessionListener: (session: Session) => void
|
||||
, public receiverListener: (availability: string) => void
|
||||
public sessionRequest: SessionRequest,
|
||||
public sessionListener: (session: Session) => void,
|
||||
public receiverListener: (availability: string) => void,
|
||||
|
||||
, public autoJoinPolicy: string
|
||||
= AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED
|
||||
, public defaultActionPolicy: string
|
||||
= DefaultActionPolicy.CREATE_SESSION) {}
|
||||
public autoJoinPolicy: string = AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED,
|
||||
public defaultActionPolicy: string = DefaultActionPolicy.CREATE_SESSION
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
export class CredentialsData {
|
||||
constructor(
|
||||
public credentials: string
|
||||
, public credentialsData: string) {}
|
||||
constructor(public credentials: string, public credentialsData: string) {}
|
||||
}
|
||||
|
||||
|
||||
export class DialRequest {
|
||||
constructor(
|
||||
public appName: string
|
||||
, public launchParameter: Nullable<string> = null) {}
|
||||
public appName: string,
|
||||
public launchParameter: Nullable<string> = null
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
export class Error {
|
||||
constructor(
|
||||
public code: string
|
||||
, public description: Nullable<string> = null
|
||||
, public details: any = null) {}
|
||||
public code: string,
|
||||
public description: Nullable<string> = null,
|
||||
public details: any = null
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
export class Image {
|
||||
width: Nullable<number> = null;
|
||||
height: Nullable<number> = null;
|
||||
@@ -51,29 +47,25 @@ export class Image {
|
||||
constructor(public url: string) {}
|
||||
}
|
||||
|
||||
|
||||
export class Receiver {
|
||||
displayStatus: Nullable<ReceiverDisplayStatus> = null;
|
||||
isActiveInput: Nullable<boolean> = null;
|
||||
receiverType = ReceiverType.CAST;
|
||||
|
||||
constructor(
|
||||
public label: string
|
||||
, public friendlyName: string
|
||||
, public capabilities: Capability[] = []
|
||||
, public volume: Nullable<Volume> = null) {}
|
||||
public label: string,
|
||||
public friendlyName: string,
|
||||
public capabilities: Capability[] = [],
|
||||
public volume: Nullable<Volume> = null
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
export class ReceiverDisplayStatus {
|
||||
showStop: Nullable<boolean> = null;
|
||||
|
||||
constructor(
|
||||
public statusText: string
|
||||
, public appImages: Image[]) {}
|
||||
constructor(public statusText: string, public appImages: Image[]) {}
|
||||
}
|
||||
|
||||
|
||||
export class SenderApplication {
|
||||
packageId: Nullable<string> = null;
|
||||
url: Nullable<string> = null;
|
||||
@@ -81,20 +73,18 @@ export class SenderApplication {
|
||||
constructor(public platform: string) {}
|
||||
}
|
||||
|
||||
|
||||
export class SessionRequest {
|
||||
language: Nullable<string> = null;
|
||||
|
||||
constructor(
|
||||
public appId: string
|
||||
, public capabilities = [ Capability.VIDEO_OUT
|
||||
, Capability.AUDIO_OUT ]
|
||||
, public requestSessionTimeout = (new Timeout()).requestSession
|
||||
, public androidReceiverCompatible = false
|
||||
, public credentialsData: Nullable<CredentialsData> = null) {}
|
||||
public appId: string,
|
||||
public capabilities = [Capability.VIDEO_OUT, Capability.AUDIO_OUT],
|
||||
public requestSessionTimeout = new Timeout().requestSession,
|
||||
public androidReceiverCompatible = false,
|
||||
public credentialsData: Nullable<CredentialsData> = null
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
export class Timeout {
|
||||
leaveSession = 3000;
|
||||
requestSession = 60000;
|
||||
@@ -103,12 +93,12 @@ export class Timeout {
|
||||
stopSession = 3000;
|
||||
}
|
||||
|
||||
|
||||
export class Volume {
|
||||
controlType?: VolumeControlType;
|
||||
stepInterval?: number;
|
||||
|
||||
constructor(
|
||||
public level: Nullable<number> = null
|
||||
, public muted: Nullable<boolean> = null) {}
|
||||
public level: Nullable<number> = null,
|
||||
public muted: Nullable<boolean> = null
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
"use strict";
|
||||
|
||||
export enum AutoJoinPolicy {
|
||||
TAB_AND_ORIGIN_SCOPED = "tab_and_origin_scoped"
|
||||
, ORIGIN_SCOPED = "origin_scoped"
|
||||
, PAGE_SCOPED = "page_scoped"
|
||||
, CUSTOM_CONTROLLER_SCOPED = "custom_controller_scoped"
|
||||
TAB_AND_ORIGIN_SCOPED = "tab_and_origin_scoped",
|
||||
ORIGIN_SCOPED = "origin_scoped",
|
||||
PAGE_SCOPED = "page_scoped",
|
||||
CUSTOM_CONTROLLER_SCOPED = "custom_controller_scoped"
|
||||
}
|
||||
|
||||
export enum Capability {
|
||||
VIDEO_OUT = "video_out"
|
||||
, AUDIO_OUT = "audio_out"
|
||||
, VIDEO_IN = "video_in"
|
||||
, AUDIO_IN = "audio_in"
|
||||
, MULTIZONE_GROUP = "multizone_group"
|
||||
VIDEO_OUT = "video_out",
|
||||
AUDIO_OUT = "audio_out",
|
||||
VIDEO_IN = "video_in",
|
||||
AUDIO_IN = "audio_in",
|
||||
MULTIZONE_GROUP = "multizone_group"
|
||||
}
|
||||
|
||||
export enum DefaultActionPolicy {
|
||||
CREATE_SESSION = "create_session"
|
||||
, CAST_THIS_TAB = "cast_this_tab"
|
||||
CREATE_SESSION = "create_session",
|
||||
CAST_THIS_TAB = "cast_this_tab"
|
||||
}
|
||||
|
||||
export enum DialAppState {
|
||||
RUNNING = "running"
|
||||
, STOPPED = "stopped"
|
||||
, ERROR = "error"
|
||||
RUNNING = "running",
|
||||
STOPPED = "stopped",
|
||||
ERROR = "error"
|
||||
}
|
||||
|
||||
export enum 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"
|
||||
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 enum ReceiverAction {
|
||||
CAST = "cast"
|
||||
, STOP = "stop"
|
||||
CAST = "cast",
|
||||
STOP = "stop"
|
||||
}
|
||||
|
||||
export enum ReceiverAvailability {
|
||||
AVAILABLE = "available"
|
||||
, UNAVAILABLE = "unavailable"
|
||||
AVAILABLE = "available",
|
||||
UNAVAILABLE = "unavailable"
|
||||
}
|
||||
|
||||
export enum ReceiverType {
|
||||
CAST = "cast"
|
||||
, DIAL = "dial"
|
||||
, HANGOUT = "hangout"
|
||||
, CUSTOM = "custom"
|
||||
CAST = "cast",
|
||||
DIAL = "dial",
|
||||
HANGOUT = "hangout",
|
||||
CUSTOM = "custom"
|
||||
}
|
||||
|
||||
export enum SenderPlatform {
|
||||
CHROME = "chrome"
|
||||
, IOS = "ios"
|
||||
, ANDROID = "android"
|
||||
CHROME = "chrome",
|
||||
IOS = "ios",
|
||||
ANDROID = "android"
|
||||
}
|
||||
|
||||
export enum SessionStatus {
|
||||
CONNECTED = "connected"
|
||||
, DISCONNECTED = "disconnected"
|
||||
, STOPPED = "stopped"
|
||||
CONNECTED = "connected",
|
||||
DISCONNECTED = "disconnected",
|
||||
STOPPED = "stopped"
|
||||
}
|
||||
|
||||
export enum VolumeControlType {
|
||||
ATTENUATION = "attenuation"
|
||||
, FIXED = "fixed"
|
||||
, MASTER = "master"
|
||||
ATTENUATION = "attenuation",
|
||||
FIXED = "fixed",
|
||||
MASTER = "master"
|
||||
}
|
||||
|
||||
@@ -7,29 +7,47 @@ import { ErrorCallback, SuccessCallback } from "../types";
|
||||
|
||||
import { onMessage, sendMessageResponse } from "../eventMessageChannel";
|
||||
|
||||
import { AutoJoinPolicy, Capability, DefaultActionPolicy, DialAppState
|
||||
, ErrorCode, ReceiverAction, ReceiverAvailability, ReceiverType
|
||||
, SenderPlatform, SessionStatus, VolumeControlType } from "./enums";
|
||||
import {
|
||||
AutoJoinPolicy,
|
||||
Capability,
|
||||
DefaultActionPolicy,
|
||||
DialAppState,
|
||||
ErrorCode,
|
||||
ReceiverAction,
|
||||
ReceiverAvailability,
|
||||
ReceiverType,
|
||||
SenderPlatform,
|
||||
SessionStatus,
|
||||
VolumeControlType
|
||||
} from "./enums";
|
||||
|
||||
import { ApiConfig, CredentialsData, DialRequest, Error as Error_, Image
|
||||
, Receiver, ReceiverDisplayStatus, SenderApplication, SessionRequest
|
||||
, Timeout, Volume } from "./dataClasses";
|
||||
import {
|
||||
ApiConfig,
|
||||
CredentialsData,
|
||||
DialRequest,
|
||||
Error as Error_,
|
||||
Image,
|
||||
Receiver,
|
||||
ReceiverDisplayStatus,
|
||||
SenderApplication,
|
||||
SessionRequest,
|
||||
Timeout,
|
||||
Volume
|
||||
} from "./dataClasses";
|
||||
|
||||
import Session from "./Session";
|
||||
|
||||
|
||||
type ReceiverActionListener = (
|
||||
receiver: Receiver
|
||||
, receiverAction: string) => void;
|
||||
receiver: Receiver,
|
||||
receiverAction: string
|
||||
) => void;
|
||||
|
||||
type RequestSessionSuccessCallback = (session: Session) => void;
|
||||
|
||||
|
||||
let apiConfig: Nullable<ApiConfig>;
|
||||
let sessionRequest: Nullable<SessionRequest>;
|
||||
|
||||
let requestSessionSuccessCallback: Nullable<
|
||||
RequestSessionSuccessCallback>;
|
||||
let requestSessionSuccessCallback: Nullable<RequestSessionSuccessCallback>;
|
||||
let requestSessionErrorCallback: Nullable<ErrorCallback>;
|
||||
|
||||
const receiverActionListeners = new Set<ReceiverActionListener>();
|
||||
@@ -37,16 +55,36 @@ const receiverActionListeners = new Set<ReceiverActionListener>();
|
||||
const receiverDevices = new Map<string, ReceiverDevice>();
|
||||
const sessions = new Map<string, Session>();
|
||||
|
||||
export {
|
||||
AutoJoinPolicy,
|
||||
Capability,
|
||||
DefaultActionPolicy,
|
||||
DialAppState,
|
||||
ErrorCode,
|
||||
ReceiverAction,
|
||||
ReceiverAvailability,
|
||||
ReceiverType,
|
||||
SenderPlatform,
|
||||
SessionStatus,
|
||||
VolumeControlType
|
||||
};
|
||||
|
||||
export { AutoJoinPolicy, Capability, DefaultActionPolicy, DialAppState
|
||||
, ErrorCode, ReceiverAction, ReceiverAvailability, ReceiverType
|
||||
, SenderPlatform, SessionStatus, VolumeControlType };
|
||||
export {
|
||||
ApiConfig,
|
||||
CredentialsData,
|
||||
DialRequest,
|
||||
Error_ as Error,
|
||||
Image,
|
||||
Receiver,
|
||||
ReceiverDisplayStatus,
|
||||
SenderApplication,
|
||||
SessionRequest,
|
||||
Timeout,
|
||||
Volume,
|
||||
Session
|
||||
};
|
||||
|
||||
export { ApiConfig, CredentialsData, DialRequest, Error_ as Error, Image
|
||||
, Receiver, ReceiverDisplayStatus, SenderApplication, SessionRequest
|
||||
, Timeout, Volume, Session };
|
||||
|
||||
export const VERSION = [ 1, 2 ];
|
||||
export const VERSION = [1, 2];
|
||||
export let isAvailable = false;
|
||||
|
||||
export const timeout = new Timeout();
|
||||
@@ -54,31 +92,33 @@ export const timeout = new Timeout();
|
||||
// chrome.cast.media namespace
|
||||
export * as media from "./media";
|
||||
|
||||
|
||||
function sendSessionRequest(sessionRequest: SessionRequest
|
||||
, receiverDevice: ReceiverDevice) {
|
||||
|
||||
function sendSessionRequest(
|
||||
sessionRequest: SessionRequest,
|
||||
receiverDevice: ReceiverDevice
|
||||
) {
|
||||
for (const listener of receiverActionListeners) {
|
||||
const receiver = new Receiver(
|
||||
receiverDevice.id
|
||||
, receiverDevice.friendlyName);
|
||||
receiverDevice.id,
|
||||
receiverDevice.friendlyName
|
||||
);
|
||||
|
||||
listener(receiver, ReceiverAction.CAST);
|
||||
}
|
||||
|
||||
sendMessageResponse({
|
||||
subject: "bridge:createCastSession"
|
||||
, data: {
|
||||
appId: sessionRequest.appId
|
||||
, receiverDevice: receiverDevice
|
||||
subject: "bridge:createCastSession",
|
||||
data: {
|
||||
appId: sessionRequest.appId,
|
||||
receiverDevice: receiverDevice
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function initialize(newApiConfig: ApiConfig
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
export function initialize(
|
||||
newApiConfig: ApiConfig,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
logger.info("cast.initialize");
|
||||
|
||||
// Already initialized
|
||||
@@ -90,22 +130,25 @@ export function initialize(newApiConfig: ApiConfig
|
||||
apiConfig = newApiConfig;
|
||||
|
||||
sendMessageResponse({
|
||||
subject: "main:shimReady"
|
||||
, data: { appId: apiConfig.sessionRequest.appId }
|
||||
subject: "main:shimReady",
|
||||
data: { appId: apiConfig.sessionRequest.appId }
|
||||
});
|
||||
|
||||
successCallback?.();
|
||||
|
||||
apiConfig.receiverListener(receiverDevices.size
|
||||
? ReceiverAvailability.AVAILABLE
|
||||
: ReceiverAvailability.UNAVAILABLE);
|
||||
apiConfig.receiverListener(
|
||||
receiverDevices.size
|
||||
? ReceiverAvailability.AVAILABLE
|
||||
: ReceiverAvailability.UNAVAILABLE
|
||||
);
|
||||
}
|
||||
|
||||
export function requestSession(successCallback: RequestSessionSuccessCallback
|
||||
, errorCallback: ErrorCallback
|
||||
, newSessionRequest?: SessionRequest
|
||||
, receiverDevice?: ReceiverDevice) {
|
||||
|
||||
export function requestSession(
|
||||
successCallback: RequestSessionSuccessCallback,
|
||||
errorCallback: ErrorCallback,
|
||||
newSessionRequest?: SessionRequest,
|
||||
receiverDevice?: ReceiverDevice
|
||||
) {
|
||||
logger.info("cast.requestSession");
|
||||
|
||||
// Not yet initialized
|
||||
@@ -116,9 +159,12 @@ export function requestSession(successCallback: RequestSessionSuccessCallback
|
||||
|
||||
// Already requesting session
|
||||
if (sessionRequest) {
|
||||
errorCallback?.(new Error_(
|
||||
ErrorCode.INVALID_PARAMETER
|
||||
, "Session request already in progress."));
|
||||
errorCallback?.(
|
||||
new Error_(
|
||||
ErrorCode.INVALID_PARAMETER,
|
||||
"Session request already in progress."
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,9 +203,11 @@ export function requestSessionById(_sessionId: string): void {
|
||||
logger.info("STUB :: cast.requestSessionById");
|
||||
}
|
||||
|
||||
export function setCustomReceivers(_receivers: Receiver[]
|
||||
, _successCallback?: SuccessCallback
|
||||
, _errorCallback?: ErrorCallback): void {
|
||||
export function setCustomReceivers(
|
||||
_receivers: Receiver[],
|
||||
_successCallback?: SuccessCallback,
|
||||
_errorCallback?: ErrorCallback
|
||||
): void {
|
||||
logger.info("STUB :: cast.setCustomReceivers");
|
||||
}
|
||||
|
||||
@@ -190,7 +238,6 @@ export function precache(_data: string) {
|
||||
logger.info("STUB :: cast.precache");
|
||||
}
|
||||
|
||||
|
||||
onMessage(message => {
|
||||
switch (message.subject) {
|
||||
case "shim:initialized": {
|
||||
@@ -212,18 +259,19 @@ onMessage(message => {
|
||||
|
||||
// TODO: Implement persistent per-origin receiver IDs
|
||||
const receiver = new Receiver(
|
||||
status.receiverFriendlyName // label
|
||||
, status.receiverFriendlyName // friendlyName
|
||||
, [ Capability.VIDEO_OUT
|
||||
, Capability.AUDIO_OUT ] // capabilities
|
||||
, status.volume); // volume
|
||||
status.receiverFriendlyName, // label
|
||||
status.receiverFriendlyName, // friendlyName
|
||||
[Capability.VIDEO_OUT, Capability.AUDIO_OUT], // capabilities
|
||||
status.volume // volume
|
||||
);
|
||||
|
||||
const session = new Session(
|
||||
status.sessionId // sessionId
|
||||
, status.appId // appId
|
||||
, status.displayName // displayName
|
||||
, status.appImages // appImages
|
||||
, receiver); // receiver
|
||||
status.sessionId, // sessionId
|
||||
status.appId, // appId
|
||||
status.displayName, // displayName
|
||||
status.appImages, // appImages
|
||||
receiver // receiver
|
||||
);
|
||||
|
||||
session.senderApps = status.senderApps;
|
||||
session.transportId = status.transportId;
|
||||
@@ -293,7 +341,7 @@ onMessage(message => {
|
||||
|
||||
const callbacks = session._sendMessageCallbacks.get(messageId);
|
||||
if (callbacks) {
|
||||
const [ successCallback, errorCallback ] = callbacks;
|
||||
const [successCallback, errorCallback] = callbacks;
|
||||
|
||||
if (error) {
|
||||
errorCallback?.(new Error_(error));
|
||||
@@ -316,8 +364,7 @@ onMessage(message => {
|
||||
|
||||
if (apiConfig) {
|
||||
// Notify listeners of new cast destination
|
||||
apiConfig.receiverListener(
|
||||
ReceiverAvailability.AVAILABLE);
|
||||
apiConfig.receiverListener(ReceiverAvailability.AVAILABLE);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -331,7 +378,8 @@ onMessage(message => {
|
||||
if (receiverDevices.size === 0) {
|
||||
if (apiConfig) {
|
||||
apiConfig.receiverListener(
|
||||
ReceiverAvailability.UNAVAILABLE);
|
||||
ReceiverAvailability.UNAVAILABLE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,8 +407,9 @@ onMessage(message => {
|
||||
|
||||
for (const listener of receiverActionListeners) {
|
||||
const castReceiver = new Receiver(
|
||||
receiver.id
|
||||
, receiver.friendlyName);
|
||||
receiver.id,
|
||||
receiver.friendlyName
|
||||
);
|
||||
|
||||
listener(castReceiver, ReceiverAction.STOP);
|
||||
}
|
||||
@@ -376,12 +425,10 @@ onMessage(message => {
|
||||
if (sessionRequest) {
|
||||
sessionRequest = null;
|
||||
|
||||
requestSessionErrorCallback?.(
|
||||
new Error_(ErrorCode.CANCEL));
|
||||
requestSessionErrorCallback?.(new Error_(ErrorCode.CANCEL));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,22 +5,34 @@ import { v1 as uuid } from "uuid";
|
||||
import logger from "../../../lib/logger";
|
||||
|
||||
import { Volume, Error as _Error } from "../dataClasses";
|
||||
import { BreakStatus, EditTracksInfoRequest, GetStatusRequest, LiveSeekableRange
|
||||
, MediaInfo, PauseRequest, PlayRequest, QueueData, QueueJumpRequest
|
||||
, QueueInsertItemsRequest, QueueItem, QueueSetPropertiesRequest
|
||||
, QueueRemoveItemsRequest, QueueReorderItemsRequest
|
||||
, QueueUpdateItemsRequest, SeekRequest, StopRequest, VideoInformation
|
||||
, VolumeRequest } from "./dataClasses";
|
||||
import {
|
||||
BreakStatus,
|
||||
EditTracksInfoRequest,
|
||||
GetStatusRequest,
|
||||
LiveSeekableRange,
|
||||
MediaInfo,
|
||||
PauseRequest,
|
||||
PlayRequest,
|
||||
QueueData,
|
||||
QueueJumpRequest,
|
||||
QueueInsertItemsRequest,
|
||||
QueueItem,
|
||||
QueueSetPropertiesRequest,
|
||||
QueueRemoveItemsRequest,
|
||||
QueueReorderItemsRequest,
|
||||
QueueUpdateItemsRequest,
|
||||
SeekRequest,
|
||||
StopRequest,
|
||||
VideoInformation,
|
||||
VolumeRequest
|
||||
} from "./dataClasses";
|
||||
|
||||
import { PlayerState, RepeatMode } from "./enums";
|
||||
import { ErrorCode } from "../enums";
|
||||
|
||||
import { ErrorCallback
|
||||
, SuccessCallback
|
||||
, UpdateListener } from "../../types";
|
||||
import { ErrorCallback, SuccessCallback, UpdateListener } from "../../types";
|
||||
import { SenderMediaMessage } from "../types";
|
||||
|
||||
|
||||
export default class Media {
|
||||
#id = uuid();
|
||||
|
||||
@@ -49,12 +61,13 @@ export default class Media {
|
||||
preloadedItemId: Nullable<number> = null;
|
||||
queueData?: QueueData;
|
||||
|
||||
|
||||
constructor(public sessionId: string
|
||||
, public mediaSessionId: number
|
||||
, public _sendMediaMessage: (message: DistributiveOmit<
|
||||
SenderMediaMessage, "requestId">) => Promise<void>) {
|
||||
}
|
||||
constructor(
|
||||
public sessionId: string,
|
||||
public mediaSessionId: number,
|
||||
public _sendMediaMessage: (
|
||||
message: DistributiveOmit<SenderMediaMessage, "requestId">
|
||||
) => Promise<void>
|
||||
) {}
|
||||
|
||||
addUpdateListener(listener: UpdateListener) {
|
||||
this._updateListeners.add(listener);
|
||||
@@ -63,14 +76,16 @@ export default class Media {
|
||||
this._updateListeners.delete(listener);
|
||||
}
|
||||
|
||||
editTracksInfo(editTracksInfoRequest: EditTracksInfoRequest
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...editTracksInfoRequest
|
||||
, type: "EDIT_TRACKS_INFO"
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
editTracksInfo(
|
||||
editTracksInfoRequest: EditTracksInfoRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...editTracksInfoRequest,
|
||||
type: "EDIT_TRACKS_INFO",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
@@ -92,14 +107,16 @@ export default class Media {
|
||||
*/
|
||||
getEstimatedTime(): number {
|
||||
if (this.playerState === PlayerState.PLAYING && this._lastUpdateTime) {
|
||||
let estimatedTime = this.currentTime +
|
||||
(((Date.now() - this._lastUpdateTime) / 1000));
|
||||
let estimatedTime =
|
||||
this.currentTime + (Date.now() - this._lastUpdateTime) / 1000;
|
||||
|
||||
// Enforce valid range
|
||||
if (estimatedTime < 0) {
|
||||
estimatedTime = 0;
|
||||
} else if (this.media?.duration &&
|
||||
estimatedTime > this.media.duration) {
|
||||
} else if (
|
||||
this.media?.duration &&
|
||||
estimatedTime > this.media.duration
|
||||
) {
|
||||
estimatedTime = this.media.duration;
|
||||
}
|
||||
|
||||
@@ -113,92 +130,104 @@ export default class Media {
|
||||
* Request media status from the receiver application. This
|
||||
* will also trigger any added media update listeners.
|
||||
*/
|
||||
getStatus(getStatusRequest = new GetStatusRequest()
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...getStatusRequest
|
||||
, type: "MEDIA_GET_STATUS"
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
getStatus(
|
||||
getStatusRequest = new GetStatusRequest(),
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...getStatusRequest,
|
||||
type: "MEDIA_GET_STATUS",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
pause(pauseRequest = new PauseRequest()
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...pauseRequest
|
||||
, type: "PAUSE"
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
pause(
|
||||
pauseRequest = new PauseRequest(),
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...pauseRequest,
|
||||
type: "PAUSE",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
play(playRequest = new PlayRequest()
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...playRequest
|
||||
, type: "PLAY"
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
play(
|
||||
playRequest = new PlayRequest(),
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...playRequest,
|
||||
type: "PLAY",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueAppendItem(item: QueueItem
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...new QueueInsertItemsRequest([ item ])
|
||||
, type: "QUEUE_INSERT"
|
||||
, sessionId: this.sessionId
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
queueAppendItem(
|
||||
item: QueueItem,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...new QueueInsertItemsRequest([item]),
|
||||
type: "QUEUE_INSERT",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueInsertItems(queueInsertItemsRequest: QueueInsertItemsRequest
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...queueInsertItemsRequest
|
||||
, type: "QUEUE_INSERT"
|
||||
, sessionId: this.sessionId
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
queueInsertItems(
|
||||
queueInsertItemsRequest: QueueInsertItemsRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...queueInsertItemsRequest,
|
||||
type: "QUEUE_INSERT",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
|
||||
}
|
||||
|
||||
queueJumpToItem(itemId: number
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
queueJumpToItem(
|
||||
itemId: number,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
if (this.items?.find(item => item.itemId === itemId)) {
|
||||
const jumpRequest = new QueueJumpRequest();
|
||||
jumpRequest.currentItemId = itemId;
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...jumpRequest
|
||||
, type: "QUEUE_UPDATE"
|
||||
, sessionId: this.sessionId
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
this._sendMediaMessage({
|
||||
...jumpRequest,
|
||||
type: "QUEUE_UPDATE",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
}
|
||||
|
||||
queueMoveItemToNewIndex(itemId: number
|
||||
, newIndex: number
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
queueMoveItemToNewIndex(
|
||||
itemId: number,
|
||||
newIndex: number,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
// Return early if not in queue
|
||||
if (!this.items) {
|
||||
return;
|
||||
@@ -213,170 +242,194 @@ export default class Media {
|
||||
errorCallback(new _Error(ErrorCode.INVALID_PARAMETER));
|
||||
}
|
||||
} else if (newIndex == itemIndex) {
|
||||
if (successCallback) { successCallback(); }
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (newIndex > itemIndex) {
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
const reorderItemsRequest =
|
||||
new QueueReorderItemsRequest([ itemId ]);
|
||||
const reorderItemsRequest = new QueueReorderItemsRequest([itemId]);
|
||||
if (newIndex < this.items.length) {
|
||||
const existingItem = this.items[newIndex];
|
||||
reorderItemsRequest.insertBefore = existingItem.itemId;
|
||||
}
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...reorderItemsRequest
|
||||
, type: "QUEUE_REORDER"
|
||||
, sessionId: this.sessionId
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
this._sendMediaMessage({
|
||||
...reorderItemsRequest,
|
||||
type: "QUEUE_REORDER",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
}
|
||||
|
||||
queueNext(successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
queueNext(
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
const jumpRequest = new QueueJumpRequest();
|
||||
jumpRequest.jump = 1;
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...jumpRequest
|
||||
, type: "QUEUE_UPDATE"
|
||||
, sessionId: this.sessionId
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
this._sendMediaMessage({
|
||||
...jumpRequest,
|
||||
type: "QUEUE_UPDATE",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queuePrev(successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
queuePrev(
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
const jumpRequest = new QueueJumpRequest();
|
||||
jumpRequest.jump = -1;
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...jumpRequest
|
||||
, type: "QUEUE_UPDATE"
|
||||
, sessionId: this.sessionId
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
this._sendMediaMessage({
|
||||
...jumpRequest,
|
||||
type: "QUEUE_UPDATE",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueRemoveItem(itemId: number
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
queueRemoveItem(
|
||||
itemId: number,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
const item = this.items?.find(item => item.itemId === itemId);
|
||||
if (item) {
|
||||
this.queueRemoveItems(
|
||||
new QueueRemoveItemsRequest([ itemId ])
|
||||
, successCallback, errorCallback);
|
||||
new QueueRemoveItemsRequest([itemId]),
|
||||
successCallback,
|
||||
errorCallback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
queueRemoveItems(queueRemoveItemsRequest: QueueRemoveItemsRequest
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
queueRemoveItems(
|
||||
queueRemoveItemsRequest: QueueRemoveItemsRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...queueRemoveItemsRequest,
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...queueRemoveItemsRequest
|
||||
|
||||
, mediaSessionId: this.mediaSessionId
|
||||
, type: "QUEUE_REMOVE"
|
||||
, sessionId: this.sessionId })
|
||||
mediaSessionId: this.mediaSessionId,
|
||||
type: "QUEUE_REMOVE",
|
||||
sessionId: this.sessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueReorderItems(queueReorderItemsRequest: QueueReorderItemsRequest
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
queueReorderItems(
|
||||
queueReorderItemsRequest: QueueReorderItemsRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...queueReorderItemsRequest,
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...queueReorderItemsRequest
|
||||
|
||||
, mediaSessionId: this.mediaSessionId
|
||||
, type: "QUEUE_REORDER"
|
||||
, sessionId: this.sessionId })
|
||||
mediaSessionId: this.mediaSessionId,
|
||||
type: "QUEUE_REORDER",
|
||||
sessionId: this.sessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueSetRepeatMode(repeatMode: string
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
queueSetRepeatMode(
|
||||
repeatMode: string,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
const setPropertiesRequest = new QueueSetPropertiesRequest();
|
||||
setPropertiesRequest.repeatMode = repeatMode;
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...setPropertiesRequest
|
||||
, type: "QUEUE_UPDATE"
|
||||
, sessionId: this.sessionId
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
this._sendMediaMessage({
|
||||
...setPropertiesRequest,
|
||||
type: "QUEUE_UPDATE",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueUpdateItems(queueUpdateItemsRequest: QueueUpdateItemsRequest
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...queueUpdateItemsRequest
|
||||
, type: "QUEUE_UPDATE"
|
||||
, sessionId: this.sessionId
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
queueUpdateItems(
|
||||
queueUpdateItemsRequest: QueueUpdateItemsRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...queueUpdateItemsRequest,
|
||||
type: "QUEUE_UPDATE",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
seek(seekRequest: SeekRequest
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...seekRequest
|
||||
, type: "SEEK"
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
seek(
|
||||
seekRequest: SeekRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...seekRequest,
|
||||
type: "SEEK",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
setVolume(volumeRequest: VolumeRequest
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
this._sendMediaMessage(
|
||||
{ ...volumeRequest
|
||||
, type: "MEDIA_SET_VOLUME"
|
||||
, mediaSessionId: this.mediaSessionId })
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
setVolume(
|
||||
volumeRequest: VolumeRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
this._sendMediaMessage({
|
||||
...volumeRequest,
|
||||
type: "MEDIA_SET_VOLUME",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
stop(stopRequest?: StopRequest
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback) {
|
||||
|
||||
stop(
|
||||
stopRequest?: StopRequest,
|
||||
successCallback?: SuccessCallback,
|
||||
errorCallback?: ErrorCallback
|
||||
) {
|
||||
if (!stopRequest) {
|
||||
stopRequest = new StopRequest();
|
||||
}
|
||||
|
||||
this._sendMediaMessage({
|
||||
...stopRequest
|
||||
, type: "STOP"
|
||||
, mediaSessionId: this.mediaSessionId
|
||||
}).then(() => {
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
}).catch(errorCallback);
|
||||
...stopRequest,
|
||||
type: "STOP",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(() => {
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
})
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
supportsCommand(command: string): boolean {
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
|
||||
import { Image, Volume } from "../dataClasses";
|
||||
|
||||
import { ContainerType
|
||||
, HdrType
|
||||
, HlsSegmentFormat
|
||||
, HlsVideoSegmentFormat
|
||||
, MetadataType
|
||||
, RepeatMode
|
||||
, ResumeState, StreamType
|
||||
, TrackType, UserAction } from "./enums";
|
||||
|
||||
import {
|
||||
ContainerType,
|
||||
HdrType,
|
||||
HlsSegmentFormat,
|
||||
HlsVideoSegmentFormat,
|
||||
MetadataType,
|
||||
RepeatMode,
|
||||
ResumeState,
|
||||
StreamType,
|
||||
TrackType,
|
||||
UserAction
|
||||
} from "./enums";
|
||||
|
||||
export class AudiobookChapterMediaMetadata {
|
||||
bookTitle?: string;
|
||||
@@ -35,9 +38,10 @@ export class Break {
|
||||
isWatched = false;
|
||||
|
||||
constructor(
|
||||
public id: string
|
||||
, public breakClipIds: string[]
|
||||
, public position: number) {}
|
||||
public id: string,
|
||||
public breakClipIds: string[],
|
||||
public position: number
|
||||
) {}
|
||||
}
|
||||
|
||||
export class BreakClip {
|
||||
@@ -71,17 +75,17 @@ export class ContainerMetadata {
|
||||
title?: string;
|
||||
|
||||
constructor(
|
||||
public containerType: ContainerType =
|
||||
ContainerType.GENERIC_CONTAINER) {}
|
||||
public containerType: ContainerType = ContainerType.GENERIC_CONTAINER
|
||||
) {}
|
||||
}
|
||||
|
||||
export class EditTracksInfoRequest {
|
||||
requestId = 0;
|
||||
|
||||
constructor(
|
||||
public activeTrackIds: Nullable<number[]> = null
|
||||
, public textTrackStyle: Nullable<string> = null) {
|
||||
}
|
||||
public activeTrackIds: Nullable<number[]> = null,
|
||||
public textTrackStyle: Nullable<string> = null
|
||||
) {}
|
||||
}
|
||||
|
||||
export class GenericMediaMetadata {
|
||||
@@ -100,10 +104,11 @@ export class GetStatusRequest {
|
||||
|
||||
export class LiveSeekableRange {
|
||||
constructor(
|
||||
public start?: number
|
||||
, public end?: number
|
||||
, public isMovingWindow?: boolean
|
||||
, public isLiveDone?: boolean) {}
|
||||
public start?: number,
|
||||
public end?: number,
|
||||
public isMovingWindow?: boolean,
|
||||
public isLiveDone?: boolean
|
||||
) {}
|
||||
}
|
||||
|
||||
export class LoadRequest {
|
||||
@@ -123,13 +128,12 @@ export class LoadRequest {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type Metadata =
|
||||
GenericMediaMetadata
|
||||
| MovieMediaMetadata
|
||||
| MusicTrackMediaMetadata
|
||||
| PhotoMediaMetadata
|
||||
| TvShowMediaMetadata;
|
||||
| GenericMediaMetadata
|
||||
| MovieMediaMetadata
|
||||
| MusicTrackMediaMetadata
|
||||
| PhotoMediaMetadata
|
||||
| TvShowMediaMetadata;
|
||||
|
||||
export class MediaInfo {
|
||||
atvEntity?: string;
|
||||
@@ -149,12 +153,9 @@ export class MediaInfo {
|
||||
userActionStates?: UserActionState[];
|
||||
vmapAdsRequest?: VastAdsRequest;
|
||||
|
||||
constructor(
|
||||
public contentId: string
|
||||
, public contentType: string) {}
|
||||
constructor(public contentId: string, public contentType: string) {}
|
||||
}
|
||||
|
||||
|
||||
export class MediaMetadata {
|
||||
queueItemId?: number;
|
||||
sectionDuration?: number;
|
||||
@@ -224,13 +225,14 @@ export class QueueData {
|
||||
shuffle = false;
|
||||
|
||||
constructor(
|
||||
public id?: string
|
||||
, public name?: string
|
||||
, public description?: string
|
||||
, public repeatMode?: RepeatMode
|
||||
, public items?: QueueItem[]
|
||||
, public startIndex?: number
|
||||
, public startTime?: number) {}
|
||||
public id?: string,
|
||||
public name?: string,
|
||||
public description?: string,
|
||||
public repeatMode?: RepeatMode,
|
||||
public items?: QueueItem[],
|
||||
public startIndex?: number,
|
||||
public startTime?: number
|
||||
) {}
|
||||
}
|
||||
|
||||
export class QueueInsertItemsRequest {
|
||||
@@ -240,8 +242,7 @@ export class QueueInsertItemsRequest {
|
||||
sessionId: Nullable<string> = null;
|
||||
type = "QUEUE_INSERT";
|
||||
|
||||
constructor(
|
||||
public items: QueueItem[]) {}
|
||||
constructor(public items: QueueItem[]) {}
|
||||
}
|
||||
|
||||
export class QueueItem {
|
||||
@@ -302,7 +303,6 @@ export class QueueUpdateItemsRequest {
|
||||
constructor(public items: QueueItem[]) {}
|
||||
}
|
||||
|
||||
|
||||
export class SeekRequest {
|
||||
currentTime: Nullable<number> = null;
|
||||
customData: any = null;
|
||||
@@ -336,9 +336,7 @@ export class Track {
|
||||
trackContentId: Nullable<string> = null;
|
||||
trackContentType: Nullable<string> = null;
|
||||
|
||||
constructor(
|
||||
public trackId: number
|
||||
, public type: TrackType) {}
|
||||
constructor(public trackId: number, public type: TrackType) {}
|
||||
}
|
||||
|
||||
export class TvShowMediaMetadata {
|
||||
@@ -359,8 +357,7 @@ export class TvShowMediaMetadata {
|
||||
export class UserActionState {
|
||||
customData: any = null;
|
||||
|
||||
constructor(
|
||||
public userAction: UserAction) {}
|
||||
constructor(public userAction: UserAction) {}
|
||||
}
|
||||
|
||||
export class VastAdsRequest {
|
||||
@@ -370,14 +367,14 @@ export class VastAdsRequest {
|
||||
|
||||
export class VideoInformation {
|
||||
constructor(
|
||||
public width: number
|
||||
, public height: number
|
||||
, public hdrType: HdrType) {}
|
||||
public width: number,
|
||||
public height: number,
|
||||
public hdrType: HdrType
|
||||
) {}
|
||||
}
|
||||
|
||||
export class VolumeRequest {
|
||||
customData: any = null;
|
||||
|
||||
constructor(
|
||||
public volume: Volume) {}
|
||||
constructor(public volume: Volume) {}
|
||||
}
|
||||
|
||||
@@ -1,139 +1,139 @@
|
||||
"use strict";
|
||||
|
||||
export enum ContainerType {
|
||||
GENERIC_CONTAINER
|
||||
, AUDIOBOOK_CONTAINER
|
||||
GENERIC_CONTAINER,
|
||||
AUDIOBOOK_CONTAINER
|
||||
}
|
||||
|
||||
export enum HdrType {
|
||||
SDR = "sdr"
|
||||
, HDR = "hdr"
|
||||
, DV = "dv"
|
||||
SDR = "sdr",
|
||||
HDR = "hdr",
|
||||
DV = "dv"
|
||||
}
|
||||
|
||||
export enum HlsSegmentFormat {
|
||||
AAC = "aac"
|
||||
, AC3 = "ac3"
|
||||
, MP3 = "mp3"
|
||||
, TS = "ts"
|
||||
, TS_AAC = "ts_aac"
|
||||
, E_AC3 = "e_ac3"
|
||||
, FMP4 = "fmp4"
|
||||
AAC = "aac",
|
||||
AC3 = "ac3",
|
||||
MP3 = "mp3",
|
||||
TS = "ts",
|
||||
TS_AAC = "ts_aac",
|
||||
E_AC3 = "e_ac3",
|
||||
FMP4 = "fmp4"
|
||||
}
|
||||
|
||||
export enum HlsVideoSegmentFormat {
|
||||
MPEG2_TS = "mpeg2_ts"
|
||||
, FMP4 = "fmp4"
|
||||
MPEG2_TS = "mpeg2_ts",
|
||||
FMP4 = "fmp4"
|
||||
}
|
||||
|
||||
export enum IdleReason {
|
||||
CANCELLED = "CANCELLED"
|
||||
, INTERRUPTED = "INTERRUPTED"
|
||||
, FINISHED = "FINISHED"
|
||||
, ERROR = "ERROR"
|
||||
CANCELLED = "CANCELLED",
|
||||
INTERRUPTED = "INTERRUPTED",
|
||||
FINISHED = "FINISHED",
|
||||
ERROR = "ERROR"
|
||||
}
|
||||
|
||||
export enum MediaCommand {
|
||||
PAUSE = "pause"
|
||||
, SEEK = "seek"
|
||||
, STREAM_VOLUME = "stream_volume"
|
||||
, STREAM_MUTE = "stream_mute"
|
||||
PAUSE = "pause",
|
||||
SEEK = "seek",
|
||||
STREAM_VOLUME = "stream_volume",
|
||||
STREAM_MUTE = "stream_mute"
|
||||
}
|
||||
|
||||
export enum MetadataType {
|
||||
GENERIC
|
||||
, MOVIE
|
||||
, TV_SHOW
|
||||
, MUSIC_TRACK
|
||||
, PHOTO
|
||||
, AUDIOBOOK_CHAPTER
|
||||
GENERIC,
|
||||
MOVIE,
|
||||
TV_SHOW,
|
||||
MUSIC_TRACK,
|
||||
PHOTO,
|
||||
AUDIOBOOK_CHAPTER
|
||||
}
|
||||
|
||||
export enum PlayerState {
|
||||
IDLE = "IDLE"
|
||||
, PLAYING = "PLAYING"
|
||||
, PAUSED = "PAUSED"
|
||||
, BUFFERING = "BUFFERING"
|
||||
IDLE = "IDLE",
|
||||
PLAYING = "PLAYING",
|
||||
PAUSED = "PAUSED",
|
||||
BUFFERING = "BUFFERING"
|
||||
}
|
||||
|
||||
export enum QueueType {
|
||||
ALBUM = "ALBUM"
|
||||
, PLAYLIST = "PLAYLIST"
|
||||
, AUDIOBOOK = "AUDIOBOOK"
|
||||
, RADIO_STATION = "RADIO_STATION"
|
||||
, PODCAST_SERIES = "PODCAST_SERIES"
|
||||
, TV_SERIES = "TV_SERIES"
|
||||
, VIDEO_PLAYLIST = "VIDEO_PLAYLIST"
|
||||
, LIVE_TV = "LIVETV"
|
||||
, MOVIE = "MOVIE"
|
||||
ALBUM = "ALBUM",
|
||||
PLAYLIST = "PLAYLIST",
|
||||
AUDIOBOOK = "AUDIOBOOK",
|
||||
RADIO_STATION = "RADIO_STATION",
|
||||
PODCAST_SERIES = "PODCAST_SERIES",
|
||||
TV_SERIES = "TV_SERIES",
|
||||
VIDEO_PLAYLIST = "VIDEO_PLAYLIST",
|
||||
LIVE_TV = "LIVETV",
|
||||
MOVIE = "MOVIE"
|
||||
}
|
||||
|
||||
export enum RepeatMode {
|
||||
OFF = "REPEAT_OFF"
|
||||
, ALL = "REPEAT_ALL"
|
||||
, SINGLE = "REPEAT_SINGLE"
|
||||
, ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
|
||||
OFF = "REPEAT_OFF",
|
||||
ALL = "REPEAT_ALL",
|
||||
SINGLE = "REPEAT_SINGLE",
|
||||
ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
|
||||
}
|
||||
|
||||
export enum ResumeState {
|
||||
PLAYBACK_START = "PLAYBACK_START"
|
||||
, PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
|
||||
PLAYBACK_START = "PLAYBACK_START",
|
||||
PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
|
||||
}
|
||||
|
||||
export enum StreamType {
|
||||
BUFFERED = "BUFFERED"
|
||||
, LIVE = "LIVE"
|
||||
, OTHER = "OTHER"
|
||||
BUFFERED = "BUFFERED",
|
||||
LIVE = "LIVE",
|
||||
OTHER = "OTHER"
|
||||
}
|
||||
|
||||
export enum TextTrackEdgeType {
|
||||
NONE = "NONE"
|
||||
, OUTLINE = "OUTLINE"
|
||||
, DROP_SHADOW = "DROP_SHADOW"
|
||||
, RAISED = "RAISED"
|
||||
, DEPRESSED = "DEPRESSED"
|
||||
NONE = "NONE",
|
||||
OUTLINE = "OUTLINE",
|
||||
DROP_SHADOW = "DROP_SHADOW",
|
||||
RAISED = "RAISED",
|
||||
DEPRESSED = "DEPRESSED"
|
||||
}
|
||||
|
||||
export enum TextTrackFontGenericFamily {
|
||||
SANS_SERIF = "SANS_SERIF"
|
||||
, MONOSPACED_SANS_SERIF = "MONOSPACED_SANS_SERIF"
|
||||
, SERIF = "SERIF"
|
||||
, MONOSPACED_SERIF = "MONOSPACED_SERIF"
|
||||
, CASUAL = "CASUAL"
|
||||
, CURSIVE = "CURSIVE"
|
||||
, SMALL_CAPITALS = "SMALL_CAPITALS"
|
||||
SANS_SERIF = "SANS_SERIF",
|
||||
MONOSPACED_SANS_SERIF = "MONOSPACED_SANS_SERIF",
|
||||
SERIF = "SERIF",
|
||||
MONOSPACED_SERIF = "MONOSPACED_SERIF",
|
||||
CASUAL = "CASUAL",
|
||||
CURSIVE = "CURSIVE",
|
||||
SMALL_CAPITALS = "SMALL_CAPITALS"
|
||||
}
|
||||
|
||||
export enum TextTrackFontStyle {
|
||||
NORMAL = "NORMAL"
|
||||
, BOLD = "BOLD"
|
||||
, BOLD_ITALIC = "BOLD_ITALIC"
|
||||
, ITALIC = "ITALIC"
|
||||
NORMAL = "NORMAL",
|
||||
BOLD = "BOLD",
|
||||
BOLD_ITALIC = "BOLD_ITALIC",
|
||||
ITALIC = "ITALIC"
|
||||
}
|
||||
|
||||
export enum TextTrackType {
|
||||
SUBTITLES = "SUBTITLES"
|
||||
, CAPTIONS = "CAPTIONS"
|
||||
, DESCRIPTIONS = "DESCRIPTIONS"
|
||||
, CHAPTERS = "CHAPTERS"
|
||||
, METADATA = "METADATA"
|
||||
SUBTITLES = "SUBTITLES",
|
||||
CAPTIONS = "CAPTIONS",
|
||||
DESCRIPTIONS = "DESCRIPTIONS",
|
||||
CHAPTERS = "CHAPTERS",
|
||||
METADATA = "METADATA"
|
||||
}
|
||||
|
||||
export enum TextTrackWindowType {
|
||||
NONE = "NONE"
|
||||
, NORMAL = "NORMAL"
|
||||
, ROUNDED_CORNERS = "ROUNDED_CORNERS"
|
||||
NONE = "NONE",
|
||||
NORMAL = "NORMAL",
|
||||
ROUNDED_CORNERS = "ROUNDED_CORNERS"
|
||||
}
|
||||
|
||||
export enum TrackType {
|
||||
TEXT = "TEXT"
|
||||
, AUDIO = "AUDIO"
|
||||
, VIDEO = "VIDEO"
|
||||
TEXT = "TEXT",
|
||||
AUDIO = "AUDIO",
|
||||
VIDEO = "VIDEO"
|
||||
}
|
||||
|
||||
export enum UserAction {
|
||||
LIKE = "LIKE"
|
||||
, DISLIKE = "DISLIKE"
|
||||
, FOLLOW = "FOLLOW"
|
||||
, UNFOLLOW = "UNFOLLOW"
|
||||
LIKE = "LIKE",
|
||||
DISLIKE = "DISLIKE",
|
||||
FOLLOW = "FOLLOW",
|
||||
UNFOLLOW = "UNFOLLOW"
|
||||
}
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
|
||||
import Media from "./Media";
|
||||
|
||||
|
||||
export { Media };
|
||||
|
||||
export * from "./dataClasses";
|
||||
export * from "./enums";
|
||||
|
||||
export const timeout = {
|
||||
editTracksInfo: 0
|
||||
, getStatus: 0
|
||||
, load: 0
|
||||
, pause: 0
|
||||
, play: 0
|
||||
, queue: 0
|
||||
, seek: 0
|
||||
, setVolume: 0
|
||||
, stop: 0
|
||||
editTracksInfo: 0,
|
||||
getStatus: 0,
|
||||
load: 0,
|
||||
pause: 0,
|
||||
play: 0,
|
||||
queue: 0,
|
||||
seek: 0,
|
||||
setVolume: 0,
|
||||
stop: 0
|
||||
};
|
||||
|
||||
export const DEFAULT_MEDIA_RECEIVER_APP_ID = "CC1AD845";
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
|
||||
import { SenderApplication, Volume, Image } from "./dataClasses";
|
||||
import { MediaInfo, QueueItem } from "./media/dataClasses";
|
||||
import { IdleReason
|
||||
, PlayerState
|
||||
, RepeatMode
|
||||
, ResumeState } from "./media/enums";
|
||||
|
||||
import {
|
||||
IdleReason,
|
||||
PlayerState,
|
||||
RepeatMode,
|
||||
ResumeState
|
||||
} from "./media/enums";
|
||||
|
||||
export interface MediaStatus {
|
||||
mediaSessionId: number;
|
||||
@@ -23,65 +24,62 @@ export interface MediaStatus {
|
||||
currentTime: number;
|
||||
supportedMediaCommands: number;
|
||||
repeatMode: RepeatMode;
|
||||
volume: Volume
|
||||
volume: Volume;
|
||||
customData: unknown;
|
||||
}
|
||||
|
||||
export interface ReceiverApplication {
|
||||
appId: string
|
||||
, appType?: string
|
||||
, displayName: string
|
||||
, iconUrl: string
|
||||
, isIdleScreen: boolean
|
||||
, launchedFromCloud: boolean
|
||||
, namespaces: Array<{ name: string }>
|
||||
, sessionId: string
|
||||
, statusText: string
|
||||
, transportId: string
|
||||
, universalAppId: string
|
||||
appId: string;
|
||||
appType?: string;
|
||||
displayName: string;
|
||||
iconUrl: string;
|
||||
isIdleScreen: boolean;
|
||||
launchedFromCloud: boolean;
|
||||
namespaces: Array<{ name: string }>;
|
||||
sessionId: string;
|
||||
statusText: string;
|
||||
transportId: string;
|
||||
universalAppId: string;
|
||||
}
|
||||
|
||||
export interface ReceiverStatus {
|
||||
applications?: ReceiverApplication[]
|
||||
, isActiveInput?: boolean
|
||||
, isStandBy?: boolean
|
||||
, volume: Volume
|
||||
applications?: ReceiverApplication[];
|
||||
isActiveInput?: boolean;
|
||||
isStandBy?: boolean;
|
||||
volume: Volume;
|
||||
}
|
||||
|
||||
|
||||
export interface CastSessionUpdated {
|
||||
sessionId: string
|
||||
, statusText: string
|
||||
, namespaces: Array<{ name: string }>
|
||||
, volume: Volume
|
||||
sessionId: string;
|
||||
statusText: string;
|
||||
namespaces: Array<{ name: string }>;
|
||||
volume: Volume;
|
||||
}
|
||||
|
||||
export interface CastSessionCreated extends CastSessionUpdated {
|
||||
appId: string
|
||||
, appImages: Image[]
|
||||
, displayName: string
|
||||
, receiverFriendlyName: string
|
||||
, senderApps: SenderApplication[]
|
||||
, transportId: string
|
||||
appId: string;
|
||||
appImages: Image[];
|
||||
displayName: string;
|
||||
receiverFriendlyName: string;
|
||||
senderApps: SenderApplication[];
|
||||
transportId: string;
|
||||
}
|
||||
|
||||
|
||||
interface ReqBase {
|
||||
requestId: number;
|
||||
}
|
||||
|
||||
// NS: urn:x-cast:com.google.cast.receiver
|
||||
export type SenderMessage =
|
||||
ReqBase & { type: "LAUNCH", appId: string }
|
||||
| ReqBase & { type: "STOP", sessionId: string }
|
||||
| ReqBase & { type: "GET_STATUS" }
|
||||
| ReqBase & { type: "GET_APP_AVAILABILITY", appId: string[] }
|
||||
| ReqBase & { type: "SET_VOLUME", volume: Partial<Volume> };
|
||||
| (ReqBase & { type: "LAUNCH"; appId: string })
|
||||
| (ReqBase & { type: "STOP"; sessionId: string })
|
||||
| (ReqBase & { type: "GET_STATUS" })
|
||||
| (ReqBase & { type: "GET_APP_AVAILABILITY"; appId: string[] })
|
||||
| (ReqBase & { type: "SET_VOLUME"; volume: Partial<Volume> });
|
||||
|
||||
export type ReceiverMessage =
|
||||
ReqBase & { type: "RECEIVER_STATUS", status: ReceiverStatus }
|
||||
| ReqBase & { type: "LAUNCH_ERROR", reason: string }
|
||||
|
||||
| (ReqBase & { type: "RECEIVER_STATUS"; status: ReceiverStatus })
|
||||
| (ReqBase & { type: "LAUNCH_ERROR"; reason: string });
|
||||
|
||||
interface MediaReqBase extends ReqBase {
|
||||
mediaSessionId: number;
|
||||
@@ -90,84 +88,84 @@ interface MediaReqBase extends ReqBase {
|
||||
|
||||
// NS: urn:x-cast:com.google.cast.media
|
||||
export type SenderMediaMessage =
|
||||
| MediaReqBase & { type: "PLAY" }
|
||||
| MediaReqBase & { type: "PAUSE" }
|
||||
| MediaReqBase & { type: "MEDIA_GET_STATUS" }
|
||||
| MediaReqBase & { type: "STOP" }
|
||||
| MediaReqBase & { type: "MEDIA_SET_VOLUME", volume: Partial<Volume> }
|
||||
| MediaReqBase & { type: "SET_PLAYBACK_RATE" , playbackRate: number }
|
||||
| ReqBase & {
|
||||
type: "LOAD"
|
||||
, activeTrackIds: Nullable<number[]>
|
||||
, atvCredentials?: string
|
||||
, atvCredentialsType?: string
|
||||
, autoplay: Nullable<boolean>
|
||||
, currentTime: Nullable<number>
|
||||
, customData?: unknown
|
||||
, media: MediaInfo
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
| MediaReqBase & {
|
||||
type: "SEEK"
|
||||
, resumeState: Nullable<ResumeState>
|
||||
, currentTime: Nullable<number>
|
||||
}
|
||||
| MediaReqBase & {
|
||||
type: "EDIT_TRACKS_INFO"
|
||||
, activeTrackIds: Nullable<number[]>
|
||||
, textTrackStyle: Nullable<string>
|
||||
}
|
||||
// QueueLoadRequest
|
||||
| ReqBase & {
|
||||
type: "QUEUE_LOAD"
|
||||
, items: QueueItem[]
|
||||
, startIndex: number
|
||||
, repeatMode: string
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueInsertItemsRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_INSERT"
|
||||
, items: QueueItem[]
|
||||
, insertBefore: Nullable<number>
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueUpdateItemsRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_UPDATE"
|
||||
, items: QueueItem[]
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueJumpRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_UPDATE"
|
||||
, jump: Nullable<number>
|
||||
, currentItemId: Nullable<number>
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueRemoveItemsRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_REMOVE"
|
||||
, itemIds: number[]
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueReorderItemsRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_REORDER"
|
||||
, itemIds: number[]
|
||||
, insertBefore: Nullable<number>
|
||||
, sessionId: Nullable<string>
|
||||
}
|
||||
// QueueSetPropertiesRequest
|
||||
| MediaReqBase & {
|
||||
type: "QUEUE_UPDATE"
|
||||
, repeatMode: Nullable<string>
|
||||
, sessionId: Nullable<string>
|
||||
};
|
||||
| (MediaReqBase & { type: "PLAY" })
|
||||
| (MediaReqBase & { type: "PAUSE" })
|
||||
| (MediaReqBase & { type: "MEDIA_GET_STATUS" })
|
||||
| (MediaReqBase & { type: "STOP" })
|
||||
| (MediaReqBase & { type: "MEDIA_SET_VOLUME"; volume: Partial<Volume> })
|
||||
| (MediaReqBase & { type: "SET_PLAYBACK_RATE"; playbackRate: number })
|
||||
| (ReqBase & {
|
||||
type: "LOAD";
|
||||
activeTrackIds: Nullable<number[]>;
|
||||
atvCredentials?: string;
|
||||
atvCredentialsType?: string;
|
||||
autoplay: Nullable<boolean>;
|
||||
currentTime: Nullable<number>;
|
||||
customData?: unknown;
|
||||
media: MediaInfo;
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
| (MediaReqBase & {
|
||||
type: "SEEK";
|
||||
resumeState: Nullable<ResumeState>;
|
||||
currentTime: Nullable<number>;
|
||||
})
|
||||
| (MediaReqBase & {
|
||||
type: "EDIT_TRACKS_INFO";
|
||||
activeTrackIds: Nullable<number[]>;
|
||||
textTrackStyle: Nullable<string>;
|
||||
})
|
||||
// QueueLoadRequest
|
||||
| (ReqBase & {
|
||||
type: "QUEUE_LOAD";
|
||||
items: QueueItem[];
|
||||
startIndex: number;
|
||||
repeatMode: string;
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueInsertItemsRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_INSERT";
|
||||
items: QueueItem[];
|
||||
insertBefore: Nullable<number>;
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueUpdateItemsRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_UPDATE";
|
||||
items: QueueItem[];
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueJumpRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_UPDATE";
|
||||
jump: Nullable<number>;
|
||||
currentItemId: Nullable<number>;
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueRemoveItemsRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_REMOVE";
|
||||
itemIds: number[];
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueReorderItemsRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_REORDER";
|
||||
itemIds: number[];
|
||||
insertBefore: Nullable<number>;
|
||||
sessionId: Nullable<string>;
|
||||
})
|
||||
// QueueSetPropertiesRequest
|
||||
| (MediaReqBase & {
|
||||
type: "QUEUE_UPDATE";
|
||||
repeatMode: Nullable<string>;
|
||||
sessionId: Nullable<string>;
|
||||
});
|
||||
|
||||
export type ReceiverMediaMessage =
|
||||
MediaReqBase & { type: "MEDIA_STATUS", status: MediaStatus[] }
|
||||
| MediaReqBase & { type: "INVALID_PLAYER_STATE" }
|
||||
| MediaReqBase & { type: "LOAD_FAILED" }
|
||||
| MediaReqBase & { type: "LOAD_CANCELLED" }
|
||||
| MediaReqBase & { type: "INVALID_REQUEST" };
|
||||
| (MediaReqBase & { type: "MEDIA_STATUS"; status: MediaStatus[] })
|
||||
| (MediaReqBase & { type: "INVALID_PLAYER_STATE" })
|
||||
| (MediaReqBase & { type: "LOAD_FAILED" })
|
||||
| (MediaReqBase & { type: "LOAD_CANCELLED" })
|
||||
| (MediaReqBase & { type: "INVALID_REQUEST" });
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"use strict";
|
||||
|
||||
import { CAST_LOADER_SCRIPT_URL
|
||||
, CAST_SCRIPT_URLS } from "../lib/endpoints";
|
||||
import { CAST_LOADER_SCRIPT_URL, CAST_SCRIPT_URLS } from "../lib/endpoints";
|
||||
|
||||
|
||||
const _window = (window.wrappedJSObject as any);
|
||||
const _window = window.wrappedJSObject as any;
|
||||
|
||||
_window.chrome = cloneInto({}, window);
|
||||
|
||||
@@ -16,7 +14,6 @@ if (window.location.host === "www.youtube.com") {
|
||||
_window.navigator.presentation = cloneInto({}, window);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replace the src property setter on <script> elements to
|
||||
* intercept the new value.
|
||||
@@ -26,16 +23,16 @@ if (window.location.host === "www.youtube.com") {
|
||||
* which is handled in the main script.
|
||||
*/
|
||||
const desc = Reflect.getOwnPropertyDescriptor(
|
||||
HTMLScriptElement.prototype.wrappedJSObject, "src");
|
||||
HTMLScriptElement.prototype.wrappedJSObject,
|
||||
"src"
|
||||
);
|
||||
|
||||
Reflect.defineProperty(
|
||||
HTMLScriptElement.prototype.wrappedJSObject, "src", {
|
||||
Reflect.defineProperty(HTMLScriptElement.prototype.wrappedJSObject, "src", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: desc?.get,
|
||||
|
||||
configurable: true
|
||||
, enumerable: true
|
||||
, get: desc?.get
|
||||
|
||||
, set: exportFunction(function setFunc(this: HTMLScriptElement, value) {
|
||||
set: exportFunction(function setFunc(this: HTMLScriptElement, value) {
|
||||
if (CAST_SCRIPT_URLS.includes(value)) {
|
||||
return desc?.set?.call(this, CAST_LOADER_SCRIPT_URL);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { onMessageResponse, sendMessage } from "./eventMessageChannel";
|
||||
|
||||
import messaging, { Message } from "../messaging";
|
||||
|
||||
|
||||
// Message port to background script
|
||||
export const backgroundPort = messaging.connect({ name: "shim" });
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
import { Message } from "../messaging";
|
||||
|
||||
|
||||
type ListenerFunc = (message: Message) => void;
|
||||
|
||||
export interface ListenerObject {
|
||||
disconnect (): void;
|
||||
disconnect(): void;
|
||||
}
|
||||
|
||||
|
||||
export function onMessage(listener: ListenerFunc): ListenerObject {
|
||||
function on__castMessage(ev: CustomEvent) {
|
||||
listener(JSON.parse(ev.detail));
|
||||
@@ -26,16 +24,16 @@ export function onMessage(listener: ListenerFunc): ListenerObject {
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
document.addEventListener(
|
||||
"__castMessage"
|
||||
, on__castMessage, true);
|
||||
document.addEventListener("__castMessage", on__castMessage, true);
|
||||
|
||||
return {
|
||||
disconnect() {
|
||||
// @ts-ignore
|
||||
document.removeEventListener(
|
||||
"__castMessage"
|
||||
, on__castMessage, true);
|
||||
"__castMessage",
|
||||
on__castMessage,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -48,7 +46,6 @@ export function sendMessageResponse(message: Message) {
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
|
||||
export function onMessageResponse(listener: ListenerFunc): ListenerObject {
|
||||
function on__castMessageResponse(ev: CustomEvent) {
|
||||
listener(JSON.parse(ev.detail));
|
||||
@@ -56,15 +53,19 @@ export function onMessageResponse(listener: ListenerFunc): ListenerObject {
|
||||
|
||||
// @ts-ignore
|
||||
document.addEventListener(
|
||||
"__castMessageResponse"
|
||||
, on__castMessageResponse, true);
|
||||
"__castMessageResponse",
|
||||
on__castMessageResponse,
|
||||
true
|
||||
);
|
||||
|
||||
return {
|
||||
disconnect() {
|
||||
// @ts-ignore
|
||||
document.removeEventListener(
|
||||
"__castMessageResponse"
|
||||
, on__castMessageResponse, true);
|
||||
"__castMessageResponse",
|
||||
on__castMessageResponse,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import { Message } from "../messaging";
|
||||
import { BridgeInfo } from "../lib/bridge";
|
||||
import { TypedMessagePort } from "../lib/TypedMessagePort";
|
||||
|
||||
import { onMessage
|
||||
, onMessageResponse
|
||||
, sendMessage } from "./eventMessageChannel";
|
||||
|
||||
import {
|
||||
onMessage,
|
||||
onMessageResponse,
|
||||
sendMessage
|
||||
} from "./eventMessageChannel";
|
||||
|
||||
let initializedBridgeInfo: BridgeInfo;
|
||||
let initializedBackgroundPort: MessagePort;
|
||||
@@ -23,7 +24,6 @@ let initializedBackgroundPort: MessagePort;
|
||||
*/
|
||||
export function ensureInit(): Promise<TypedMessagePort<Message>> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
// If already initialized, just return existing bridge info
|
||||
if (initializedBridgeInfo) {
|
||||
if (initializedBridgeInfo.isVersionCompatible) {
|
||||
@@ -45,8 +45,9 @@ export function ensureInit(): Promise<TypedMessagePort<Message>> {
|
||||
* URL.
|
||||
*/
|
||||
if (window.location.protocol === "moz-extension:") {
|
||||
const { default: ShimManager } =
|
||||
await import("../background/ShimManager");
|
||||
const { default: ShimManager } = await import(
|
||||
"../background/ShimManager"
|
||||
);
|
||||
|
||||
// port2 will post bridge messages to port 1
|
||||
await ShimManager.init();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import logger from "../../lib/logger";
|
||||
|
||||
|
||||
/**
|
||||
* Custom element for a cast button used by sites that injects
|
||||
* a cast icon and manages visibility state and event handling.
|
||||
@@ -48,19 +47,31 @@ export default class GoogleCastLauncher extends HTMLElement {
|
||||
|
||||
iconArch1.classList.add("cast_caf_state_d");
|
||||
iconArch1.setAttribute("id", "cast_caf_icon_arch1");
|
||||
iconArch1.setAttribute("d", "M1 14v2c2.76 0 5 2.2 5 5h2c0-3.87-3.13-7-7-7z");
|
||||
iconArch1.setAttribute(
|
||||
"d",
|
||||
"M1 14v2c2.76 0 5 2.2 5 5h2c0-3.87-3.13-7-7-7z"
|
||||
);
|
||||
|
||||
iconArch2.classList.add("cast_caf_state_d");
|
||||
iconArch2.setAttribute("id", "cast_caf_icon_arch2");
|
||||
iconArch2.setAttribute("d", "M1 10v2c4.97 0 9 4 9 9h2c0-6.08-4.93-11-11-11z");
|
||||
iconArch2.setAttribute(
|
||||
"d",
|
||||
"M1 10v2c4.97 0 9 4 9 9h2c0-6.08-4.93-11-11-11z"
|
||||
);
|
||||
|
||||
iconBox.classList.add("cast_caf_state_d");
|
||||
iconBox.setAttribute("id", "cast_caf_icon_box");
|
||||
iconBox.setAttribute("d", "M21 3H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z");
|
||||
iconBox.setAttribute(
|
||||
"d",
|
||||
"M21 3H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
|
||||
);
|
||||
|
||||
iconBoxFill.classList.add("cast_caf_state_h");
|
||||
iconBoxFill.setAttribute("id", "cast_caf_icon_boxfill");
|
||||
iconBoxFill.setAttribute("d", "M5 7v1.63C8 8.6 13.37 14 13.37 17H19V7z");
|
||||
iconBoxFill.setAttribute(
|
||||
"d",
|
||||
"M5 7v1.63C8 8.6 13.37 14 13.37 17H19V7z"
|
||||
);
|
||||
|
||||
// Add icon paths to SVG
|
||||
icon.append(iconArch0, iconArch1, iconArch2, iconBox, iconBoxFill);
|
||||
@@ -68,7 +79,6 @@ export default class GoogleCastLauncher extends HTMLElement {
|
||||
const shadow = this.attachShadow({ mode: "open" });
|
||||
shadow.append(icon, style);
|
||||
|
||||
|
||||
this.addEventListener("click", () => {
|
||||
logger.info("<google-cast-launcher> onClick");
|
||||
});
|
||||
|
||||
@@ -4,11 +4,8 @@ import EventData from "./EventData";
|
||||
|
||||
import { SessionEventType } from "../enums";
|
||||
|
||||
|
||||
export default class ActiveInputStateEventData extends EventData {
|
||||
constructor(
|
||||
public activeInputState: number) {
|
||||
|
||||
constructor(public activeInputState: number) {
|
||||
super(SessionEventType.ACTIVE_INPUT_STATE_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import * as cast from "../../cast";
|
||||
|
||||
|
||||
export default class ApplicationMetadata {
|
||||
public applicationId: string;
|
||||
public images: cast.Image[];
|
||||
@@ -16,6 +15,7 @@ export default class ApplicationMetadata {
|
||||
|
||||
// Convert [{ name: <ns> }, ...] to [ <ns>, ... ]
|
||||
this.namespaces = sessionObj.namespaces.map(
|
||||
namespaceObj => namespaceObj.name);
|
||||
namespaceObj => namespaceObj.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,8 @@ import EventData from "./EventData";
|
||||
|
||||
import { SessionEventType } from "../enums";
|
||||
|
||||
|
||||
export default class ApplicationMetadataEventData extends EventData {
|
||||
constructor(
|
||||
public metadata: ApplicationMetadata) {
|
||||
|
||||
constructor(public metadata: ApplicationMetadata) {
|
||||
super(SessionEventType.APPLICATION_METADATA_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,8 @@ import EventData from "./EventData";
|
||||
|
||||
import { SessionEventType } from "../enums";
|
||||
|
||||
|
||||
export default class ApplicationStatusEventData extends EventData {
|
||||
constructor(
|
||||
public status: string) {
|
||||
|
||||
constructor(public status: string) {
|
||||
super(SessionEventType.APPLICATION_STATUS_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import logger from "../../../lib/logger";
|
||||
import CastOptions from "./CastOptions";
|
||||
import CastSession from "./CastSession";
|
||||
|
||||
|
||||
export default class CastContext extends EventTarget {
|
||||
public endCurrentSession(_stopCasting: boolean): void {
|
||||
logger.info("STUB :: CastContext#endCurrentSession");
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
import * as cast from "../../cast";
|
||||
|
||||
|
||||
export default class CastOptions {
|
||||
public autoJoinPolicy: string = cast.AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED;
|
||||
public language: (string | null) = null;
|
||||
public receiverApplicationId: (string | null) = null;
|
||||
public language: string | null = null;
|
||||
public receiverApplicationId: string | null = null;
|
||||
public resumeSavedSession = true;
|
||||
|
||||
constructor(options: CastOptions = ({} as CastOptions)) {
|
||||
constructor(options: CastOptions = {} as CastOptions) {
|
||||
if (options.autoJoinPolicy) {
|
||||
this.autoJoinPolicy = options.autoJoinPolicy;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ import * as cast from "../../cast";
|
||||
|
||||
import ApplicationMetadata from "./ApplicationMetadata";
|
||||
|
||||
|
||||
type MessageListener = (namespace: string, message: string) => void;
|
||||
|
||||
|
||||
export default class CastSession extends EventTarget {
|
||||
constructor(_sessionObj: cast.Session, _state: string) {
|
||||
super();
|
||||
@@ -17,9 +15,9 @@ export default class CastSession extends EventTarget {
|
||||
}
|
||||
|
||||
public addMessageListener(
|
||||
_namespace: string
|
||||
, _listener: MessageListener): void {
|
||||
|
||||
_namespace: string,
|
||||
_listener: MessageListener
|
||||
): void {
|
||||
logger.info("STUB :: CastSession#addMessageListener");
|
||||
}
|
||||
|
||||
@@ -83,17 +81,17 @@ export default class CastSession extends EventTarget {
|
||||
}
|
||||
|
||||
public removeMessageListener(
|
||||
_namespace: string
|
||||
, _listener: MessageListener): void {
|
||||
|
||||
_namespace: string,
|
||||
_listener: MessageListener
|
||||
): void {
|
||||
logger.info("STUB :: CastSession#removeMessageListener");
|
||||
}
|
||||
|
||||
public sendMessage(
|
||||
_namespace: string
|
||||
// @ts-ignore
|
||||
, _data: any): Promise<string> {
|
||||
|
||||
_namespace: string,
|
||||
// @ts-ignore
|
||||
_data: any
|
||||
): Promise<string> {
|
||||
logger.info("STUB :: CastSession#sendMessage");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,8 @@ import EventData from "./EventData";
|
||||
|
||||
import { CastContextEventType } from "../enums";
|
||||
|
||||
|
||||
export default class CastStateEventData extends EventData {
|
||||
constructor(
|
||||
public castState: string) {
|
||||
|
||||
constructor(public castState: string) {
|
||||
super(CastContextEventType.CAST_STATE_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
export default class EventData {
|
||||
constructor(
|
||||
public type: string) {}
|
||||
constructor(public type: string) {}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,8 @@ import EventData from "./EventData";
|
||||
|
||||
import { SessionEventType } from "../enums";
|
||||
|
||||
|
||||
export default class MediaSessionEventData extends EventData {
|
||||
constructor(
|
||||
public mediaSession: cast.media.Media) {
|
||||
|
||||
constructor(public mediaSession: cast.media.Media) {
|
||||
super(SessionEventType.MEDIA_SESSION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import * as cast from "../../cast";
|
||||
|
||||
import RemotePlayerController from "./RemotePlayerController";
|
||||
|
||||
|
||||
interface SavedPlayerState {
|
||||
mediaInfo: string;
|
||||
currentTime: number;
|
||||
@@ -15,19 +14,19 @@ export default class RemotePlayer {
|
||||
public canControlVolume = false;
|
||||
public canPause = false;
|
||||
public canSeek = false;
|
||||
public controller: (RemotePlayerController | null) = null;
|
||||
public controller: RemotePlayerController | null = null;
|
||||
public currentTime = 0;
|
||||
public displayName = "";
|
||||
public displayStatus = "";
|
||||
public duration = 0;
|
||||
public imageUrl: (string | null) = null;
|
||||
public imageUrl: string | null = null;
|
||||
public isConnected = false;
|
||||
public isMediaLoaded = false;
|
||||
public isMuted = false;
|
||||
public isPaused = false;
|
||||
public mediaInfo: (cast.media.MediaInfo | null) = null;
|
||||
public playerState: (string | null) = null;
|
||||
public savedPlayerState: (SavedPlayerState | null) = null;
|
||||
public mediaInfo: cast.media.MediaInfo | null = null;
|
||||
public playerState: string | null = null;
|
||||
public savedPlayerState: SavedPlayerState | null = null;
|
||||
public statusText = "";
|
||||
public title = "";
|
||||
public volumeLevel = 1;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
export default class RemotePlayerChangedEvent {
|
||||
constructor(
|
||||
public type: string
|
||||
, public field: string
|
||||
, public value: any) {}
|
||||
constructor(public type: string, public field: string, public value: any) {}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import logger from "../../../lib/logger";
|
||||
|
||||
import RemotePlayer from "./RemotePlayer";
|
||||
|
||||
|
||||
export default class RemotePlayerController extends EventTarget {
|
||||
constructor(_player: RemotePlayer) {
|
||||
super();
|
||||
@@ -16,7 +15,7 @@ export default class RemotePlayerController extends EventTarget {
|
||||
const minutes = Math.floor(timeInSec / 60) % 60;
|
||||
const seconds = timeInSec % 60;
|
||||
|
||||
return [ hours, minutes, seconds ]
|
||||
return [hours, minutes, seconds]
|
||||
.map(c => c.toString().padStart(2, "0"))
|
||||
.join(":");
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@ import EventData from "./EventData";
|
||||
|
||||
import { SessionEventType } from "../enums";
|
||||
|
||||
|
||||
export default class SessionStateEventData extends EventData {
|
||||
constructor(
|
||||
public session: CastSession
|
||||
, public sessionState: string
|
||||
, public errorCode: (string | null) = null) {
|
||||
|
||||
public session: CastSession,
|
||||
public sessionState: string,
|
||||
public errorCode: string | null = null
|
||||
) {
|
||||
super(SessionEventType.APPLICATION_STATUS_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
import { SessionEventType } from "../enums";
|
||||
|
||||
|
||||
export default class VolumeEventData {
|
||||
public type = SessionEventType.VOLUME_CHANGED;
|
||||
|
||||
constructor(
|
||||
public volume: number
|
||||
, public isMute: boolean) {}
|
||||
constructor(public volume: number, public isMute: boolean) {}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
"use strict";
|
||||
|
||||
export enum ActiveInputState {
|
||||
ACTIVE_INPUT_STATE_UNKNOWN = -1
|
||||
, ACTIVE_INPUT_STATE_NO = 0
|
||||
, ACTIVE_INPUT_YES = 1
|
||||
ACTIVE_INPUT_STATE_UNKNOWN = -1,
|
||||
ACTIVE_INPUT_STATE_NO = 0,
|
||||
ACTIVE_INPUT_YES = 1
|
||||
}
|
||||
|
||||
export enum CastContextEventType {
|
||||
CAST_STATE_CHANGED = "caststatechanged"
|
||||
, SESSION_STATE_CHANGED = "sessionstatechanged"
|
||||
CAST_STATE_CHANGED = "caststatechanged",
|
||||
SESSION_STATE_CHANGED = "sessionstatechanged"
|
||||
}
|
||||
|
||||
export enum CastState {
|
||||
NO_DEVICES_AVAILABLE = "NO_DEVICES_AVAILABLE"
|
||||
, NOT_CONNECTED = "NOT_CONNECTED"
|
||||
, CONNECTING = "CONNECTING"
|
||||
, CONNECTED = "CONNECTED"
|
||||
NO_DEVICES_AVAILABLE = "NO_DEVICES_AVAILABLE",
|
||||
NOT_CONNECTED = "NOT_CONNECTED",
|
||||
CONNECTING = "CONNECTING",
|
||||
CONNECTED = "CONNECTED"
|
||||
}
|
||||
|
||||
export enum LoggerLevel {
|
||||
DEBUG = 0
|
||||
, INFO = 800
|
||||
, WARNING = 900
|
||||
, ERROR = 1000
|
||||
, NONE = 1500
|
||||
DEBUG = 0,
|
||||
INFO = 800,
|
||||
WARNING = 900,
|
||||
ERROR = 1000,
|
||||
NONE = 1500
|
||||
}
|
||||
|
||||
export enum RemotePlayerEventType {
|
||||
ANY_CHANGE = "anyChanged"
|
||||
, IS_CONNECTED_CHANGE = "isConnectedChanged"
|
||||
, IS_MEDIA_LOADED_CHANGED = "isMediaLoadedChanged"
|
||||
, DURATION_CHANGED = "durationChanged"
|
||||
, CURRENT_TIME_CHANGED = "currentTimeChanged"
|
||||
, IS_PAUSED_CHANGED = "isPausedChanged"
|
||||
, VOLUME_LEVEL_CHANGED = "volumeLevelChanged"
|
||||
, CAN_CONTROL_VOLUME_CHANGED = "canControlVolumeChanged"
|
||||
, IS_MUTED_CHANGED = "isMutedChanged"
|
||||
, CAN_PAUSE_CHANGED = "canPauseChanged"
|
||||
, CAN_SEEK_CHANGED = "canSeekChanged"
|
||||
, DISPLAY_NAME_CHANGED = "displayNameChanged"
|
||||
, STATUS_TEXT_CHANGED = "statusTextChanged"
|
||||
, MEDIA_INFO_CHANGED = "mediaInfoChanged"
|
||||
, IMAGE_URL_CHANGED = "imageUrlChanged"
|
||||
, PLAYER_STATE_CHANGED = "playerStateChanged"
|
||||
ANY_CHANGE = "anyChanged",
|
||||
IS_CONNECTED_CHANGE = "isConnectedChanged",
|
||||
IS_MEDIA_LOADED_CHANGED = "isMediaLoadedChanged",
|
||||
DURATION_CHANGED = "durationChanged",
|
||||
CURRENT_TIME_CHANGED = "currentTimeChanged",
|
||||
IS_PAUSED_CHANGED = "isPausedChanged",
|
||||
VOLUME_LEVEL_CHANGED = "volumeLevelChanged",
|
||||
CAN_CONTROL_VOLUME_CHANGED = "canControlVolumeChanged",
|
||||
IS_MUTED_CHANGED = "isMutedChanged",
|
||||
CAN_PAUSE_CHANGED = "canPauseChanged",
|
||||
CAN_SEEK_CHANGED = "canSeekChanged",
|
||||
DISPLAY_NAME_CHANGED = "displayNameChanged",
|
||||
STATUS_TEXT_CHANGED = "statusTextChanged",
|
||||
MEDIA_INFO_CHANGED = "mediaInfoChanged",
|
||||
IMAGE_URL_CHANGED = "imageUrlChanged",
|
||||
PLAYER_STATE_CHANGED = "playerStateChanged"
|
||||
}
|
||||
|
||||
export enum SessionEventType {
|
||||
APPLICATION_STATUS_CHANGED = "applicationstatuschanged"
|
||||
, APPLICATION_METADATA_CHANGED = "applicationmetadatachanged"
|
||||
, ACTIVE_INPUT_STATE_CHANGED = "activeinputstatechanged"
|
||||
, VOLUME_CHANGED = "volumechanged"
|
||||
, MEDIA_SESSION = "mediasession"
|
||||
APPLICATION_STATUS_CHANGED = "applicationstatuschanged",
|
||||
APPLICATION_METADATA_CHANGED = "applicationmetadatachanged",
|
||||
ACTIVE_INPUT_STATE_CHANGED = "activeinputstatechanged",
|
||||
VOLUME_CHANGED = "volumechanged",
|
||||
MEDIA_SESSION = "mediasession"
|
||||
}
|
||||
|
||||
export enum SessionState {
|
||||
NO_SESSION = "NO_SESSION"
|
||||
, SESSION_STARTING = "SESSION_STARTING"
|
||||
, SESSION_STARTED = "SESSION_STARTED"
|
||||
, SESSION_START_FAILED = "SESSION_START_FAILED"
|
||||
, SESSION_ENDING = "SESSION_ENDING"
|
||||
, SESSION_ENDED = "SESSION_ENDED"
|
||||
, SESSION_RESUMED = "SESSION_RESUMED"
|
||||
NO_SESSION = "NO_SESSION",
|
||||
SESSION_STARTING = "SESSION_STARTING",
|
||||
SESSION_STARTED = "SESSION_STARTED",
|
||||
SESSION_START_FAILED = "SESSION_START_FAILED",
|
||||
SESSION_ENDING = "SESSION_ENDING",
|
||||
SESSION_ENDED = "SESSION_ENDED",
|
||||
SESSION_RESUMED = "SESSION_RESUMED"
|
||||
}
|
||||
|
||||
@@ -18,49 +18,63 @@ import RemotePlayerController from "./classes/RemotePlayerController";
|
||||
import SessionStateEventData from "./classes/SessionStateEventData";
|
||||
import VolumeEventData from "./classes/VolumeEventData";
|
||||
|
||||
import { ActiveInputState
|
||||
, CastContextEventType
|
||||
, CastState
|
||||
, LoggerLevel
|
||||
, RemotePlayerEventType
|
||||
, SessionEventType
|
||||
, SessionState } from "./enums";
|
||||
import {
|
||||
ActiveInputState,
|
||||
CastContextEventType,
|
||||
CastState,
|
||||
LoggerLevel,
|
||||
RemotePlayerEventType,
|
||||
SessionEventType,
|
||||
SessionState
|
||||
} from "./enums";
|
||||
|
||||
import GoogleCastLauncher from "./GoogleCastLauncher";
|
||||
|
||||
|
||||
export default {
|
||||
// Enums
|
||||
ActiveInputState, CastContextEventType, CastState, LoggerLevel
|
||||
, RemotePlayerEventType, SessionEventType, SessionState
|
||||
ActiveInputState,
|
||||
CastContextEventType,
|
||||
CastState,
|
||||
LoggerLevel,
|
||||
RemotePlayerEventType,
|
||||
SessionEventType,
|
||||
SessionState,
|
||||
|
||||
// Classes
|
||||
, ActiveInputStateEventData, ApplicationMetadata
|
||||
, ApplicationMetadataEventData, ApplicationStatusEventData, CastOptions
|
||||
, CastSession, CastStateEventData, EventData, MediaSessionEventData
|
||||
, RemotePlayer, RemotePlayerChangedEvent, RemotePlayerController
|
||||
, SessionStateEventData, VolumeEventData
|
||||
ActiveInputStateEventData,
|
||||
ApplicationMetadata,
|
||||
ApplicationMetadataEventData,
|
||||
ApplicationStatusEventData,
|
||||
CastOptions,
|
||||
CastSession,
|
||||
CastStateEventData,
|
||||
EventData,
|
||||
MediaSessionEventData,
|
||||
RemotePlayer,
|
||||
RemotePlayerChangedEvent,
|
||||
RemotePlayerController,
|
||||
SessionStateEventData,
|
||||
VolumeEventData,
|
||||
|
||||
/**
|
||||
* CastContext class with an extra getInstance method used to
|
||||
* instantiate and fetch a singleton instance.
|
||||
*/
|
||||
, CastContext: {
|
||||
...CastContext
|
||||
CastContext: {
|
||||
...CastContext,
|
||||
|
||||
, getInstance() {
|
||||
getInstance() {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
, VERSION: "1.0.07"
|
||||
VERSION: "1.0.07",
|
||||
|
||||
, setLoggerLevel(_level: number) {
|
||||
setLoggerLevel(_level: number) {
|
||||
logger.info("STUB :: cast.framework.setLoggerLevel");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The Framework API defines a <google-cast-launcher> element
|
||||
* and a <button is="google-cast-button"> element extension,
|
||||
|
||||
@@ -6,18 +6,15 @@ import { CAST_FRAMEWORK_SCRIPT_URL } from "../lib/endpoints";
|
||||
import { loadScript } from "../lib/utils";
|
||||
import { onMessage } from "./eventMessageChannel";
|
||||
|
||||
|
||||
const _window = (window as any);
|
||||
const _window = window as any;
|
||||
|
||||
if (!_window.chrome) {
|
||||
_window.chrome = {};
|
||||
}
|
||||
|
||||
|
||||
// Create page-accessible API object
|
||||
_window.chrome.cast = cast;
|
||||
|
||||
|
||||
let bridgeInfo: any;
|
||||
let isFramework = false;
|
||||
|
||||
@@ -29,14 +26,13 @@ function callPageReadyFunction() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If loaded within a page via a <script> element,
|
||||
* document.currentScript should exist and we can check its
|
||||
* [src] query string for the loadCastFramework param.
|
||||
*/
|
||||
if (document.currentScript) {
|
||||
const currentScript = (document.currentScript as HTMLScriptElement);
|
||||
const currentScript = document.currentScript as HTMLScriptElement;
|
||||
const currentScriptUrl = new URL(currentScript.src);
|
||||
const currentScriptParams = new URLSearchParams(currentScriptUrl.search);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { Error as Error_ } from "./cast/dataClasses";
|
||||
import { Media } from "./cast/media";
|
||||
|
||||
|
||||
export type SuccessCallback = () => void;
|
||||
export type ErrorCallback = (err: Error_) => void;
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { ReceiverStatus } from "./shim/cast/types";
|
||||
|
||||
export interface ReceiverDevice {
|
||||
host: string
|
||||
friendlyName: string
|
||||
, id: string
|
||||
, port: number
|
||||
, status?: ReceiverStatus
|
||||
host: string;
|
||||
friendlyName: string;
|
||||
id: string;
|
||||
port: number;
|
||||
status?: ReceiverStatus;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user