mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-11 18:19:58 +00:00
prettier: Re-format .ts files
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
"tabWidth": 4,
|
"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" {
|
declare module "bplist-creator" {
|
||||||
import Buffer from "buffer";
|
import Buffer from "buffer";
|
||||||
|
|
||||||
function bplist (dicts: object): Buffer;
|
function bplist(dicts: object): Buffer;
|
||||||
export = bplist;
|
export = bplist;
|
||||||
|
|
||||||
namespace bplist {
|
namespace bplist {
|
||||||
export class Real {
|
export class Real {
|
||||||
public value: number;
|
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 var maxObjectCount: number;
|
||||||
|
|
||||||
export class UID {
|
export class UID {
|
||||||
constructor (id: number);
|
constructor(id: number);
|
||||||
UID: number;
|
UID: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParseFileCallback = (
|
type ParseFileCallback = (err: string, result?: Buffer) => void;
|
||||||
err: string
|
|
||||||
, result?: Buffer) => void;
|
|
||||||
|
|
||||||
export function parseFile (
|
export function parseFile(
|
||||||
fileNameOrBuffer: Buffer | string
|
fileNameOrBuffer: Buffer | string,
|
||||||
, callback: ParseFileCallback): void;
|
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;
|
type CallbackFunction = () => void;
|
||||||
|
|
||||||
|
|
||||||
export interface Channel extends EventEmitter {
|
export interface Channel extends EventEmitter {
|
||||||
bus: Client;
|
bus: Client;
|
||||||
sourceId: string;
|
sourceId: string;
|
||||||
@@ -18,51 +17,55 @@ declare module "castv2" {
|
|||||||
namespace: string;
|
namespace: string;
|
||||||
encoding: string;
|
encoding: string;
|
||||||
|
|
||||||
send (data: any): void;
|
send(data: any): void;
|
||||||
close (): void;
|
close(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceAuthMessage {
|
export interface DeviceAuthMessage {
|
||||||
parse (data: any): any;
|
parse(data: any): any;
|
||||||
serialize (data: any): any;
|
serialize(data: any): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Client extends EventEmitter {
|
export class Client extends EventEmitter {
|
||||||
public connect (
|
public connect(
|
||||||
options: ClientConnectOptions | string
|
options: ClientConnectOptions | string,
|
||||||
, callback?: CallbackFunction): void;
|
callback?: CallbackFunction
|
||||||
|
): void;
|
||||||
|
|
||||||
public close (): void;
|
public close(): void;
|
||||||
|
|
||||||
public send (
|
public send(
|
||||||
sourceId: string
|
sourceId: string,
|
||||||
, destinationId: string
|
destinationId: string,
|
||||||
, namespace: string
|
namespace: string,
|
||||||
, data: Buffer | string): void;
|
data: Buffer | string
|
||||||
|
): void;
|
||||||
|
|
||||||
public createChannel (
|
public createChannel(
|
||||||
sourceId: string
|
sourceId: string,
|
||||||
, destinationId: string
|
destinationId: string,
|
||||||
, namespace: string
|
namespace: string,
|
||||||
, encoding: string): Channel;
|
encoding: string
|
||||||
|
): Channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Server extends EventEmitter {
|
export class Server extends EventEmitter {
|
||||||
constructor (options: object);
|
constructor(options: object);
|
||||||
|
|
||||||
public listen (
|
public listen(
|
||||||
port: number
|
port: number,
|
||||||
, host: string
|
host: string,
|
||||||
, callback?: CallbackFunction): void;
|
callback?: CallbackFunction
|
||||||
|
): void;
|
||||||
|
|
||||||
public send (
|
public send(
|
||||||
clientId: string
|
clientId: string,
|
||||||
, sourceId: string
|
sourceId: string,
|
||||||
, destinationId: string
|
destinationId: string,
|
||||||
, namespace: string
|
namespace: string,
|
||||||
, data: Buffer | string): void;
|
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 };
|
export const params: { [key: number]: Param };
|
||||||
|
|
||||||
type GenKeyCallback = (
|
type GenKeyCallback = (err: string, buf: Buffer) => void;
|
||||||
err: string
|
|
||||||
, buf: Buffer) => void;
|
|
||||||
|
|
||||||
export function genKey (
|
export function genKey(bytes: number, callback: GenKeyCallback): void;
|
||||||
bytes: number
|
|
||||||
, callback: GenKeyCallback): void;
|
|
||||||
|
|
||||||
export function computeVerifier (
|
export function computeVerifier(
|
||||||
params: object
|
params: object,
|
||||||
, salt: Buffer
|
salt: Buffer,
|
||||||
, I: Buffer
|
I: Buffer,
|
||||||
, P: Buffer): Buffer;
|
P: Buffer
|
||||||
|
): Buffer;
|
||||||
|
|
||||||
export class Client {
|
export class Client {
|
||||||
constructor (
|
constructor(
|
||||||
params: object
|
params: object,
|
||||||
, salt_buf: Buffer
|
salt_buf: Buffer,
|
||||||
, identity_buf: Buffer
|
identity_buf: Buffer,
|
||||||
, password_buf: Buffer
|
password_buf: Buffer,
|
||||||
, secret1_buf: Buffer);
|
secret1_buf: Buffer
|
||||||
|
);
|
||||||
|
|
||||||
computeA (): Buffer;
|
computeA(): Buffer;
|
||||||
setB(B_buf: Buffer): void;
|
setB(B_buf: Buffer): void;
|
||||||
computeM1 (): Buffer;
|
computeM1(): Buffer;
|
||||||
checkM2 (serverM2_buf: Buffer): void;
|
checkM2(serverM2_buf: Buffer): void;
|
||||||
computeK (): Buffer;
|
computeK(): Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Server {
|
export class Server {
|
||||||
constructor (
|
constructor(params: object, verifier_buf: Buffer, secret2_buf: Buffer);
|
||||||
params: object
|
|
||||||
, verifier_buf: Buffer
|
|
||||||
, secret2_buf: Buffer);
|
|
||||||
|
|
||||||
computeB (): Buffer;
|
computeB(): Buffer;
|
||||||
setA (A_buf: Buffer): void;
|
setA(A_buf: Buffer): void;
|
||||||
checkM1 (clientM1_buf: Buffer): Buffer;
|
checkM1(clientM1_buf: Buffer): Buffer;
|
||||||
computeK (): Buffer;
|
computeK(): Buffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import fetch, { Headers } from "node-fetch";
|
|||||||
import nacl from "tweetnacl";
|
import nacl from "tweetnacl";
|
||||||
import bplist from "./bplist";
|
import bplist from "./bplist";
|
||||||
|
|
||||||
|
|
||||||
const AIRPLAY_PORT = 7000;
|
const AIRPLAY_PORT = 7000;
|
||||||
const MIMETYPE_BPLIST = "application/x-apple-binary-plist";
|
const MIMETYPE_BPLIST = "application/x-apple-binary-plist";
|
||||||
|
|
||||||
@@ -27,10 +26,10 @@ export class AirPlayAuthCredentials {
|
|||||||
public clientPk: Uint8Array;
|
public clientPk: Uint8Array;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
clientId?: string
|
clientId?: string,
|
||||||
, clientSk?: Uint8Array
|
clientSk?: Uint8Array,
|
||||||
, clientPk?: Uint8Array) {
|
clientPk?: Uint8Array
|
||||||
|
) {
|
||||||
if (clientId && clientSk && clientPk) {
|
if (clientId && clientSk && clientPk) {
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.clientSk = clientSk;
|
this.clientSk = clientSk;
|
||||||
@@ -74,8 +73,7 @@ export class AirPlayAuth {
|
|||||||
*/
|
*/
|
||||||
public async finishPairing(pin: string) {
|
public async finishPairing(pin: string) {
|
||||||
// Stage 1 response
|
// Stage 1 response
|
||||||
const { pk: serverPk
|
const { pk: serverPk, salt: serverSalt } = await this.pairSetupPin1();
|
||||||
, salt: serverSalt } = await this.pairSetupPin1();
|
|
||||||
|
|
||||||
// SRP params must 2048-bit SHA1
|
// SRP params must 2048-bit SHA1
|
||||||
const srpParams = srp6a.params[2048];
|
const srpParams = srp6a.params[2048];
|
||||||
@@ -83,19 +81,21 @@ export class AirPlayAuth {
|
|||||||
|
|
||||||
// Create SRP client
|
// Create SRP client
|
||||||
const srpClient = new srp6a.Client(
|
const srpClient = new srp6a.Client(
|
||||||
srpParams // Params
|
srpParams, // Params
|
||||||
, serverSalt // Receiver salt
|
serverSalt, // Receiver salt
|
||||||
, Buffer.from(this.credentials.clientId) // Username
|
Buffer.from(this.credentials.clientId), // Username
|
||||||
, Buffer.from(pin) // Password (receiver pin)
|
Buffer.from(pin), // Password (receiver pin)
|
||||||
, Buffer.from(this.credentials.clientSk)); // Client secret key
|
Buffer.from(this.credentials.clientSk) // Client secret key
|
||||||
|
);
|
||||||
|
|
||||||
// Add receiver's public key
|
// Add receiver's public key
|
||||||
srpClient.setB(serverPk);
|
srpClient.setB(serverPk);
|
||||||
|
|
||||||
// Stage 2 response
|
// Stage 2 response
|
||||||
await this.pairSetupPin2(
|
await this.pairSetupPin2(
|
||||||
srpClient.computeA() // SRP public key
|
srpClient.computeA(), // SRP public key
|
||||||
, srpClient.computeM1()); // SRP proof
|
srpClient.computeM1() // SRP proof
|
||||||
|
);
|
||||||
|
|
||||||
// Stage 3 response
|
// Stage 3 response
|
||||||
await this.pairSetupPin3(srpClient.computeK());
|
await this.pairSetupPin3(srpClient.computeK());
|
||||||
@@ -108,12 +108,10 @@ export class AirPlayAuth {
|
|||||||
* its public key / salt.
|
* its public key / salt.
|
||||||
*/
|
*/
|
||||||
public async pairSetupPin1(): Promise<any> {
|
public async pairSetupPin1(): Promise<any> {
|
||||||
const [ response ] = await this.sendPostRequestBplist(
|
const [response] = await this.sendPostRequestBplist("/pair-setup-pin", {
|
||||||
"/pair-setup-pin"
|
method: "pin",
|
||||||
, {
|
user: this.credentials.clientId
|
||||||
method: "pin"
|
});
|
||||||
, user: this.credentials.clientId
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -125,13 +123,11 @@ export class AirPlayAuth {
|
|||||||
* public keys, sending them to the receiver and receiving its
|
* public keys, sending them to the receiver and receiving its
|
||||||
* proof.
|
* proof.
|
||||||
*/
|
*/
|
||||||
public async pairSetupPin2(
|
public async pairSetupPin2(pk: Buffer, proof: Buffer): Promise<any> {
|
||||||
pk: Buffer
|
const [response] = await this.sendPostRequestBplist("/pair-setup-pin", {
|
||||||
, proof: Buffer): Promise<any> {
|
pk,
|
||||||
|
proof
|
||||||
const [ response ] = await this.sendPostRequestBplist(
|
});
|
||||||
"/pair-setup-pin"
|
|
||||||
, { pk, proof });
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -144,17 +140,19 @@ export class AirPlayAuth {
|
|||||||
* responds confirming the pairing is complete.
|
* responds confirming the pairing is complete.
|
||||||
*/
|
*/
|
||||||
public async pairSetupPin3(
|
public async pairSetupPin3(
|
||||||
sharedSecretHash: crypto.BinaryLike): Promise<any> {
|
sharedSecretHash: crypto.BinaryLike
|
||||||
|
): Promise<any> {
|
||||||
// Create AES key
|
// Create AES key
|
||||||
const aesKey = crypto.createHash("sha512")
|
const aesKey = crypto
|
||||||
|
.createHash("sha512")
|
||||||
.update("Pair-Setup-AES-Key")
|
.update("Pair-Setup-AES-Key")
|
||||||
.update(sharedSecretHash)
|
.update(sharedSecretHash)
|
||||||
.digest()
|
.digest()
|
||||||
.slice(0, 16);
|
.slice(0, 16);
|
||||||
|
|
||||||
// Create AES IV
|
// Create AES IV
|
||||||
const aesIv = crypto.createHash("sha512")
|
const aesIv = crypto
|
||||||
|
.createHash("sha512")
|
||||||
.update("Pair-Setup-AES-IV")
|
.update("Pair-Setup-AES-IV")
|
||||||
.update(sharedSecretHash)
|
.update(sharedSecretHash)
|
||||||
.digest()
|
.digest()
|
||||||
@@ -162,7 +160,6 @@ export class AirPlayAuth {
|
|||||||
|
|
||||||
aesIv[15]++;
|
aesIv[15]++;
|
||||||
|
|
||||||
|
|
||||||
const cipher = crypto.createCipheriv("aes-128-gcm", aesKey, aesIv);
|
const cipher = crypto.createCipheriv("aes-128-gcm", aesKey, aesIv);
|
||||||
|
|
||||||
// Encode client public key
|
// Encode client public key
|
||||||
@@ -170,23 +167,23 @@ export class AirPlayAuth {
|
|||||||
cipher.final();
|
cipher.final();
|
||||||
const authTag = cipher.getAuthTag();
|
const authTag = cipher.getAuthTag();
|
||||||
|
|
||||||
const [ response ] = await this.sendPostRequestBplist(
|
const [response] = await this.sendPostRequestBplist("/pair-setup-pin", {
|
||||||
"/pair-setup-pin"
|
epk,
|
||||||
, { epk, authTag });
|
authTag
|
||||||
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a POST request to receiver and returns the
|
* Sends a POST request to receiver and returns the
|
||||||
* response.
|
* response.
|
||||||
*/
|
*/
|
||||||
public async sendPostRequest(
|
public async sendPostRequest(
|
||||||
path: string
|
path: string,
|
||||||
, contentType?: string
|
contentType?: string,
|
||||||
, data?: Buffer | string): Promise<any> {
|
data?: Buffer | string
|
||||||
|
): Promise<any> {
|
||||||
// Create URL from base receiver URL and path
|
// Create URL from base receiver URL and path
|
||||||
const requestUrl = new URL(path, this.baseUrl);
|
const requestUrl = new URL(path, this.baseUrl);
|
||||||
|
|
||||||
@@ -200,9 +197,9 @@ export class AirPlayAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(requestUrl.href, {
|
const response = await fetch(requestUrl.href, {
|
||||||
method: "POST"
|
method: "POST",
|
||||||
, headers: requestHeaders
|
headers: requestHeaders,
|
||||||
, body: data
|
body: data
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -217,16 +214,17 @@ export class AirPlayAuth {
|
|||||||
* receiver, then decodes and returns the response.
|
* receiver, then decodes and returns the response.
|
||||||
*/
|
*/
|
||||||
public async sendPostRequestBplist(
|
public async sendPostRequestBplist(
|
||||||
path: string
|
path: string,
|
||||||
, data?: object): Promise<any> {
|
data?: object
|
||||||
|
): Promise<any> {
|
||||||
// Convert data to compatible type
|
// Convert data to compatible type
|
||||||
const requestBody = data
|
const requestBody = data ? bplist.create(data) : undefined;
|
||||||
? bplist.create(data)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const response = await this.sendPostRequest(
|
const response = await this.sendPostRequest(
|
||||||
path, MIMETYPE_BPLIST, requestBody);
|
path,
|
||||||
|
MIMETYPE_BPLIST,
|
||||||
|
requestBody
|
||||||
|
);
|
||||||
|
|
||||||
// Convert response data to Buffer for bplist-parser
|
// Convert response data to Buffer for bplist-parser
|
||||||
return bplist.parse.parseBuffer(response);
|
return bplist.parse.parseBuffer(response);
|
||||||
|
|||||||
@@ -7,14 +7,12 @@ import { sendMessage } from "../../lib/nativeMessaging";
|
|||||||
import { ReceiverDevice } from "../../types";
|
import { ReceiverDevice } from "../../types";
|
||||||
import { ReceiverApplication, ReceiverMessage, SenderMessage } from "./types";
|
import { ReceiverApplication, ReceiverMessage, SenderMessage } from "./types";
|
||||||
|
|
||||||
|
|
||||||
export const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection";
|
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_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
|
||||||
export const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
|
export const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
|
||||||
|
|
||||||
const HEARTBEAT_INTERVAL = 5000;
|
const HEARTBEAT_INTERVAL = 5000;
|
||||||
|
|
||||||
|
|
||||||
class CastClient {
|
class CastClient {
|
||||||
protected client = new Client();
|
protected client = new Client();
|
||||||
|
|
||||||
@@ -22,18 +20,26 @@ class CastClient {
|
|||||||
protected heartbeatChannel?: Channel;
|
protected heartbeatChannel?: Channel;
|
||||||
protected heartbeatIntervalId?: NodeJS.Timeout;
|
protected heartbeatIntervalId?: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(protected sourceId = "sender-0"
|
constructor(
|
||||||
, protected destinationId = "receiver-0") {}
|
protected sourceId = "sender-0",
|
||||||
|
protected destinationId = "receiver-0"
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a channel on the client connection with a given
|
* Create a channel on the client connection with a given
|
||||||
* namespace.
|
* namespace.
|
||||||
*/
|
*/
|
||||||
createChannel(namespace: string
|
createChannel(
|
||||||
, sourceId = this.sourceId
|
namespace: string,
|
||||||
, destinationId = this.destinationId) {
|
sourceId = this.sourceId,
|
||||||
|
destinationId = this.destinationId
|
||||||
return this.client.createChannel(sourceId, destinationId, namespace, "JSON");
|
) {
|
||||||
|
return this.client.createChannel(
|
||||||
|
sourceId,
|
||||||
|
destinationId,
|
||||||
|
namespace,
|
||||||
|
"JSON"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(host: string, port: number, onHeartbeat?: () => void) {
|
connect(host: string, port: number, onHeartbeat?: () => void) {
|
||||||
@@ -66,7 +72,6 @@ class CastClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type OnSessionCreatedCallback = (sessionId: string) => void;
|
type OnSessionCreatedCallback = (sessionId: string) => void;
|
||||||
|
|
||||||
export default class Session extends CastClient {
|
export default class Session extends CastClient {
|
||||||
@@ -93,12 +98,17 @@ export default class Session extends CastClient {
|
|||||||
|
|
||||||
private onSessionCreated?: OnSessionCreatedCallback;
|
private onSessionCreated?: OnSessionCreatedCallback;
|
||||||
|
|
||||||
|
|
||||||
private establishAppConnection(transportId: string) {
|
private establishAppConnection(transportId: string) {
|
||||||
this.transportConnection = this.createChannel(
|
this.transportConnection = this.createChannel(
|
||||||
NS_CONNECTION, this.sourceId, transportId);
|
NS_CONNECTION,
|
||||||
|
this.sourceId,
|
||||||
|
transportId
|
||||||
|
);
|
||||||
this.transportHeartbeat = this.createChannel(
|
this.transportHeartbeat = this.createChannel(
|
||||||
NS_HEARTBEAT, this.sourceId, transportId);
|
NS_HEARTBEAT,
|
||||||
|
this.sourceId,
|
||||||
|
transportId
|
||||||
|
);
|
||||||
|
|
||||||
this.transportConnection.send({ type: "CONNECT" });
|
this.transportConnection.send({ type: "CONNECT" });
|
||||||
}
|
}
|
||||||
@@ -111,7 +121,8 @@ export default class Session extends CastClient {
|
|||||||
case "RECEIVER_STATUS": {
|
case "RECEIVER_STATUS": {
|
||||||
const { status } = message;
|
const { status } = message;
|
||||||
const application = status.applications?.find(
|
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
|
* If application isn't set, still waiting on the launch
|
||||||
@@ -133,20 +144,20 @@ export default class Session extends CastClient {
|
|||||||
const { friendlyName } = this.receiverDevice;
|
const { friendlyName } = this.receiverDevice;
|
||||||
|
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "shim:castSessionCreated"
|
subject: "shim:castSessionCreated",
|
||||||
, data: {
|
data: {
|
||||||
sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
, statusText: application.statusText
|
statusText: application.statusText,
|
||||||
, namespaces: application.namespaces
|
namespaces: application.namespaces,
|
||||||
, volume: status.volume
|
volume: status.volume,
|
||||||
, appId: application.appId
|
appId: application.appId,
|
||||||
, displayName: application.displayName
|
displayName: application.displayName,
|
||||||
, receiverFriendlyName: friendlyName
|
receiverFriendlyName: friendlyName,
|
||||||
, transportId: this.sessionId
|
transportId: this.sessionId,
|
||||||
|
|
||||||
// TODO: Fix this
|
// TODO: Fix this
|
||||||
, senderApps: []
|
senderApps: [],
|
||||||
, appImages: []
|
appImages: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -161,12 +172,12 @@ export default class Session extends CastClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "shim:castSessionUpdated"
|
subject: "shim:castSessionUpdated",
|
||||||
, data: {
|
data: {
|
||||||
sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
, statusText: application.statusText
|
statusText: application.statusText,
|
||||||
, namespaces: application.namespaces
|
namespaces: application.namespaces,
|
||||||
, volume: message.status.volume
|
volume: message.status.volume
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,13 +190,16 @@ export default class Session extends CastClient {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
sendMessage(namespace: string, message: unknown) {
|
sendMessage(namespace: string, message: unknown) {
|
||||||
let channel = this.namespaceChannelMap.get(namespace);
|
let channel = this.namespaceChannelMap.get(namespace);
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
channel = this.createChannel(
|
channel = this.createChannel(
|
||||||
namespace, this.sourceId, this.transportId);
|
namespace,
|
||||||
|
this.sourceId,
|
||||||
|
this.transportId
|
||||||
|
);
|
||||||
|
|
||||||
channel.on("message", messageData => {
|
channel.on("message", messageData => {
|
||||||
if (!this.sessionId) {
|
if (!this.sessionId) {
|
||||||
@@ -195,11 +209,11 @@ export default class Session extends CastClient {
|
|||||||
messageData = JSON.stringify(messageData);
|
messageData = JSON.stringify(messageData);
|
||||||
|
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "shim:receivedCastSessionMessage"
|
subject: "shim:receivedCastSessionMessage",
|
||||||
, data: {
|
data: {
|
||||||
sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
, namespace
|
namespace,
|
||||||
, messageData
|
messageData
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -222,25 +236,24 @@ export default class Session extends CastClient {
|
|||||||
return requestId;
|
return requestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(public appId: string
|
constructor(public appId: string, public receiverDevice: ReceiverDevice) {
|
||||||
, public receiverDevice: ReceiverDevice) {
|
|
||||||
|
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.client.on("close", () => {
|
this.client.on("close", () => {
|
||||||
if (this.sessionId) {
|
if (this.sessionId) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "shim:castSessionStopped"
|
subject: "shim:castSessionStopped",
|
||||||
, data: { sessionId: this.sessionId }
|
data: { sessionId: this.sessionId }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(host: string
|
async connect(
|
||||||
, port: number
|
host: string,
|
||||||
, onSessionCreated?: OnSessionCreatedCallback) {
|
port: number,
|
||||||
|
onSessionCreated?: OnSessionCreatedCallback
|
||||||
|
) {
|
||||||
if (onSessionCreated) {
|
if (onSessionCreated) {
|
||||||
this.onSessionCreated = onSessionCreated;
|
this.onSessionCreated = onSessionCreated;
|
||||||
}
|
}
|
||||||
@@ -253,8 +266,8 @@ export default class Session extends CastClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.launchRequestId = this.sendReceiverMessage({
|
this.launchRequestId = this.sendReceiverMessage({
|
||||||
type: "LAUNCH"
|
type: "LAUNCH",
|
||||||
, appId: this.appId
|
appId: this.appId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import castv2 from "castv2";
|
|||||||
import { sendMessage } from "../../lib/nativeMessaging";
|
import { sendMessage } from "../../lib/nativeMessaging";
|
||||||
import { Message } from "../../messaging";
|
import { Message } from "../../messaging";
|
||||||
|
|
||||||
import Session, { NS_CONNECTION
|
import Session, { NS_CONNECTION, NS_RECEIVER } from "./Session";
|
||||||
, NS_RECEIVER } from "./Session";
|
|
||||||
|
|
||||||
|
|
||||||
const sessions = new Map<string, Session>();
|
const sessions = new Map<string, Session>();
|
||||||
|
|
||||||
@@ -19,9 +17,12 @@ export function handleCastMessage(message: Message) {
|
|||||||
// Connect and store with returned ID
|
// Connect and store with returned ID
|
||||||
const session = new Session(appId, receiverDevice);
|
const session = new Session(appId, receiverDevice);
|
||||||
session.connect(
|
session.connect(
|
||||||
receiverDevice.host, receiverDevice.port, sessionId => {
|
receiverDevice.host,
|
||||||
sessions.set(sessionId, session);
|
receiverDevice.port,
|
||||||
});
|
sessionId => {
|
||||||
|
sessions.set(sessionId, session);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -32,10 +33,11 @@ export function handleCastMessage(message: Message) {
|
|||||||
const session = sessions.get(sessionId);
|
const session = sessions.get(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "shim:impl_sendCastMessage"
|
subject: "shim:impl_sendCastMessage",
|
||||||
, data: {
|
data: {
|
||||||
error: "Session does not exist"
|
error: "Session does not exist",
|
||||||
, sessionId, messageId
|
sessionId,
|
||||||
|
messageId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,10 +48,11 @@ export function handleCastMessage(message: Message) {
|
|||||||
session.sendReceiverMessage(messageData);
|
session.sendReceiverMessage(messageData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "shim:impl_sendCastMessage"
|
subject: "shim:impl_sendCastMessage",
|
||||||
, data: {
|
data: {
|
||||||
error: `Failed to send message (${err})`
|
error: `Failed to send message (${err})`,
|
||||||
, sessionId, messageId
|
sessionId,
|
||||||
|
messageId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,8 +61,8 @@ export function handleCastMessage(message: Message) {
|
|||||||
|
|
||||||
// Success
|
// Success
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "shim:impl_sendCastMessage"
|
subject: "shim:impl_sendCastMessage",
|
||||||
, data: { sessionId, messageId }
|
data: { sessionId, messageId }
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -71,10 +74,11 @@ export function handleCastMessage(message: Message) {
|
|||||||
const session = sessions.get(sessionId);
|
const session = sessions.get(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "shim:impl_sendCastMessage"
|
subject: "shim:impl_sendCastMessage",
|
||||||
, data: {
|
data: {
|
||||||
error: "Session does not exist"
|
error: "Session does not exist",
|
||||||
, sessionId, messageId
|
sessionId,
|
||||||
|
messageId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,10 +95,11 @@ export function handleCastMessage(message: Message) {
|
|||||||
session.sendMessage(namespace, messageData);
|
session.sendMessage(namespace, messageData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "shim:impl_sendCastMessage"
|
subject: "shim:impl_sendCastMessage",
|
||||||
, data: {
|
data: {
|
||||||
error: `Failed to send message (${err})`
|
error: `Failed to send message (${err})`,
|
||||||
, sessionId, messageId
|
sessionId,
|
||||||
|
messageId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,8 +108,8 @@ export function handleCastMessage(message: Message) {
|
|||||||
|
|
||||||
// Success
|
// Success
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "shim:impl_sendCastMessage"
|
subject: "shim:impl_sendCastMessage",
|
||||||
, data: { sessionId, messageId }
|
data: { sessionId, messageId }
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -119,9 +124,17 @@ export function handleCastMessage(message: Message) {
|
|||||||
const destinationId = "receiver-0";
|
const destinationId = "receiver-0";
|
||||||
|
|
||||||
const clientConnection = client.createChannel(
|
const clientConnection = client.createChannel(
|
||||||
sourceId, destinationId, NS_CONNECTION, "JSON");
|
sourceId,
|
||||||
|
destinationId,
|
||||||
|
NS_CONNECTION,
|
||||||
|
"JSON"
|
||||||
|
);
|
||||||
const clientReceiver = client.createChannel(
|
const clientReceiver = client.createChannel(
|
||||||
sourceId, destinationId, NS_RECEIVER, "JSON");
|
sourceId,
|
||||||
|
destinationId,
|
||||||
|
NS_RECEIVER,
|
||||||
|
"JSON"
|
||||||
|
);
|
||||||
|
|
||||||
clientConnection.send({ type: "CONNECT" });
|
clientConnection.send({ type: "CONNECT" });
|
||||||
clientReceiver.send({ type: "STOP", requestId: 1 });
|
clientReceiver.send({ type: "STOP", requestId: 1 });
|
||||||
|
|||||||
@@ -7,18 +7,18 @@ export interface Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Capability {
|
enum Capability {
|
||||||
VIDEO_OUT = "video_out"
|
VIDEO_OUT = "video_out",
|
||||||
, AUDIO_OUT = "audio_out"
|
AUDIO_OUT = "audio_out",
|
||||||
, VIDEO_IN = "video_in"
|
VIDEO_IN = "video_in",
|
||||||
, AUDIO_IN = "audio_in"
|
AUDIO_IN = "audio_in",
|
||||||
, MULTIZONE_GROUP = "multizone_group"
|
MULTIZONE_GROUP = "multizone_group"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReceiverType {
|
enum ReceiverType {
|
||||||
CAST = "cast"
|
CAST = "cast",
|
||||||
, DIAL = "dial"
|
DIAL = "dial",
|
||||||
, HANGOUT = "hangout"
|
HANGOUT = "hangout",
|
||||||
, CUSTOM = "custom"
|
CUSTOM = "custom"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SenderApplication {
|
export interface SenderApplication {
|
||||||
@@ -28,9 +28,9 @@ export interface SenderApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum VolumeControlType {
|
enum VolumeControlType {
|
||||||
ATTENUATION = "attenuation"
|
ATTENUATION = "attenuation",
|
||||||
, FIXED = "fixed"
|
FIXED = "fixed",
|
||||||
, MASTER = "master"
|
MASTER = "master"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Volume {
|
export interface Volume {
|
||||||
@@ -43,75 +43,74 @@ export interface Volume {
|
|||||||
// Media
|
// Media
|
||||||
|
|
||||||
enum IdleReason {
|
enum IdleReason {
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED",
|
||||||
, INTERRUPTED = "INTERRUPTED"
|
INTERRUPTED = "INTERRUPTED",
|
||||||
, FINISHED = "FINISHED"
|
FINISHED = "FINISHED",
|
||||||
, ERROR = "ERROR"
|
ERROR = "ERROR"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HlsSegmentFormat {
|
enum HlsSegmentFormat {
|
||||||
AAC = "aac"
|
AAC = "aac",
|
||||||
, AC3 = "ac3"
|
AC3 = "ac3",
|
||||||
, MP3 = "mp3"
|
MP3 = "mp3",
|
||||||
, TS = "ts"
|
TS = "ts",
|
||||||
, TS_AAC = "ts_aac"
|
TS_AAC = "ts_aac",
|
||||||
, E_AC3 = "e_ac3"
|
E_AC3 = "e_ac3",
|
||||||
, FMP4 = "fmp4"
|
FMP4 = "fmp4"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum HlsVideoSegmentFormat {
|
export enum HlsVideoSegmentFormat {
|
||||||
MPEG2_TS = "mpeg2_ts"
|
MPEG2_TS = "mpeg2_ts",
|
||||||
, FMP4 = "fmp4"
|
FMP4 = "fmp4"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MetadataType {
|
enum MetadataType {
|
||||||
GENERIC
|
GENERIC,
|
||||||
, MOVIE
|
MOVIE,
|
||||||
, TV_SHOW
|
TV_SHOW,
|
||||||
, MUSIC_TRACK
|
MUSIC_TRACK,
|
||||||
, PHOTO
|
PHOTO,
|
||||||
, AUDIOBOOK_CHAPTER
|
AUDIOBOOK_CHAPTER
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PlayerState {
|
enum PlayerState {
|
||||||
IDLE = "IDLE"
|
IDLE = "IDLE",
|
||||||
, PLAYING = "PLAYING"
|
PLAYING = "PLAYING",
|
||||||
, PAUSED = "PAUSED"
|
PAUSED = "PAUSED",
|
||||||
, BUFFERING = "BUFFERING"
|
BUFFERING = "BUFFERING"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RepeatMode {
|
enum RepeatMode {
|
||||||
OFF = "REPEAT_OFF"
|
OFF = "REPEAT_OFF",
|
||||||
, ALL = "REPEAT_ALL"
|
ALL = "REPEAT_ALL",
|
||||||
, SINGLE = "REPEAT_SINGLE"
|
SINGLE = "REPEAT_SINGLE",
|
||||||
, ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
|
ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ResumeState {
|
enum ResumeState {
|
||||||
PLAYBACK_START = "PLAYBACK_START"
|
PLAYBACK_START = "PLAYBACK_START",
|
||||||
, PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
|
PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StreamType {
|
enum StreamType {
|
||||||
BUFFERED = "BUFFERED"
|
BUFFERED = "BUFFERED",
|
||||||
, LIVE = "LIVE"
|
LIVE = "LIVE",
|
||||||
, OTHER = "OTHER"
|
OTHER = "OTHER"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TrackType {
|
enum TrackType {
|
||||||
TEXT = "TEXT"
|
TEXT = "TEXT",
|
||||||
, AUDIO = "AUDIO"
|
AUDIO = "AUDIO",
|
||||||
, VIDEO = "VIDEO"
|
VIDEO = "VIDEO"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserAction {
|
export enum UserAction {
|
||||||
LIKE = "LIKE"
|
LIKE = "LIKE",
|
||||||
, DISLIKE = "DISLIKE"
|
DISLIKE = "DISLIKE",
|
||||||
, FOLLOW = "FOLLOW"
|
FOLLOW = "FOLLOW",
|
||||||
, UNFOLLOW = "UNFOLLOW"
|
UNFOLLOW = "UNFOLLOW"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface Break {
|
interface Break {
|
||||||
breakClipIds: string[];
|
breakClipIds: string[];
|
||||||
duration?: number;
|
duration?: number;
|
||||||
@@ -173,11 +172,11 @@ interface VastAdsRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Metadata =
|
type Metadata =
|
||||||
GenericMediaMetadata
|
| GenericMediaMetadata
|
||||||
| MovieMediaMetadata
|
| MovieMediaMetadata
|
||||||
| MusicTrackMediaMetadata
|
| MusicTrackMediaMetadata
|
||||||
| PhotoMediaMetadata
|
| PhotoMediaMetadata
|
||||||
| TvShowMediaMetadata;
|
| TvShowMediaMetadata;
|
||||||
|
|
||||||
interface MediaInformation {
|
interface MediaInformation {
|
||||||
atvEntity?: string;
|
atvEntity?: string;
|
||||||
@@ -284,11 +283,11 @@ export interface MediaStatus {
|
|||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
playerState: PlayerState;
|
playerState: PlayerState;
|
||||||
idleReason?: IdleReason;
|
idleReason?: IdleReason;
|
||||||
items?: QueueItem[]
|
items?: QueueItem[];
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
supportedMediaCommands: number;
|
supportedMediaCommands: number;
|
||||||
repeatMode: RepeatMode;
|
repeatMode: RepeatMode;
|
||||||
volume: Volume
|
volume: Volume;
|
||||||
customData: unknown;
|
customData: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,23 +328,21 @@ export interface ReceiverStatus {
|
|||||||
volume: Volume;
|
volume: Volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface ReqBase {
|
interface ReqBase {
|
||||||
requestId: number;
|
requestId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NS: urn:x-cast:com.google.cast.receiver
|
// NS: urn:x-cast:com.google.cast.receiver
|
||||||
export type SenderMessage =
|
export type SenderMessage =
|
||||||
ReqBase & { type: "LAUNCH", appId: string }
|
| (ReqBase & { type: "LAUNCH"; appId: string })
|
||||||
| ReqBase & { type: "STOP", sessionId: string }
|
| (ReqBase & { type: "STOP"; sessionId: string })
|
||||||
| ReqBase & { type: "GET_STATUS" }
|
| (ReqBase & { type: "GET_STATUS" })
|
||||||
| ReqBase & { type: "GET_APP_AVAILABILITY", appId: string[] }
|
| (ReqBase & { type: "GET_APP_AVAILABILITY"; appId: string[] })
|
||||||
| ReqBase & { type: "SET_VOLUME", volume: Volume };
|
| (ReqBase & { type: "SET_VOLUME"; volume: Volume });
|
||||||
|
|
||||||
export type ReceiverMessage =
|
export type ReceiverMessage =
|
||||||
ReqBase & { type: "RECEIVER_STATUS", status: ReceiverStatus }
|
| (ReqBase & { type: "RECEIVER_STATUS"; status: ReceiverStatus })
|
||||||
| ReqBase & { type: "LAUNCH_ERROR", reason: string }
|
| (ReqBase & { type: "LAUNCH_ERROR"; reason: string });
|
||||||
|
|
||||||
|
|
||||||
interface MediaReqBase extends ReqBase {
|
interface MediaReqBase extends ReqBase {
|
||||||
mediaSessionId: number;
|
mediaSessionId: number;
|
||||||
@@ -354,84 +351,84 @@ interface MediaReqBase extends ReqBase {
|
|||||||
|
|
||||||
// NS: urn:x-cast:com.google.cast.media
|
// NS: urn:x-cast:com.google.cast.media
|
||||||
export type SenderMediaMessage =
|
export type SenderMediaMessage =
|
||||||
| MediaReqBase & { type: "PLAY" }
|
| (MediaReqBase & { type: "PLAY" })
|
||||||
| MediaReqBase & { type: "PAUSE" }
|
| (MediaReqBase & { type: "PAUSE" })
|
||||||
| MediaReqBase & { type: "MEDIA_GET_STATUS" }
|
| (MediaReqBase & { type: "MEDIA_GET_STATUS" })
|
||||||
| MediaReqBase & { type: "STOP" }
|
| (MediaReqBase & { type: "STOP" })
|
||||||
| MediaReqBase & { type: "MEDIA_SET_VOLUME", volume: Volume }
|
| (MediaReqBase & { type: "MEDIA_SET_VOLUME"; volume: Volume })
|
||||||
| MediaReqBase & { type: "SET_PLAYBACK_RATE" , playbackRate: number }
|
| (MediaReqBase & { type: "SET_PLAYBACK_RATE"; playbackRate: number })
|
||||||
| ReqBase & {
|
| (ReqBase & {
|
||||||
type: "LOAD"
|
type: "LOAD";
|
||||||
, activeTrackIds: Nullable<number[]>
|
activeTrackIds: Nullable<number[]>;
|
||||||
, atvCredentials?: string
|
atvCredentials?: string;
|
||||||
, atvCredentialsType?: string
|
atvCredentialsType?: string;
|
||||||
, autoplay: Nullable<boolean>
|
autoplay: Nullable<boolean>;
|
||||||
, currentTime: Nullable<number>
|
currentTime: Nullable<number>;
|
||||||
, customData?: unknown
|
customData?: unknown;
|
||||||
, media: MediaInformation
|
media: MediaInformation;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "SEEK"
|
type: "SEEK";
|
||||||
, resumeState: Nullable<ResumeState>
|
resumeState: Nullable<ResumeState>;
|
||||||
, currentTime: Nullable<number>
|
currentTime: Nullable<number>;
|
||||||
}
|
})
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "EDIT_TRACKS_INFO"
|
type: "EDIT_TRACKS_INFO";
|
||||||
, activeTrackIds: Nullable<number[]>
|
activeTrackIds: Nullable<number[]>;
|
||||||
, textTrackStyle: Nullable<string>
|
textTrackStyle: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueLoadRequest
|
// QueueLoadRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_LOAD"
|
type: "QUEUE_LOAD";
|
||||||
, items: QueueItem[]
|
items: QueueItem[];
|
||||||
, startIndex: number
|
startIndex: number;
|
||||||
, repeatMode: string
|
repeatMode: string;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueInsertItemsRequest
|
// QueueInsertItemsRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_INSERT"
|
type: "QUEUE_INSERT";
|
||||||
, items: QueueItem[]
|
items: QueueItem[];
|
||||||
, insertBefore: Nullable<number>
|
insertBefore: Nullable<number>;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueUpdateItemsRequest
|
// QueueUpdateItemsRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_UPDATE"
|
type: "QUEUE_UPDATE";
|
||||||
, items: QueueItem[]
|
items: QueueItem[];
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueJumpRequest
|
// QueueJumpRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_UPDATE"
|
type: "QUEUE_UPDATE";
|
||||||
, jump: Nullable<number>
|
jump: Nullable<number>;
|
||||||
, currentItemId: Nullable<number>
|
currentItemId: Nullable<number>;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueRemoveItemsRequest
|
// QueueRemoveItemsRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_REMOVE"
|
type: "QUEUE_REMOVE";
|
||||||
, itemIds: number[]
|
itemIds: number[];
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueReorderItemsRequest
|
// QueueReorderItemsRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_REORDER"
|
type: "QUEUE_REORDER";
|
||||||
, itemIds: number[]
|
itemIds: number[];
|
||||||
, insertBefore: Nullable<number>
|
insertBefore: Nullable<number>;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueSetPropertiesRequest
|
// QueueSetPropertiesRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_UPDATE"
|
type: "QUEUE_UPDATE";
|
||||||
, repeatMode: Nullable<string>
|
repeatMode: Nullable<string>;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
};
|
});
|
||||||
|
|
||||||
export type ReceiverMediaMessage =
|
export type ReceiverMediaMessage =
|
||||||
MediaReqBase & { type: "MEDIA_STATUS", status: MediaStatus[] }
|
| (MediaReqBase & { type: "MEDIA_STATUS"; status: MediaStatus[] })
|
||||||
| MediaReqBase & { type: "INVALID_PLAYER_STATE" }
|
| (MediaReqBase & { type: "INVALID_PLAYER_STATE" })
|
||||||
| MediaReqBase & { type: "LOAD_FAILED" }
|
| (MediaReqBase & { type: "LOAD_FAILED" })
|
||||||
| MediaReqBase & { type: "LOAD_CANCELLED" }
|
| (MediaReqBase & { type: "LOAD_CANCELLED" })
|
||||||
| MediaReqBase & { type: "INVALID_REQUEST" };
|
| (MediaReqBase & { type: "INVALID_REQUEST" });
|
||||||
|
|||||||
@@ -9,25 +9,31 @@ import mdns from "mdns";
|
|||||||
import { sendMessage } from "../lib/nativeMessaging";
|
import { sendMessage } from "../lib/nativeMessaging";
|
||||||
|
|
||||||
import { ReceiverStatus } from "./cast/types";
|
import { ReceiverStatus } from "./cast/types";
|
||||||
import { NS_CONNECTION, NS_HEARTBEAT, NS_RECEIVER }
|
import { NS_CONNECTION, NS_HEARTBEAT, NS_RECEIVER } from "./cast/Session";
|
||||||
from "./cast/Session";
|
|
||||||
|
|
||||||
|
|
||||||
interface CastTxtRecord {
|
interface CastTxtRecord {
|
||||||
id: string; cd: string; rm: string;
|
id: string;
|
||||||
ve: string; md: string; ic: string;
|
cd: string;
|
||||||
fn: string; ca: string; st: string;
|
rm: string;
|
||||||
bs: string; nf: string; rs: 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"), {
|
const browser = mdns.createBrowser(mdns.tcp("googlecast"), {
|
||||||
resolverSequence: [
|
resolverSequence: [
|
||||||
mdns.rst.DNSServiceResolve()
|
mdns.rst.DNSServiceResolve(),
|
||||||
, "DNSServiceGetAddrInfo" in mdns.dns_sd
|
"DNSServiceGetAddrInfo" in mdns.dns_sd
|
||||||
? mdns.rst.DNSServiceGetAddrInfo()
|
? mdns.rst.DNSServiceGetAddrInfo()
|
||||||
// Some issues on Linux with IPv6, so restrict to IPv4
|
: // Some issues on Linux with IPv6, so restrict to IPv4
|
||||||
: mdns.rst.getaddrinfo({ families: [ 4 ]})
|
mdns.rst.getaddrinfo({ families: [4] }),
|
||||||
, mdns.rst.makeAddressesUnique()
|
mdns.rst.makeAddressesUnique()
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,13 +45,13 @@ function onBrowserServiceUp(service: mdns.Service) {
|
|||||||
|
|
||||||
const txtRecord = service.txtRecord as CastTxtRecord;
|
const txtRecord = service.txtRecord as CastTxtRecord;
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "main:receiverDeviceUp"
|
subject: "main:receiverDeviceUp",
|
||||||
, data: {
|
data: {
|
||||||
receiverDevice: {
|
receiverDevice: {
|
||||||
host: service.addresses[0]
|
host: service.addresses[0],
|
||||||
, port: service.port
|
port: service.port,
|
||||||
, id: service.name
|
id: service.name,
|
||||||
, friendlyName: txtRecord.fn
|
friendlyName: txtRecord.fn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -59,15 +65,14 @@ function onBrowserServiceDown(service: mdns.Service) {
|
|||||||
|
|
||||||
const txtRecord = service.txtRecord as CastTxtRecord;
|
const txtRecord = service.txtRecord as CastTxtRecord;
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "main:receiverDeviceDown"
|
subject: "main:receiverDeviceDown",
|
||||||
, data: { receiverDeviceId: service.name }
|
data: { receiverDeviceId: service.name }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.on("serviceUp", onBrowserServiceUp);
|
browser.on("serviceUp", onBrowserServiceUp);
|
||||||
browser.on("serviceDown", onBrowserServiceDown);
|
browser.on("serviceDown", onBrowserServiceDown);
|
||||||
|
|
||||||
|
|
||||||
interface InitializeOptions {
|
interface InitializeOptions {
|
||||||
shouldWatchStatus?: boolean;
|
shouldWatchStatus?: boolean;
|
||||||
}
|
}
|
||||||
@@ -88,8 +93,7 @@ export function startDiscovery(options: InitializeOptions) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const listener = new StatusListener(
|
const listener = new StatusListener(service.addresses[0], service.port);
|
||||||
service.addresses[0], service.port);
|
|
||||||
|
|
||||||
listener.on("receiverStatus", (status: ReceiverStatus) => {
|
listener.on("receiverStatus", (status: ReceiverStatus) => {
|
||||||
if (!service.name) {
|
if (!service.name) {
|
||||||
@@ -97,10 +101,10 @@ export function startDiscovery(options: InitializeOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "main:receiverDeviceUpdated"
|
subject: "main:receiverDeviceUpdated",
|
||||||
, data: {
|
data: {
|
||||||
receiverDeviceId: service.name
|
receiverDeviceId: service.name,
|
||||||
, status
|
status
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -122,12 +126,11 @@ export function stopDiscovery() {
|
|||||||
browser.stop();
|
browser.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a connection to a receiver device and forwards
|
* Creates a connection to a receiver device and forwards
|
||||||
* RECEIVER_STATUS updates to the extension.
|
* RECEIVER_STATUS updates to the extension.
|
||||||
*/
|
*/
|
||||||
export default class StatusListener extends EventEmitter {
|
export default class StatusListener extends EventEmitter {
|
||||||
private client: Client;
|
private client: Client;
|
||||||
private clientReceiver?: Channel;
|
private clientReceiver?: Channel;
|
||||||
private clientHeartbeatIntervalId?: NodeJS.Timeout;
|
private clientHeartbeatIntervalId?: NodeJS.Timeout;
|
||||||
@@ -160,17 +163,28 @@ export function stopDiscovery() {
|
|||||||
this.client.close();
|
this.client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private onConnect(): void {
|
private onConnect(): void {
|
||||||
const sourceId = "sender-0";
|
const sourceId = "sender-0";
|
||||||
const destinationId = "receiver-0";
|
const destinationId = "receiver-0";
|
||||||
|
|
||||||
const clientConnection = this.client.createChannel(
|
const clientConnection = this.client.createChannel(
|
||||||
sourceId, destinationId, NS_CONNECTION, "JSON");
|
sourceId,
|
||||||
|
destinationId,
|
||||||
|
NS_CONNECTION,
|
||||||
|
"JSON"
|
||||||
|
);
|
||||||
const clientHeartbeat = this.client.createChannel(
|
const clientHeartbeat = this.client.createChannel(
|
||||||
sourceId, destinationId, NS_HEARTBEAT, "JSON");
|
sourceId,
|
||||||
|
destinationId,
|
||||||
|
NS_HEARTBEAT,
|
||||||
|
"JSON"
|
||||||
|
);
|
||||||
const clientReceiver = this.client.createChannel(
|
const clientReceiver = this.client.createChannel(
|
||||||
sourceId, destinationId, NS_RECEIVER, "JSON");
|
sourceId,
|
||||||
|
destinationId,
|
||||||
|
NS_RECEIVER,
|
||||||
|
"JSON"
|
||||||
|
);
|
||||||
|
|
||||||
clientReceiver.on("message", data => {
|
clientReceiver.on("message", data => {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import mime from "mime-types";
|
|||||||
import { sendMessage } from "../lib/nativeMessaging";
|
import { sendMessage } from "../lib/nativeMessaging";
|
||||||
import { convertSrtToVtt } from "../lib/subtitles";
|
import { convertSrtToVtt } from "../lib/subtitles";
|
||||||
|
|
||||||
|
|
||||||
export let mediaServer: http.Server | undefined;
|
export let mediaServer: http.Server | undefined;
|
||||||
|
|
||||||
export async function startMediaServer(filePath: string, port: number) {
|
export async function startMediaServer(filePath: string, port: number) {
|
||||||
@@ -41,7 +40,7 @@ export async function startMediaServer(filePath: string, port: number) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error: Failed to find media path.");
|
console.error("Error: Failed to find media path.");
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "mediaCast:mediaServerError"
|
subject: "mediaCast:mediaServerError"
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -63,15 +62,19 @@ export async function startMediaServer(filePath: string, port: number) {
|
|||||||
*/
|
*/
|
||||||
const subtitles = new Map<string, string>();
|
const subtitles = new Map<string, string>();
|
||||||
try {
|
try {
|
||||||
const dirEntries = await fs.promises.readdir(
|
const dirEntries = await fs.promises.readdir(fileDir, {
|
||||||
fileDir, { withFileTypes: true });
|
withFileTypes: true
|
||||||
|
});
|
||||||
|
|
||||||
for (const dirEntry of dirEntries) {
|
for (const dirEntry of dirEntries) {
|
||||||
if (dirEntry.isFile()
|
if (
|
||||||
&& mime.lookup(dirEntry.name) === "application/x-subrip") {
|
dirEntry.isFile() &&
|
||||||
|
mime.lookup(dirEntry.name) === "application/x-subrip"
|
||||||
subtitles.set(dirEntry.name, await convertSrtToVtt(
|
) {
|
||||||
path.join(fileDir, dirEntry.name)));
|
subtitles.set(
|
||||||
|
dirEntry.name,
|
||||||
|
await convertSrtToVtt(path.join(fileDir, dirEntry.name))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -96,22 +99,20 @@ export async function startMediaServer(filePath: string, port: number) {
|
|||||||
if (range) {
|
if (range) {
|
||||||
const bounds = range.substring(6).split("-");
|
const bounds = range.substring(6).split("-");
|
||||||
const start = parseInt(bounds[0]);
|
const start = parseInt(bounds[0]);
|
||||||
const end = bounds[1]
|
const end = bounds[1] ? parseInt(bounds[1]) : fileSize - 1;
|
||||||
? parseInt(bounds[1]) : fileSize - 1;
|
|
||||||
|
|
||||||
res.writeHead(206, {
|
res.writeHead(206, {
|
||||||
"Accept-Ranges": "bytes"
|
"Accept-Ranges": "bytes",
|
||||||
, "Content-Range": `bytes ${start}-${end}/${fileSize}`
|
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
|
||||||
, "Content-Length": (end - start) + 1
|
"Content-Length": end - start + 1,
|
||||||
, "Content-Type": contentType
|
"Content-Type": contentType
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.createReadStream(
|
fs.createReadStream(filePath, { start, end }).pipe(res);
|
||||||
filePath, { start, end }).pipe(res);
|
|
||||||
} else {
|
} else {
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
"Content-Length": fileSize
|
"Content-Length": fileSize,
|
||||||
, "Content-Type": contentType
|
"Content-Type": contentType
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.createReadStream(filePath).pipe(res);
|
fs.createReadStream(filePath).pipe(res);
|
||||||
@@ -139,7 +140,8 @@ export async function startMediaServer(filePath: string, port: number) {
|
|||||||
const ifaces = Object.values(os.networkInterfaces());
|
const ifaces = Object.values(os.networkInterfaces());
|
||||||
for (const iface of ifaces) {
|
for (const iface of ifaces) {
|
||||||
const matchingIface = iface?.find(
|
const matchingIface = iface?.find(
|
||||||
details => details.family === "IPv4" && !details.internal);
|
details => details.family === "IPv4" && !details.internal
|
||||||
|
);
|
||||||
if (matchingIface) {
|
if (matchingIface) {
|
||||||
localAddress = matchingIface.address;
|
localAddress = matchingIface.address;
|
||||||
}
|
}
|
||||||
@@ -155,21 +157,25 @@ export async function startMediaServer(filePath: string, port: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "mediaCast:mediaServerStarted"
|
subject: "mediaCast:mediaServerStarted",
|
||||||
, data: {
|
data: {
|
||||||
mediaPath: fileName
|
mediaPath: fileName,
|
||||||
, subtitlePaths: Array.from(subtitles.keys())
|
subtitlePaths: Array.from(subtitles.keys()),
|
||||||
, localAddress
|
localAddress
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaServer.on("close", () => sendMessage({
|
mediaServer.on("close", () =>
|
||||||
subject: "mediaCast:mediaServerStopped"
|
sendMessage({
|
||||||
}));
|
subject: "mediaCast:mediaServerStopped"
|
||||||
mediaServer.on("error", () => sendMessage({
|
})
|
||||||
subject: "mediaCast:mediaServerError"
|
);
|
||||||
}));
|
mediaServer.on("error", () =>
|
||||||
|
sendMessage({
|
||||||
|
subject: "mediaCast:mediaServerError"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
mediaServer.listen(port);
|
mediaServer.listen(port);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import path from "path";
|
|||||||
|
|
||||||
import { sendMessage } from "../lib/nativeMessaging";
|
import { sendMessage } from "../lib/nativeMessaging";
|
||||||
|
|
||||||
|
|
||||||
function fatal(message: string) {
|
function fatal(message: string) {
|
||||||
console.error(message);
|
console.error(message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let selectorApp: child_process.ChildProcess | undefined;
|
let selectorApp: child_process.ChildProcess | undefined;
|
||||||
let selectorAppOpen = false;
|
let selectorAppOpen = false;
|
||||||
|
|
||||||
@@ -35,10 +33,12 @@ export function startReceiverSelector(data: string) {
|
|||||||
selectorAppOpen = false;
|
selectorAppOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectorPath = path.join(process.cwd()
|
const selectorPath = path.join(
|
||||||
, "fx_cast_selector.app/Contents/MacOS/fx_cast_selector");
|
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;
|
selectorAppOpen = true;
|
||||||
|
|
||||||
if (selectorApp.stdout) {
|
if (selectorApp.stdout) {
|
||||||
@@ -48,24 +48,24 @@ export function startReceiverSelector(data: string) {
|
|||||||
|
|
||||||
if (!jsonData.mediaType) {
|
if (!jsonData.mediaType) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "main:receiverSelector/stopped"
|
subject: "main:receiverSelector/stopped",
|
||||||
, data: jsonData
|
data: jsonData
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "main:receiverSelector/selected"
|
subject: "main:receiverSelector/selected",
|
||||||
, data: jsonData
|
data: jsonData
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
selectorApp.on("error", err => {
|
selectorApp.on("error", err => {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
subject: "main:receiverSelector/error"
|
subject: "main:receiverSelector/error",
|
||||||
, data: err.message
|
data: err.message
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import { Message } from "./messaging";
|
|||||||
import { handleCastMessage } from "./components/cast";
|
import { handleCastMessage } from "./components/cast";
|
||||||
import { startDiscovery, stopDiscovery } from "./components/discovery";
|
import { startDiscovery, stopDiscovery } from "./components/discovery";
|
||||||
import { startMediaServer, stopMediaServer } from "./components/mediaServer";
|
import { startMediaServer, stopMediaServer } from "./components/mediaServer";
|
||||||
import { startReceiverSelector, stopReceiverSelector }
|
import {
|
||||||
from "./components/receiverSelector";
|
startReceiverSelector,
|
||||||
|
stopReceiverSelector
|
||||||
|
} from "./components/receiverSelector";
|
||||||
|
|
||||||
import { __applicationName, __applicationVersion } from "../../package.json";
|
import { __applicationName, __applicationVersion } from "../../package.json";
|
||||||
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
stopDiscovery();
|
stopDiscovery();
|
||||||
stopMediaServer();
|
stopMediaServer();
|
||||||
stopReceiverSelector();
|
stopReceiverSelector();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle incoming messages from the extension and forward
|
* Handle incoming messages from the extension and forward
|
||||||
* them to the appropriate handlers.
|
* them to the appropriate handlers.
|
||||||
@@ -41,10 +41,12 @@ decodeTransform.on("data", (message: Message) => {
|
|||||||
|
|
||||||
// Receiver selector
|
// Receiver selector
|
||||||
case "bridge:openReceiverSelector": {
|
case "bridge:openReceiverSelector": {
|
||||||
startReceiverSelector(message.data); break;
|
startReceiverSelector(message.data);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
case "bridge:closeReceiverSelector": {
|
case "bridge:closeReceiverSelector": {
|
||||||
stopReceiverSelector(); break;
|
stopReceiverSelector();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media server
|
// Media server
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { DecodeTransform, EncodeTransform } from "../../transforms";
|
import { DecodeTransform, EncodeTransform } from "../../transforms";
|
||||||
import { Message } from "../messaging";
|
import { Message } from "../messaging";
|
||||||
|
|
||||||
|
|
||||||
export const decodeTransform = new DecodeTransform();
|
export const decodeTransform = new DecodeTransform();
|
||||||
export const encodeTransform = new EncodeTransform();
|
export const encodeTransform = new EncodeTransform();
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a SubRip file and outputs text content as WebVTT.
|
* Reads a SubRip file and outputs text content as WebVTT.
|
||||||
*/
|
*/
|
||||||
export async function convertSrtToVtt(srtFilePath: string) {
|
export async function convertSrtToVtt(srtFilePath: string) {
|
||||||
const fileStream = fs.createReadStream(
|
const fileStream = fs.createReadStream(srtFilePath, { encoding: "utf-8" });
|
||||||
srtFilePath, { encoding: "utf-8" });
|
|
||||||
|
|
||||||
let fileContents = "";
|
let fileContents = "";
|
||||||
for await (let chunk of fileStream) {
|
for await (let chunk of fileStream) {
|
||||||
@@ -21,7 +19,6 @@ export async function convertSrtToVtt(srtFilePath: string) {
|
|||||||
fileContents += chunk.replace(/$\r\n/gm, "\n");
|
fileContents += chunk.replace(/$\r\n/gm, "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let vttText = "WEBVTT\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
|
* (followed by a new line), then any text content until a blank
|
||||||
* line.
|
* 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
|
* WebVTT is very similar to SubRip, the main differences being
|
||||||
|
|||||||
@@ -1,121 +1,110 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { Image
|
import {
|
||||||
, ReceiverStatus
|
Image,
|
||||||
, SenderApplication
|
ReceiverStatus,
|
||||||
, SenderMessage
|
SenderApplication,
|
||||||
, Volume } from "./components/cast/types";
|
SenderMessage,
|
||||||
|
Volume
|
||||||
import { ReceiverDevice
|
} from "./components/cast/types";
|
||||||
, ReceiverSelectionCast
|
|
||||||
, ReceiverSelectionStop } from "./types";
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
ReceiverDevice,
|
||||||
|
ReceiverSelectionCast,
|
||||||
|
ReceiverSelectionStop
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
interface CastSessionUpdated {
|
interface CastSessionUpdated {
|
||||||
sessionId: string
|
sessionId: string;
|
||||||
, statusText: string
|
statusText: string;
|
||||||
, namespaces: Array<{ name: string }>
|
namespaces: Array<{ name: string }>;
|
||||||
, volume: Volume
|
volume: Volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CastSessionCreated extends CastSessionUpdated {
|
interface CastSessionCreated extends CastSessionUpdated {
|
||||||
appId: string
|
appId: string;
|
||||||
, appImages: Image[]
|
appImages: Image[];
|
||||||
, displayName: string
|
displayName: string;
|
||||||
, receiverFriendlyName: string
|
receiverFriendlyName: string;
|
||||||
, senderApps: SenderApplication[]
|
senderApps: SenderApplication[];
|
||||||
, transportId: string
|
transportId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageDefinitions = {
|
type MessageDefinitions = {
|
||||||
"shim:castSessionCreated": CastSessionCreated
|
"shim:castSessionCreated": CastSessionCreated;
|
||||||
, "shim:castSessionUpdated": CastSessionUpdated
|
"shim:castSessionUpdated": CastSessionUpdated;
|
||||||
, "shim:castSessionStopped": {
|
"shim:castSessionStopped": {
|
||||||
sessionId: string
|
sessionId: string;
|
||||||
}
|
};
|
||||||
|
"shim:receivedCastSessionMessage": {
|
||||||
, "shim:receivedCastSessionMessage": {
|
sessionId: string;
|
||||||
sessionId: string
|
namespace: string;
|
||||||
, namespace: string
|
messageData: string;
|
||||||
, messageData: string
|
};
|
||||||
}
|
"shim:impl_sendCastMessage": {
|
||||||
|
sessionId: string;
|
||||||
, "shim:impl_sendCastMessage": {
|
messageId: string;
|
||||||
sessionId: string
|
error?: string;
|
||||||
, messageId: string
|
};
|
||||||
, error?: string
|
"bridge:createCastSession": {
|
||||||
}
|
appId: string;
|
||||||
|
receiverDevice: ReceiverDevice;
|
||||||
, "bridge:createCastSession": {
|
};
|
||||||
appId: string
|
"bridge:sendCastReceiverMessage": {
|
||||||
, receiverDevice: ReceiverDevice
|
sessionId: string;
|
||||||
}
|
messageData: SenderMessage;
|
||||||
, "bridge:sendCastReceiverMessage": {
|
messageId: string;
|
||||||
sessionId: string
|
};
|
||||||
, messageData: SenderMessage
|
"bridge:sendCastSessionMessage": {
|
||||||
, messageId: string
|
sessionId: string;
|
||||||
}
|
namespace: string;
|
||||||
, "bridge:sendCastSessionMessage": {
|
messageData: object | string;
|
||||||
sessionId: string
|
messageId: string;
|
||||||
, namespace: string
|
};
|
||||||
, messageData: object | string
|
"bridge:stopCastApp": { receiverDevice: ReceiverDevice };
|
||||||
, messageId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
, "bridge:stopCastApp": { receiverDevice: ReceiverDevice }
|
|
||||||
|
|
||||||
// Bridge messages
|
// Bridge messages
|
||||||
, "main:receiverSelector/selected": ReceiverSelectionCast
|
"main:receiverSelector/selected": ReceiverSelectionCast;
|
||||||
, "main:receiverSelector/stopped": ReceiverSelectionStop
|
"main:receiverSelector/stopped": ReceiverSelectionStop;
|
||||||
, "main:receiverSelector/cancelled": {}
|
"main:receiverSelector/cancelled": {};
|
||||||
, "main:receiverSelector/error": string
|
"main:receiverSelector/error": string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getInfo uses the old :/ form for compat with old bridge
|
* getInfo uses the old :/ form for compat with old bridge
|
||||||
* versions.
|
* versions.
|
||||||
*/
|
*/
|
||||||
, "bridge:getInfo": string
|
"bridge:getInfo": string;
|
||||||
, "bridge:/getInfo": string
|
"bridge:/getInfo": string;
|
||||||
|
"bridge:startDiscovery": {
|
||||||
, "bridge:startDiscovery": {
|
shouldWatchStatus: boolean;
|
||||||
shouldWatchStatus: boolean
|
};
|
||||||
}
|
"bridge:openReceiverSelector": string;
|
||||||
|
"bridge:closeReceiverSelector": {};
|
||||||
, "bridge:openReceiverSelector": string
|
"bridge:startMediaServer": {
|
||||||
, "bridge:closeReceiverSelector": {}
|
filePath: string;
|
||||||
|
port: number;
|
||||||
, "bridge:startMediaServer": {
|
};
|
||||||
filePath: string
|
"bridge:stopMediaServer": {};
|
||||||
, port: number
|
"mediaCast:mediaServerStarted": {
|
||||||
}
|
mediaPath: string;
|
||||||
, "bridge:stopMediaServer": {}
|
subtitlePaths: string[];
|
||||||
|
localAddress: string;
|
||||||
, "mediaCast:mediaServerStarted": {
|
};
|
||||||
mediaPath: string
|
"mediaCast:mediaServerStopped": {};
|
||||||
, subtitlePaths: string[]
|
"mediaCast:mediaServerError": {};
|
||||||
, localAddress: string
|
"main:serviceUp": ReceiverDevice;
|
||||||
}
|
"main:serviceDown": { id: string };
|
||||||
, "mediaCast:mediaServerStopped": {}
|
"main:updateReceiverStatus": {
|
||||||
, "mediaCast:mediaServerError": {}
|
id: string;
|
||||||
|
status: ReceiverStatus;
|
||||||
|
};
|
||||||
, "main:serviceUp": ReceiverDevice
|
"main:receiverDeviceUp": { receiverDevice: ReceiverDevice };
|
||||||
, "main:serviceDown": { id: string }
|
"main:receiverDeviceDown": { receiverDeviceId: string };
|
||||||
|
"main:receiverDeviceUpdated": {
|
||||||
, "main:updateReceiverStatus": {
|
receiverDeviceId: string;
|
||||||
id: string
|
status: ReceiverStatus;
|
||||||
, status: ReceiverStatus
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
, "main:receiverDeviceUp": { receiverDevice: ReceiverDevice }
|
|
||||||
, "main:receiverDeviceDown": { receiverDeviceId: string }
|
|
||||||
, "main:receiverDeviceUpdated": {
|
|
||||||
receiverDeviceId: string
|
|
||||||
, status: ReceiverStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface MessageBase<K extends keyof MessageDefinitions> {
|
interface MessageBase<K extends keyof MessageDefinitions> {
|
||||||
subject: K;
|
subject: K;
|
||||||
@@ -124,7 +113,7 @@ interface MessageBase<K extends keyof MessageDefinitions> {
|
|||||||
|
|
||||||
type Messages = {
|
type Messages = {
|
||||||
[K in keyof MessageDefinitions]: MessageBase<K>;
|
[K in keyof MessageDefinitions]: MessageBase<K>;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For better call semantics, make message data key optional if
|
* For better call semantics, make message data key optional if
|
||||||
@@ -137,5 +126,4 @@ type NarrowedMessage<L extends MessageBase<keyof MessageDefinitions>> =
|
|||||||
: L
|
: L
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
|
|
||||||
export type Message = NarrowedMessage<Messages[keyof Messages]>;
|
export type Message = NarrowedMessage<Messages[keyof Messages]>;
|
||||||
|
|||||||
@@ -2,17 +2,16 @@
|
|||||||
|
|
||||||
import { ReceiverStatus } from "./components/cast/types";
|
import { ReceiverStatus } from "./components/cast/types";
|
||||||
|
|
||||||
|
|
||||||
export enum ReceiverSelectorMediaType {
|
export enum ReceiverSelectorMediaType {
|
||||||
App = 1
|
App = 1,
|
||||||
, Tab = 2
|
Tab = 2,
|
||||||
, Screen = 4
|
Screen = 4,
|
||||||
, File = 8
|
File = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReceiverSelectionActionType {
|
export enum ReceiverSelectionActionType {
|
||||||
Cast = 1
|
Cast = 1,
|
||||||
, Stop = 2
|
Stop = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReceiverSelectionCast {
|
export interface ReceiverSelectionCast {
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import { Readable } from "stream";
|
|||||||
import minimist from "minimist";
|
import minimist from "minimist";
|
||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
|
|
||||||
import { DecodeTransform
|
import { DecodeTransform, EncodeTransform } from "./transforms";
|
||||||
, EncodeTransform } from "./transforms";
|
|
||||||
|
|
||||||
|
|
||||||
export function init(port: number) {
|
export function init(port: number) {
|
||||||
process.stdout.write("Starting WebSocket server... ");
|
process.stdout.write("Starting WebSocket server... ");
|
||||||
@@ -18,13 +16,12 @@ export function init(port: number) {
|
|||||||
console.log("Done!");
|
console.log("Done!");
|
||||||
});
|
});
|
||||||
|
|
||||||
wss.on("error", (err) => {
|
wss.on("error", err => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log("Failed!");
|
console.log("Failed!");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
wss.on("connection", socket => {
|
wss.on("connection", socket => {
|
||||||
// Stream for incoming WebSocket messages
|
// Stream for incoming WebSocket messages
|
||||||
const messageStream = new Readable({ objectMode: true });
|
const messageStream = new Readable({ objectMode: true });
|
||||||
@@ -35,27 +32,22 @@ export function init(port: number) {
|
|||||||
messageStream.push(JSON.parse(message));
|
messageStream.push(JSON.parse(message));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Daemon and bridge are the same binary, so spawn a new
|
* Daemon and bridge are the same binary, so spawn a new
|
||||||
* version of self in bridge mode.
|
* 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
|
// socket -> bridge.stdin
|
||||||
messageStream
|
messageStream.pipe(new EncodeTransform()).pipe(bridge.stdin);
|
||||||
.pipe(new EncodeTransform())
|
|
||||||
.pipe(bridge.stdin);
|
|
||||||
|
|
||||||
// bridge.stdout -> socket
|
// bridge.stdout -> socket
|
||||||
bridge.stdout
|
bridge.stdout.pipe(new DecodeTransform()).on("data", data => {
|
||||||
.pipe(new DecodeTransform())
|
// Socket can be CLOSING here
|
||||||
.on("data", data => {
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
// Socket can be CLOSING here
|
socket.send(JSON.stringify(data));
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
}
|
||||||
socket.send(JSON.stringify(data));
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle termination
|
// Handle termination
|
||||||
socket.on("close", () => bridge.kill());
|
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 Nullable<T> = T | null;
|
||||||
|
|
||||||
declare type DistributiveOmit<T, K extends keyof any> =
|
declare type DistributiveOmit<T, K extends keyof any> = T extends any
|
||||||
T extends any
|
? Omit<T, K>
|
||||||
? Omit<T, K>
|
: never;
|
||||||
: never;
|
|
||||||
|
|||||||
@@ -5,29 +5,28 @@ import minimist from "minimist";
|
|||||||
import { __applicationVersion } from "../package.json";
|
import { __applicationVersion } from "../package.json";
|
||||||
|
|
||||||
const argv = minimist(process.argv.slice(2), {
|
const argv = minimist(process.argv.slice(2), {
|
||||||
boolean: [ "daemon", "help", "version" ]
|
boolean: ["daemon", "help", "version"],
|
||||||
, string: [ "__name", "port" ]
|
string: ["__name", "port"],
|
||||||
, alias: {
|
alias: {
|
||||||
d: "daemon"
|
d: "daemon",
|
||||||
, h: "help"
|
h: "help",
|
||||||
, v: "version"
|
v: "version",
|
||||||
, p: "port"
|
p: "port"
|
||||||
}
|
},
|
||||||
, default: {
|
default: {
|
||||||
__name: path.basename(process.argv[0])
|
__name: path.basename(process.argv[0]),
|
||||||
, daemon: false
|
daemon: false,
|
||||||
, port: "9556"
|
port: "9556"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (argv.version) {
|
if (argv.version) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`v${__applicationVersion}`);
|
console.log(`v${__applicationVersion}`);
|
||||||
} else if (argv.help) {
|
} else if (argv.help) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(
|
console.log(
|
||||||
`Usage: ${argv.__name} [options]
|
`Usage: ${argv.__name} [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help Print usage info
|
-h, --help Print usage info
|
||||||
@@ -37,8 +36,8 @@ Options:
|
|||||||
options.
|
options.
|
||||||
-p, --port Set port number for WebSocket server. This must match the
|
-p, --port Set port number for WebSocket server. This must match the
|
||||||
port set in the extension options.
|
port set in the extension options.
|
||||||
`);
|
`
|
||||||
|
);
|
||||||
} else if (argv.daemon) {
|
} else if (argv.daemon) {
|
||||||
const port = parseInt(argv.port);
|
const port = parseInt(argv.port);
|
||||||
if (!port || port < 1025 || port > 65535) {
|
if (!port || port < 1025 || port > 65535) {
|
||||||
@@ -46,10 +45,9 @@ Options:
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
import("./daemon")
|
import("./daemon").then(daemon => {
|
||||||
.then(daemon => {
|
daemon.init(port);
|
||||||
daemon.init(port);
|
});
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
import("./bridge");
|
import("./bridge");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { Transform } from "stream";
|
import { Transform } from "stream";
|
||||||
import { Message } from "./bridge/messaging";
|
import { Message } from "./bridge/messaging";
|
||||||
|
|
||||||
|
|
||||||
type ResponseHandlerFunction = (message: Message) => Promise<any>;
|
type ResponseHandlerFunction = (message: Message) => Promise<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,29 +12,27 @@ type ResponseHandlerFunction = (message: Message) => Promise<any>;
|
|||||||
export class ResponseTransform extends Transform {
|
export class ResponseTransform extends Transform {
|
||||||
constructor(private _handler: ResponseHandlerFunction) {
|
constructor(private _handler: ResponseHandlerFunction) {
|
||||||
super({
|
super({
|
||||||
readableObjectMode: true
|
readableObjectMode: true,
|
||||||
, writableObjectMode: true
|
writableObjectMode: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public _transform(
|
public _transform(
|
||||||
chunk: Message
|
chunk: Message,
|
||||||
, _encoding: string
|
_encoding: string,
|
||||||
// tslint:disable-next-line:ban-types
|
// tslint:disable-next-line:ban-types
|
||||||
, callback: Function) {
|
callback: Function
|
||||||
|
) {
|
||||||
Promise.resolve(this._handler(chunk))
|
Promise.resolve(this._handler(chunk)).then(res => {
|
||||||
.then(res => {
|
if (res) {
|
||||||
if (res) {
|
callback(null, res);
|
||||||
callback(null, res);
|
} else {
|
||||||
} else {
|
callback(null);
|
||||||
callback(null);
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes input, decodes the message string, parses as JSON
|
* Takes input, decodes the message string, parses as JSON
|
||||||
* and outputs the parsed result.
|
* and outputs the parsed result.
|
||||||
@@ -52,53 +49,52 @@ export class DecodeTransform extends Transform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public _transform(
|
public _transform(
|
||||||
chunk: any
|
chunk: any,
|
||||||
, _encoding: string
|
_encoding: string,
|
||||||
// tslint:disable-next-line:ban-types
|
// tslint:disable-next-line:ban-types
|
||||||
, callback: Function) {
|
callback: Function
|
||||||
|
) {
|
||||||
|
// Append next chunk to buffer
|
||||||
|
this._messageBuffer = Buffer.concat([this._messageBuffer, chunk]);
|
||||||
|
|
||||||
// Append next chunk to buffer
|
for (;;) {
|
||||||
this._messageBuffer = Buffer.concat([
|
if (this._messageLength === undefined) {
|
||||||
this._messageBuffer
|
if (this._messageBuffer.length >= 4) {
|
||||||
, chunk
|
// Read message length and offset buffer
|
||||||
]);
|
this._messageLength = this._messageBuffer.readUInt32LE(0);
|
||||||
|
this._messageBuffer = this._messageBuffer.slice(4);
|
||||||
|
|
||||||
for (;;) {
|
// Next message chunk
|
||||||
if (this._messageLength === undefined) {
|
continue;
|
||||||
if (this._messageBuffer.length >= 4) {
|
}
|
||||||
// Read message length and offset buffer
|
} else {
|
||||||
this._messageLength = this._messageBuffer.readUInt32LE(0);
|
if (this._messageBuffer.length >= this._messageLength) {
|
||||||
this._messageBuffer = this._messageBuffer.slice(4);
|
const message = JSON.parse(
|
||||||
|
this._messageBuffer
|
||||||
|
.slice(0, this._messageLength)
|
||||||
|
.toString()
|
||||||
|
);
|
||||||
|
|
||||||
// Next message chunk
|
// Push message content
|
||||||
continue;
|
this.push(message);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this._messageBuffer.length >= this._messageLength) {
|
|
||||||
const message = JSON.parse(this._messageBuffer
|
|
||||||
.slice(0, this._messageLength)
|
|
||||||
.toString());
|
|
||||||
|
|
||||||
// Push message content
|
// Offset buffer to start of next message
|
||||||
this.push(message);
|
this._messageBuffer = this._messageBuffer.slice(
|
||||||
|
this._messageLength
|
||||||
|
);
|
||||||
|
this._messageLength = undefined;
|
||||||
|
|
||||||
// Offset buffer to start of next message
|
// Next message
|
||||||
this._messageBuffer = this._messageBuffer.slice(
|
continue;
|
||||||
this._messageLength);
|
}
|
||||||
this._messageLength = undefined;
|
}
|
||||||
|
|
||||||
// Next message
|
// No complete messages left
|
||||||
continue;
|
callback();
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No complete messages left
|
|
||||||
callback();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes input, encodes the message length and content and
|
* Takes input, encodes the message length and content and
|
||||||
@@ -112,11 +108,11 @@ export class EncodeTransform extends Transform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public _transform(
|
public _transform(
|
||||||
chunk: any
|
chunk: any,
|
||||||
, _encoding: string
|
_encoding: string,
|
||||||
// tslint:disable-next-line:ban-types
|
// tslint:disable-next-line:ban-types
|
||||||
, callback: Function) {
|
callback: Function
|
||||||
|
) {
|
||||||
const messageLength = Buffer.alloc(4);
|
const messageLength = Buffer.alloc(4);
|
||||||
const message = Buffer.from(JSON.stringify(chunk));
|
const message = Buffer.from(JSON.stringify(chunk));
|
||||||
|
|
||||||
@@ -124,9 +120,6 @@ export class EncodeTransform extends Transform {
|
|||||||
messageLength.writeUInt32LE(message.length, 0);
|
messageLength.writeUInt32LE(message.length, 0);
|
||||||
|
|
||||||
// Output joined length and content
|
// Output joined length and content
|
||||||
callback(null, Buffer.concat([
|
callback(null, Buffer.concat([messageLength, message]));
|
||||||
messageLength
|
|
||||||
, message
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ import logger from "../lib/logger";
|
|||||||
import messaging, { Message, Port } from "../messaging";
|
import messaging, { Message, Port } from "../messaging";
|
||||||
import options from "../lib/options";
|
import options from "../lib/options";
|
||||||
|
|
||||||
import { ReceiverSelectionActionType
|
import {
|
||||||
, ReceiverSelectorMediaType } from "./receiverSelector";
|
ReceiverSelectionActionType,
|
||||||
|
ReceiverSelectorMediaType
|
||||||
|
} from "./receiverSelector";
|
||||||
|
|
||||||
import ReceiverSelectorManager
|
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
|
||||||
from "./receiverSelector/ReceiverSelectorManager";
|
|
||||||
|
|
||||||
import receiverDevices from "./receiverDevices";
|
import receiverDevices from "./receiverDevices";
|
||||||
|
|
||||||
|
|
||||||
type AnyPort = Port | MessagePort;
|
type AnyPort = Port | MessagePort;
|
||||||
|
|
||||||
export interface Shim {
|
export interface Shim {
|
||||||
@@ -25,8 +25,7 @@ export interface Shim {
|
|||||||
appId?: string;
|
appId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default new (class ShimManager {
|
||||||
export default new class ShimManager {
|
|
||||||
private activeShims = new Set<Shim>();
|
private activeShims = new Set<Shim>();
|
||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
@@ -40,8 +39,8 @@ export default new class ShimManager {
|
|||||||
receiverDevices.addEventListener("receiverDeviceUp", ev => {
|
receiverDevices.addEventListener("receiverDeviceUp", ev => {
|
||||||
for (const shim of this.activeShims) {
|
for (const shim of this.activeShims) {
|
||||||
shim.contentPort.postMessage({
|
shim.contentPort.postMessage({
|
||||||
subject: "shim:serviceUp"
|
subject: "shim:serviceUp",
|
||||||
, data: { receiverDevice: ev.detail.receiverDevice }
|
data: { receiverDevice: ev.detail.receiverDevice }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -49,8 +48,8 @@ export default new class ShimManager {
|
|||||||
receiverDevices.addEventListener("receiverDeviceDown", ev => {
|
receiverDevices.addEventListener("receiverDeviceDown", ev => {
|
||||||
for (const shim of this.activeShims) {
|
for (const shim of this.activeShims) {
|
||||||
shim.contentPort.postMessage({
|
shim.contentPort.postMessage({
|
||||||
subject: "shim:serviceDown"
|
subject: "shim:serviceDown",
|
||||||
, data: { receiverDeviceId: ev.detail.receiverDeviceId }
|
data: { receiverDeviceId: ev.detail.receiverDeviceId }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -74,19 +73,19 @@ export default new class ShimManager {
|
|||||||
: this.createShimFromContent(port));
|
: this.createShimFromContent(port));
|
||||||
|
|
||||||
shim.contentPort.postMessage({
|
shim.contentPort.postMessage({
|
||||||
subject: "shim:initialized"
|
subject: "shim:initialized",
|
||||||
, data: await bridge.getInfo()
|
data: await bridge.getInfo()
|
||||||
});
|
});
|
||||||
|
|
||||||
this.activeShims.add(shim);
|
this.activeShims.add(shim);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createShimFromBackground(
|
private async createShimFromBackground(
|
||||||
contentPort: MessagePort): Promise<Shim> {
|
contentPort: MessagePort
|
||||||
|
): Promise<Shim> {
|
||||||
const shim: Shim = {
|
const shim: Shim = {
|
||||||
bridgePort: await bridge.connect()
|
bridgePort: await bridge.connect(),
|
||||||
, contentPort
|
contentPort
|
||||||
};
|
};
|
||||||
|
|
||||||
shim.bridgePort.onDisconnect.addListener(() => {
|
shim.bridgePort.onDisconnect.addListener(() => {
|
||||||
@@ -105,12 +104,14 @@ export default new class ShimManager {
|
|||||||
return shim;
|
return shim;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createShimFromContent(
|
private async createShimFromContent(contentPort: Port): Promise<Shim> {
|
||||||
contentPort: Port): Promise<Shim> {
|
if (
|
||||||
|
contentPort.sender?.tab?.id === undefined ||
|
||||||
if (contentPort.sender?.tab?.id === undefined
|
contentPort.sender?.frameId === undefined
|
||||||
|| contentPort.sender?.frameId === undefined) {
|
) {
|
||||||
throw logger.error("Content shim created with an invalid port context.");
|
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.
|
* tab/frame ID, disconnect it.
|
||||||
*/
|
*/
|
||||||
for (const activeShim of this.activeShims) {
|
for (const activeShim of this.activeShims) {
|
||||||
if (activeShim.contentTabId === contentPort.sender.tab.id
|
if (
|
||||||
&& activeShim.contentFrameId === contentPort.sender.frameId) {
|
activeShim.contentTabId === contentPort.sender.tab.id &&
|
||||||
|
activeShim.contentFrameId === contentPort.sender.frameId
|
||||||
|
) {
|
||||||
activeShim.bridgePort.disconnect();
|
activeShim.bridgePort.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const shim: Shim = {
|
const shim: Shim = {
|
||||||
bridgePort: await bridge.connect()
|
bridgePort: await bridge.connect(),
|
||||||
, contentPort
|
contentPort,
|
||||||
, contentTabId: contentPort.sender.tab.id
|
contentTabId: contentPort.sender.tab.id,
|
||||||
, contentFrameId: contentPort.sender.frameId
|
contentFrameId: contentPort.sender.frameId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const onContentPortMessage = (message: Message) => {
|
const onContentPortMessage = (message: Message) => {
|
||||||
this.handleContentMessage(shim, message);
|
this.handleContentMessage(shim, message);
|
||||||
};
|
};
|
||||||
@@ -150,7 +152,6 @@ export default new class ShimManager {
|
|||||||
this.activeShims.delete(shim);
|
this.activeShims.delete(shim);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
shim.bridgePort.onDisconnect.addListener(onDisconnect);
|
shim.bridgePort.onDisconnect.addListener(onDisconnect);
|
||||||
shim.bridgePort.onMessage.addListener(onBridgePortMessage);
|
shim.bridgePort.onMessage.addListener(onBridgePortMessage);
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ export default new class ShimManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleContentMessage(shim: Shim, message: Message) {
|
private async handleContentMessage(shim: Shim, message: Message) {
|
||||||
const [ destination ] = message.subject.split(":");
|
const [destination] = message.subject.split(":");
|
||||||
if (destination === "bridge") {
|
if (destination === "bridge") {
|
||||||
shim.bridgePort.postMessage(message);
|
shim.bridgePort.postMessage(message);
|
||||||
}
|
}
|
||||||
@@ -172,8 +173,8 @@ export default new class ShimManager {
|
|||||||
|
|
||||||
for (const receiverDevice of receiverDevices.getDevices()) {
|
for (const receiverDevice of receiverDevices.getDevices()) {
|
||||||
shim.contentPort.postMessage({
|
shim.contentPort.postMessage({
|
||||||
subject: "shim:serviceUp"
|
subject: "shim:serviceUp",
|
||||||
, data: { receiverDevice }
|
data: { receiverDevice }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,15 +182,21 @@ export default new class ShimManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "main:selectReceiver": {
|
case "main:selectReceiver": {
|
||||||
if (shim.contentTabId === undefined
|
if (
|
||||||
|| shim.contentFrameId === undefined) {
|
shim.contentTabId === undefined ||
|
||||||
throw logger.error("Shim associated with content sender missing tab/frame ID");
|
shim.contentFrameId === undefined
|
||||||
|
) {
|
||||||
|
throw logger.error(
|
||||||
|
"Shim associated with content sender missing tab/frame ID"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const selection =
|
const selection =
|
||||||
await ReceiverSelectorManager.getSelection(
|
await ReceiverSelectorManager.getSelection(
|
||||||
shim.contentTabId, shim.contentFrameId);
|
shim.contentTabId,
|
||||||
|
shim.contentFrameId
|
||||||
|
);
|
||||||
|
|
||||||
// Handle cancellation
|
// Handle cancellation
|
||||||
if (!selection) {
|
if (!selection) {
|
||||||
@@ -207,25 +214,26 @@ export default new class ShimManager {
|
|||||||
* been changed, we need to cancel the current
|
* been changed, we need to cancel the current
|
||||||
* sender and switch it out for the right one.
|
* sender and switch it out for the right one.
|
||||||
*/
|
*/
|
||||||
if (selection.mediaType !==
|
if (
|
||||||
ReceiverSelectorMediaType.App) {
|
selection.mediaType !==
|
||||||
|
ReceiverSelectorMediaType.App
|
||||||
|
) {
|
||||||
shim.contentPort.postMessage({
|
shim.contentPort.postMessage({
|
||||||
subject: "shim:selectReceiver/cancelled"
|
subject: "shim:selectReceiver/cancelled"
|
||||||
});
|
});
|
||||||
|
|
||||||
loadSender({
|
loadSender({
|
||||||
tabId: shim.contentTabId
|
tabId: shim.contentTabId,
|
||||||
, frameId: shim.contentFrameId
|
frameId: shim.contentFrameId,
|
||||||
, selection
|
selection
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
shim.contentPort.postMessage({
|
shim.contentPort.postMessage({
|
||||||
subject: "shim:selectReceiver/selected"
|
subject: "shim:selectReceiver/selected",
|
||||||
, data: selection
|
data: selection
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -233,8 +241,8 @@ export default new class ShimManager {
|
|||||||
|
|
||||||
case ReceiverSelectionActionType.Stop: {
|
case ReceiverSelectionActionType.Stop: {
|
||||||
shim.contentPort.postMessage({
|
shim.contentPort.postMessage({
|
||||||
subject: "shim:selectReceiver/stopped"
|
subject: "shim:selectReceiver/stopped",
|
||||||
, data: selection
|
data: selection
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -257,7 +265,8 @@ export default new class ShimManager {
|
|||||||
case "main:sessionCreated": {
|
case "main:sessionCreated": {
|
||||||
const selector = await ReceiverSelectorManager.getSelector();
|
const selector = await ReceiverSelectorManager.getSelector();
|
||||||
const shouldClose = await options.get(
|
const shouldClose = await options.get(
|
||||||
"receiverSelectorWaitForConnection");
|
"receiverSelectorWaitForConnection"
|
||||||
|
);
|
||||||
|
|
||||||
if (selector.isOpen && shouldClose) {
|
if (selector.isOpen && shouldClose) {
|
||||||
selector.close();
|
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 options from "../lib/options";
|
||||||
import bridge, { BridgeInfo } from "../lib/bridge";
|
import bridge, { BridgeInfo } from "../lib/bridge";
|
||||||
|
|
||||||
import ReceiverSelectorManager
|
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
|
||||||
from "./receiverSelector/ReceiverSelectorManager";
|
|
||||||
|
|
||||||
import ShimManager from "./ShimManager";
|
import ShimManager from "./ShimManager";
|
||||||
|
|
||||||
@@ -17,10 +16,8 @@ import receiverDevices from "./receiverDevices";
|
|||||||
import { initMenus } from "./menus";
|
import { initMenus } from "./menus";
|
||||||
import { initWhitelist } from "./whitelist";
|
import { initWhitelist } from "./whitelist";
|
||||||
|
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On install, set the default options before initializing the
|
* On install, set the default options before initializing the
|
||||||
* extension. On update, handle any unset values and set to
|
* 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
|
* Sets up media overlay content script and handles toggling
|
||||||
* on options change.
|
* on options change.
|
||||||
@@ -62,10 +58,10 @@ async function initMediaOverlay() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
contentScript = await browser.contentScripts.register({
|
contentScript = await browser.contentScripts.register({
|
||||||
allFrames: true
|
allFrames: true,
|
||||||
, js: [{ file: "senders/media/overlay/overlayContentLoader.js" }]
|
js: [{ file: "senders/media/overlay/overlayContentLoader.js" }],
|
||||||
, matches: [ "<all_urls>" ]
|
matches: ["<all_urls>"],
|
||||||
, runAt: "document_start"
|
runAt: "document_start"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Failed to register media overlay");
|
logger.error("Failed to register media overlay");
|
||||||
@@ -76,7 +72,6 @@ async function initMediaOverlay() {
|
|||||||
await contentScript?.unregister();
|
await contentScript?.unregister();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
registerMediaOverlayContentScript();
|
registerMediaOverlayContentScript();
|
||||||
|
|
||||||
// Update if toggled
|
// Update if toggled
|
||||||
@@ -90,7 +85,6 @@ async function initMediaOverlay() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the bridge can be reached and is compatible
|
* Checks whether the bridge can be reached and is compatible
|
||||||
* with the current version of the extension. If not, triggers
|
* with the current version of the extension. If not, triggers
|
||||||
@@ -113,10 +107,11 @@ async function notifyBridgeCompat() {
|
|||||||
logger.info("... bridge incompatible!");
|
logger.info("... bridge incompatible!");
|
||||||
|
|
||||||
const updateNotificationId = await browser.notifications.create({
|
const updateNotificationId = await browser.notifications.create({
|
||||||
type: "basic"
|
type: "basic",
|
||||||
, title: `${
|
title: `${_("extensionName")} — ${_(
|
||||||
_("extensionName")} — ${_("optionsBridgeIssueStatusTitle")}`
|
"optionsBridgeIssueStatusTitle"
|
||||||
, message: info.isVersionOlder
|
)}`,
|
||||||
|
message: info.isVersionOlder
|
||||||
? _("optionsBridgeOlderAction")
|
? _("optionsBridgeOlderAction")
|
||||||
: _("optionsBridgeNewerAction")
|
: _("optionsBridgeNewerAction")
|
||||||
});
|
});
|
||||||
@@ -127,14 +122,12 @@ async function notifyBridgeCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
browser.tabs.create({
|
browser.tabs.create({
|
||||||
url: `https://github.com/hensm/fx_cast/releases/tag/v${
|
url: `https://github.com/hensm/fx_cast/releases/tag/v${info.expectedVersion}`
|
||||||
info.expectedVersion}`
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
@@ -163,14 +156,13 @@ async function init() {
|
|||||||
await initWhitelist();
|
await initWhitelist();
|
||||||
await initMediaOverlay();
|
await initMediaOverlay();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the browser action is clicked, open a receiver
|
* When the browser action is clicked, open a receiver
|
||||||
* selector and load a sender for the response. The
|
* selector and load a sender for the response. The
|
||||||
* mirroring sender is loaded into the current tab at the
|
* mirroring sender is loaded into the current tab at the
|
||||||
* top-level frame.
|
* top-level frame.
|
||||||
*/
|
*/
|
||||||
browser.browserAction.onClicked.addListener(async tab => {
|
browser.browserAction.onClicked.addListener(async tab => {
|
||||||
if (tab.id === undefined) {
|
if (tab.id === undefined) {
|
||||||
throw logger.error("Tab ID not found in browser action handler.");
|
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);
|
const selection = await ReceiverSelectorManager.getSelection(tab.id);
|
||||||
if (selection) {
|
if (selection) {
|
||||||
loadSender({
|
loadSender({
|
||||||
tabId: tab.id
|
tabId: tab.id,
|
||||||
, frameId: 0
|
frameId: 0,
|
||||||
, selection
|
selection
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,22 +6,21 @@ import options from "../lib/options";
|
|||||||
|
|
||||||
import { stringify } from "../lib/utils";
|
import { stringify } from "../lib/utils";
|
||||||
|
|
||||||
import { ReceiverSelectionActionType
|
import {
|
||||||
, ReceiverSelectorMediaType } from "./receiverSelector";
|
ReceiverSelectionActionType,
|
||||||
|
ReceiverSelectorMediaType
|
||||||
|
} from "./receiverSelector";
|
||||||
|
|
||||||
import ReceiverSelectorManager
|
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
|
||||||
from "./receiverSelector/ReceiverSelectorManager";
|
|
||||||
|
|
||||||
const _ = browser.i18n.getMessage;
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
|
|
||||||
const URL_PATTERN_HTTP = "http://*/*";
|
const URL_PATTERN_HTTP = "http://*/*";
|
||||||
const URL_PATTERN_HTTPS = "https://*/*";
|
const URL_PATTERN_HTTPS = "https://*/*";
|
||||||
const URL_PATTERN_FILE = "file://*/*";
|
const URL_PATTERN_FILE = "file://*/*";
|
||||||
|
|
||||||
const URL_PATTERNS_REMOTE = [ URL_PATTERN_HTTP, URL_PATTERN_HTTPS ];
|
const URL_PATTERNS_REMOTE = [URL_PATTERN_HTTP, URL_PATTERN_HTTPS];
|
||||||
const URL_PATTERNS_ALL = [ ...URL_PATTERNS_REMOTE, URL_PATTERN_FILE ];
|
const URL_PATTERNS_ALL = [...URL_PATTERNS_REMOTE, URL_PATTERN_FILE];
|
||||||
|
|
||||||
|
|
||||||
type MenuId = string | number;
|
type MenuId = string | number;
|
||||||
|
|
||||||
@@ -39,44 +38,44 @@ export async function initMenus() {
|
|||||||
|
|
||||||
// Global "Cast..." menu item
|
// Global "Cast..." menu item
|
||||||
menuIdCast = browser.menus.create({
|
menuIdCast = browser.menus.create({
|
||||||
contexts: [ "browser_action", "page", "tools_menu" ]
|
contexts: ["browser_action", "page", "tools_menu"],
|
||||||
, title: _("contextCast")
|
title: _("contextCast")
|
||||||
});
|
});
|
||||||
|
|
||||||
// <video>/<audio> "Cast..." context menu item
|
// <video>/<audio> "Cast..." context menu item
|
||||||
menuIdMediaCast = browser.menus.create({
|
menuIdMediaCast = browser.menus.create({
|
||||||
contexts: [ "audio", "video", "image" ]
|
contexts: ["audio", "video", "image"],
|
||||||
, title: _("contextCast")
|
title: _("contextCast"),
|
||||||
, visible: opts.mediaEnabled
|
visible: opts.mediaEnabled,
|
||||||
, targetUrlPatterns: opts.localMediaEnabled
|
targetUrlPatterns: opts.localMediaEnabled
|
||||||
? URL_PATTERNS_ALL
|
? URL_PATTERNS_ALL
|
||||||
: URL_PATTERNS_REMOTE
|
: URL_PATTERNS_REMOTE
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
menuIdWhitelist = browser.menus.create({
|
menuIdWhitelist = browser.menus.create({
|
||||||
contexts: [ "browser_action" ]
|
contexts: ["browser_action"],
|
||||||
, title: _("contextAddToWhitelist")
|
title: _("contextAddToWhitelist"),
|
||||||
, enabled: false
|
enabled: false
|
||||||
});
|
});
|
||||||
|
|
||||||
menuIdWhitelistRecommended = browser.menus.create({
|
menuIdWhitelistRecommended = browser.menus.create({
|
||||||
title: _("contextAddToWhitelistRecommended")
|
title: _("contextAddToWhitelistRecommended"),
|
||||||
, parentId: menuIdWhitelist
|
parentId: menuIdWhitelist
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.menus.create({
|
browser.menus.create({
|
||||||
type: "separator"
|
type: "separator",
|
||||||
, parentId: menuIdWhitelist
|
parentId: menuIdWhitelist
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||||
if (info.parentMenuItemId === menuIdWhitelist) {
|
if (info.parentMenuItemId === menuIdWhitelist) {
|
||||||
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
|
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
|
||||||
if (!pattern) {
|
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");
|
const whitelist = await options.get("userAgentWhitelist");
|
||||||
@@ -99,7 +98,9 @@ browser.menus.onClicked.addListener(async (info, tab) => {
|
|||||||
switch (info.menuItemId) {
|
switch (info.menuItemId) {
|
||||||
case menuIdCast: {
|
case menuIdCast: {
|
||||||
const selection = await ReceiverSelectorManager.getSelection(
|
const selection = await ReceiverSelectorManager.getSelection(
|
||||||
tab.id, info.frameId);
|
tab.id,
|
||||||
|
info.frameId
|
||||||
|
);
|
||||||
|
|
||||||
// Selection cancelled
|
// Selection cancelled
|
||||||
if (!selection) {
|
if (!selection) {
|
||||||
@@ -107,9 +108,9 @@ browser.menus.onClicked.addListener(async (info, tab) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadSender({
|
loadSender({
|
||||||
tabId: tab.id
|
tabId: tab.id,
|
||||||
, frameId: info.frameId
|
frameId: info.frameId,
|
||||||
, selection
|
selection
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -117,7 +118,10 @@ browser.menus.onClicked.addListener(async (info, tab) => {
|
|||||||
|
|
||||||
case menuIdMediaCast: {
|
case menuIdMediaCast: {
|
||||||
const selection = await ReceiverSelectorManager.getSelection(
|
const selection = await ReceiverSelectorManager.getSelection(
|
||||||
tab.id, info.frameId, true);
|
tab.id,
|
||||||
|
info.frameId,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Selection cancelled
|
// Selection cancelled
|
||||||
if (!selection) {
|
if (!selection) {
|
||||||
@@ -130,30 +134,26 @@ browser.menus.onClicked.addListener(async (info, tab) => {
|
|||||||
* If the selected media type is App, that refers to the
|
* If the selected media type is App, that refers to the
|
||||||
* media sender in this context, so load media sender.
|
* media sender in this context, so load media sender.
|
||||||
*/
|
*/
|
||||||
if (selection.mediaType ===
|
if (selection.mediaType === ReceiverSelectorMediaType.App) {
|
||||||
ReceiverSelectorMediaType.App) {
|
|
||||||
|
|
||||||
await browser.tabs.executeScript(tab.id, {
|
await browser.tabs.executeScript(tab.id, {
|
||||||
code: stringify`
|
code: stringify`
|
||||||
window.receiver = ${selection.receiver};
|
window.receiver = ${selection.receiver};
|
||||||
window.mediaUrl = ${info.srcUrl};
|
window.mediaUrl = ${info.srcUrl};
|
||||||
window.targetElementId = ${
|
window.targetElementId = ${info.targetElementId};
|
||||||
info.targetElementId};
|
`,
|
||||||
`
|
frameId: info.frameId
|
||||||
, frameId: info.frameId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await browser.tabs.executeScript(tab.id, {
|
await browser.tabs.executeScript(tab.id, {
|
||||||
file: "senders/media/index.js"
|
file: "senders/media/index.js",
|
||||||
, frameId: info.frameId
|
frameId: info.frameId
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Handle other responses
|
// Handle other responses
|
||||||
loadSender({
|
loadSender({
|
||||||
tabId: tab.id
|
tabId: tab.id,
|
||||||
, frameId: info.frameId
|
frameId: info.frameId,
|
||||||
, selection
|
selection
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ browser.menus.onShown.addListener(async info => {
|
|||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [ menuId ] of whitelistChildMenuPatterns) {
|
for (const [menuId] of whitelistChildMenuPatterns) {
|
||||||
// Clear all page-specific temporary menus
|
// Clear all page-specific temporary menus
|
||||||
if (menuId !== menuIdWhitelistRecommended) {
|
if (menuId !== menuIdWhitelistRecommended) {
|
||||||
browser.menus.remove(menuId);
|
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
|
// If there is more than one subdomain, get the base domain
|
||||||
const baseDomain = (url.hostname.match(/\./g) || []).length > 1
|
const baseDomain =
|
||||||
? url.hostname.substring(url.hostname.indexOf(".") + 1)
|
(url.hostname.match(/\./g) || []).length > 1
|
||||||
: url.hostname;
|
? url.hostname.substring(url.hostname.indexOf(".") + 1)
|
||||||
|
: url.hostname;
|
||||||
|
|
||||||
const portlessOrigin = `${url.protocol}//${url.hostname}`;
|
const portlessOrigin = `${url.protocol}//${url.hostname}`;
|
||||||
|
|
||||||
@@ -253,17 +254,17 @@ browser.menus.onShown.addListener(async info => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
whitelistChildMenuPatterns.set(
|
whitelistChildMenuPatterns.set(
|
||||||
menuIdWhitelistRecommended, patternRecommended);
|
menuIdWhitelistRecommended,
|
||||||
|
patternRecommended
|
||||||
|
);
|
||||||
|
|
||||||
if (url.search) {
|
if (url.search) {
|
||||||
const whitelistSearchMenuId = browser.menus.create({
|
const whitelistSearchMenuId = browser.menus.create({
|
||||||
title: _("contextAddToWhitelistAdvancedAdd", patternSearch)
|
title: _("contextAddToWhitelistAdvancedAdd", patternSearch),
|
||||||
, parentId: menuIdWhitelist
|
parentId: menuIdWhitelist
|
||||||
});
|
});
|
||||||
|
|
||||||
whitelistChildMenuPatterns.set(
|
whitelistChildMenuPatterns.set(whitelistSearchMenuId, patternSearch);
|
||||||
whitelistSearchMenuId, patternSearch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -275,65 +276,63 @@ browser.menus.onShown.addListener(async info => {
|
|||||||
? url.pathname.substring(0, url.pathname.length - 1)
|
? url.pathname.substring(0, url.pathname.length - 1)
|
||||||
: url.pathname;
|
: url.pathname;
|
||||||
|
|
||||||
const pathSegments = pathTrimmed.split("/")
|
const pathSegments = pathTrimmed
|
||||||
.filter(segment => segment)
|
.split("/")
|
||||||
.reverse();
|
.filter(segment => segment)
|
||||||
|
.reverse();
|
||||||
|
|
||||||
if (pathSegments.length) {
|
if (pathSegments.length) {
|
||||||
for (let i = 0; i < pathSegments.length; i++) {
|
for (let i = 0; i < pathSegments.length; i++) {
|
||||||
const partialPath = pathSegments
|
const partialPath = pathSegments.slice(i).reverse().join("/");
|
||||||
.slice(i)
|
|
||||||
.reverse()
|
|
||||||
.join("/");
|
|
||||||
|
|
||||||
const pattern = `${portlessOrigin}/${partialPath}/*`;
|
const pattern = `${portlessOrigin}/${partialPath}/*`;
|
||||||
|
|
||||||
const partialPathMenuId = browser.menus.create({
|
const partialPathMenuId = browser.menus.create({
|
||||||
title: _("contextAddToWhitelistAdvancedAdd", pattern)
|
title: _("contextAddToWhitelistAdvancedAdd", pattern),
|
||||||
, parentId: menuIdWhitelist
|
parentId: menuIdWhitelist
|
||||||
});
|
});
|
||||||
|
|
||||||
whitelistChildMenuPatterns.set(
|
whitelistChildMenuPatterns.set(partialPathMenuId, pattern);
|
||||||
partialPathMenuId, pattern);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wildcardProtocolMenuId = browser.menus.create({
|
const wildcardProtocolMenuId = browser.menus.create({
|
||||||
title: _("contextAddToWhitelistAdvancedAdd"
|
title: _("contextAddToWhitelistAdvancedAdd", patternWildcardProtocol),
|
||||||
, patternWildcardProtocol)
|
parentId: menuIdWhitelist
|
||||||
, parentId: menuIdWhitelist
|
|
||||||
});
|
});
|
||||||
|
|
||||||
whitelistChildMenuPatterns.set(
|
whitelistChildMenuPatterns.set(
|
||||||
wildcardProtocolMenuId, patternWildcardProtocol);
|
wildcardProtocolMenuId,
|
||||||
|
patternWildcardProtocol
|
||||||
|
);
|
||||||
|
|
||||||
const wildcardSubdomainMenuId = browser.menus.create({
|
const wildcardSubdomainMenuId = browser.menus.create({
|
||||||
title: _("contextAddToWhitelistAdvancedAdd"
|
title: _("contextAddToWhitelistAdvancedAdd", patternWildcardSubdomain),
|
||||||
, patternWildcardSubdomain)
|
parentId: menuIdWhitelist
|
||||||
, parentId: menuIdWhitelist
|
|
||||||
});
|
});
|
||||||
|
|
||||||
whitelistChildMenuPatterns.set(
|
whitelistChildMenuPatterns.set(
|
||||||
wildcardSubdomainMenuId, patternWildcardSubdomain);
|
wildcardSubdomainMenuId,
|
||||||
|
patternWildcardSubdomain
|
||||||
|
);
|
||||||
|
|
||||||
const wildcardProtocolAndSubdomainMenuId = browser.menus.create({
|
const wildcardProtocolAndSubdomainMenuId = browser.menus.create({
|
||||||
title: _("contextAddToWhitelistAdvancedAdd"
|
title: _(
|
||||||
, patternWildcardProtocolAndSubdomain)
|
"contextAddToWhitelistAdvancedAdd",
|
||||||
, parentId: menuIdWhitelist
|
patternWildcardProtocolAndSubdomain
|
||||||
|
),
|
||||||
|
parentId: menuIdWhitelist
|
||||||
});
|
});
|
||||||
|
|
||||||
whitelistChildMenuPatterns.set(
|
whitelistChildMenuPatterns.set(
|
||||||
wildcardProtocolAndSubdomainMenuId
|
wildcardProtocolAndSubdomainMenuId,
|
||||||
, patternWildcardProtocolAndSubdomain);
|
patternWildcardProtocolAndSubdomain
|
||||||
|
);
|
||||||
|
|
||||||
await browser.menus.refresh();
|
await browser.menus.refresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
options.addEventListener("changed", async ev => {
|
options.addEventListener("changed", async ev => {
|
||||||
const alteredOpts = ev.detail;
|
const alteredOpts = ev.detail;
|
||||||
const newOpts = await options.getAll();
|
const newOpts = await options.getAll();
|
||||||
|
|||||||
@@ -8,17 +8,16 @@ import { Message, Port } from "../messaging";
|
|||||||
import { ReceiverDevice } from "../types";
|
import { ReceiverDevice } from "../types";
|
||||||
import { ReceiverStatus } from "../shim/cast/types";
|
import { ReceiverStatus } from "../shim/cast/types";
|
||||||
|
|
||||||
|
|
||||||
interface EventMap {
|
interface EventMap {
|
||||||
"receiverDeviceUp": { receiverDevice: ReceiverDevice }
|
receiverDeviceUp: { receiverDevice: ReceiverDevice };
|
||||||
, "receiverDeviceDown": { receiverDeviceId: string }
|
receiverDeviceDown: { receiverDeviceId: string };
|
||||||
, "receiverDeviceUpdated": {
|
receiverDeviceUpdated: {
|
||||||
receiverDeviceId: string
|
receiverDeviceId: string;
|
||||||
, status: ReceiverStatus
|
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
|
* Map of receiver device IDs to devices. Updated as
|
||||||
* receiverDevice messages are received from the bridge.
|
* receiverDevice messages are received from the bridge.
|
||||||
@@ -27,7 +26,6 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
|
|
||||||
private bridgePort?: Port;
|
private bridgePort?: Port;
|
||||||
|
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (!this.bridgePort) {
|
if (!this.bridgePort) {
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
@@ -47,8 +45,8 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
port.onDisconnect.addListener(this.onBridgeDisconnect);
|
port.onDisconnect.addListener(this.onBridgeDisconnect);
|
||||||
|
|
||||||
port.postMessage({
|
port.postMessage({
|
||||||
subject: "bridge:startDiscovery"
|
subject: "bridge:startDiscovery",
|
||||||
, data: {
|
data: {
|
||||||
// Also send back status messages
|
// Also send back status messages
|
||||||
shouldWatchStatus: true
|
shouldWatchStatus: true
|
||||||
}
|
}
|
||||||
@@ -69,15 +67,17 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
*/
|
*/
|
||||||
stopReceiverApp(receiverDeviceId: string) {
|
stopReceiverApp(receiverDeviceId: string) {
|
||||||
if (!this.bridgePort) {
|
if (!this.bridgePort) {
|
||||||
logger.error("Failed to stop receiver device, no bridge connection");
|
logger.error(
|
||||||
|
"Failed to stop receiver device, no bridge connection"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const receiverDevice = this.receiverDevices.get(receiverDeviceId);
|
const receiverDevice = this.receiverDevices.get(receiverDeviceId);
|
||||||
if (receiverDevice) {
|
if (receiverDevice) {
|
||||||
this.bridgePort.postMessage({
|
this.bridgePort.postMessage({
|
||||||
subject: "bridge:stopCastApp"
|
subject: "bridge:stopCastApp",
|
||||||
, data: { receiverDevice }
|
data: { receiverDevice }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,10 +89,10 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
|
|
||||||
this.receiverDevices.set(receiverDevice.id, receiverDevice);
|
this.receiverDevices.set(receiverDevice.id, receiverDevice);
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("receiverDeviceUp"
|
new CustomEvent("receiverDeviceUp", {
|
||||||
, {
|
detail: { receiverDevice }
|
||||||
detail: { receiverDevice }
|
})
|
||||||
}));
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -104,10 +104,10 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
this.receiverDevices.delete(receiverDeviceId);
|
this.receiverDevices.delete(receiverDeviceId);
|
||||||
}
|
}
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("receiverDeviceDown"
|
new CustomEvent("receiverDeviceDown", {
|
||||||
, {
|
detail: { receiverDeviceId }
|
||||||
detail: { receiverDeviceId }
|
})
|
||||||
}));
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -115,10 +115,12 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
case "main:receiverDeviceUpdated": {
|
case "main:receiverDeviceUpdated": {
|
||||||
const { receiverDeviceId, status } = message.data;
|
const { receiverDeviceId, status } = message.data;
|
||||||
const receiverDevice =
|
const receiverDevice =
|
||||||
this.receiverDevices.get(receiverDeviceId);
|
this.receiverDevices.get(receiverDeviceId);
|
||||||
|
|
||||||
if (!receiverDevice) {
|
if (!receiverDevice) {
|
||||||
logger.error(`Receiver ID \`${receiverDeviceId}\` not found!`);
|
logger.error(
|
||||||
|
`Receiver ID \`${receiverDeviceId}\` not found!`
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,27 +131,27 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
|
|
||||||
if (status.applications) {
|
if (status.applications) {
|
||||||
receiverDevice.status.applications =
|
receiverDevice.status.applications =
|
||||||
status.applications;
|
status.applications;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
receiverDevice.status = status;
|
receiverDevice.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("receiverDeviceUpdated"
|
new CustomEvent("receiverDeviceUpdated", {
|
||||||
, {
|
detail: {
|
||||||
detail: {
|
receiverDeviceId,
|
||||||
receiverDeviceId
|
status: receiverDevice.status
|
||||||
, status: receiverDevice.status
|
}
|
||||||
}
|
})
|
||||||
}));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
private onBridgeDisconnect = () => {
|
private onBridgeDisconnect = () => {
|
||||||
// Notify listeners of device availablility
|
// Notify listeners of device availablility
|
||||||
for (const [ , receiverDevice ] of this.receiverDevices) {
|
for (const [, receiverDevice] of this.receiverDevices) {
|
||||||
const event = new CustomEvent("receiverDeviceDown", {
|
const event = new CustomEvent("receiverDeviceDown", {
|
||||||
detail: { receiverDeviceId: receiverDevice.id }
|
detail: { receiverDeviceId: receiverDevice.id }
|
||||||
});
|
});
|
||||||
@@ -163,5 +165,5 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
};
|
||||||
};
|
})();
|
||||||
|
|||||||
@@ -8,25 +8,22 @@ import { TypedEventTarget } from "../../lib/TypedEventTarget";
|
|||||||
import { getWindowCenteredProps, WindowCenteredProps } from "../../lib/utils";
|
import { getWindowCenteredProps, WindowCenteredProps } from "../../lib/utils";
|
||||||
import { ReceiverDevice } from "../../types";
|
import { ReceiverDevice } from "../../types";
|
||||||
|
|
||||||
import { ReceiverSelectionCast
|
import {
|
||||||
, ReceiverSelectionStop
|
ReceiverSelectionCast,
|
||||||
, ReceiverSelectorMediaType } from "./index";
|
ReceiverSelectionStop,
|
||||||
|
ReceiverSelectorMediaType
|
||||||
|
} from "./index";
|
||||||
|
|
||||||
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
|
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
|
||||||
|
|
||||||
|
|
||||||
interface ReceiverSelectorEvents {
|
interface ReceiverSelectorEvents {
|
||||||
"selected": ReceiverSelectionCast;
|
selected: ReceiverSelectionCast;
|
||||||
"error": string;
|
error: string;
|
||||||
"cancelled": void;
|
cancelled: void;
|
||||||
"stop": ReceiverSelectionStop;
|
stop: ReceiverSelectionStop;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ReceiverSelector
|
export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorEvents> {
|
||||||
extends TypedEventTarget<ReceiverSelectorEvents> {
|
|
||||||
|
|
||||||
private windowId?: number;
|
private windowId?: number;
|
||||||
|
|
||||||
private messagePort?: Port;
|
private messagePort?: Port;
|
||||||
@@ -65,11 +62,11 @@ export default class ReceiverSelector
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async open(
|
public async open(
|
||||||
receivers: ReceiverDevice[]
|
receivers: ReceiverDevice[],
|
||||||
, defaultMediaType: ReceiverSelectorMediaType
|
defaultMediaType: ReceiverSelectorMediaType,
|
||||||
, availableMediaTypes: ReceiverSelectorMediaType
|
availableMediaTypes: ReceiverSelectorMediaType,
|
||||||
, appId?: string): Promise<void> {
|
appId?: string
|
||||||
|
): Promise<void> {
|
||||||
this.appId = appId;
|
this.appId = appId;
|
||||||
|
|
||||||
// If popup already exists, close it
|
// If popup already exists, close it
|
||||||
@@ -81,27 +78,28 @@ export default class ReceiverSelector
|
|||||||
this.defaultMediaType = defaultMediaType;
|
this.defaultMediaType = defaultMediaType;
|
||||||
this.availableMediaTypes = availableMediaTypes;
|
this.availableMediaTypes = availableMediaTypes;
|
||||||
|
|
||||||
|
|
||||||
let centeredProps: WindowCenteredProps = {
|
let centeredProps: WindowCenteredProps = {
|
||||||
left: 100
|
left: 100,
|
||||||
, top: 100
|
top: 100,
|
||||||
, width: 350
|
width: 350,
|
||||||
, height: 200
|
height: 200
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Calculate centered size/position based on current window
|
// Calculate centered size/position based on current window
|
||||||
centeredProps = getWindowCenteredProps(
|
centeredProps = getWindowCenteredProps(
|
||||||
await browser.windows.getCurrent()
|
await browser.windows.getCurrent(),
|
||||||
, centeredProps.width, centeredProps.height);
|
centeredProps.width,
|
||||||
|
centeredProps.height
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// Shouldn't ever hit this, but defaults are provided in case
|
// Shouldn't ever hit this, but defaults are provided in case
|
||||||
}
|
}
|
||||||
|
|
||||||
const popup = await browser.windows.create({
|
const popup = await browser.windows.create({
|
||||||
url: POPUP_URL
|
url: POPUP_URL,
|
||||||
, type: "popup"
|
type: "popup",
|
||||||
, ...centeredProps
|
...centeredProps
|
||||||
});
|
});
|
||||||
|
|
||||||
if (popup?.id === undefined) {
|
if (popup?.id === undefined) {
|
||||||
@@ -116,22 +114,23 @@ export default class ReceiverSelector
|
|||||||
...centeredProps
|
...centeredProps
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const closeIfFocusLost = await options.get(
|
const closeIfFocusLost = await options.get(
|
||||||
"receiverSelectorCloseIfFocusLost");
|
"receiverSelectorCloseIfFocusLost"
|
||||||
|
);
|
||||||
|
|
||||||
if (closeIfFocusLost) {
|
if (closeIfFocusLost) {
|
||||||
// Add focus listener
|
// Add focus listener
|
||||||
browser.windows.onFocusChanged.addListener(
|
browser.windows.onFocusChanged.addListener(
|
||||||
this.onWindowsFocusChanged);
|
this.onWindowsFocusChanged
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(receivers: ReceiverDevice[]) {
|
public update(receivers: ReceiverDevice[]) {
|
||||||
this.receivers = receivers;
|
this.receivers = receivers;
|
||||||
this.messagePort?.postMessage({
|
this.messagePort?.postMessage({
|
||||||
subject: "popup:update"
|
subject: "popup:update",
|
||||||
, data: {
|
data: {
|
||||||
receivers: this.receivers
|
receivers: this.receivers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -167,23 +166,25 @@ export default class ReceiverSelector
|
|||||||
this.messagePortDisconnected = true;
|
this.messagePortDisconnected = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.receivers
|
if (
|
||||||
|| !this.defaultMediaType
|
!this.receivers ||
|
||||||
|| !this.availableMediaTypes) {
|
!this.defaultMediaType ||
|
||||||
|
!this.availableMediaTypes
|
||||||
|
) {
|
||||||
throw logger.error("Popup receiver data not found.");
|
throw logger.error("Popup receiver data not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messagePort.postMessage({
|
this.messagePort.postMessage({
|
||||||
subject: "popup:init"
|
subject: "popup:init",
|
||||||
, data: { appId: this.appId }
|
data: { appId: this.appId }
|
||||||
});
|
});
|
||||||
|
|
||||||
this.messagePort.postMessage({
|
this.messagePort.postMessage({
|
||||||
subject: "popup:update"
|
subject: "popup:update",
|
||||||
, data: {
|
data: {
|
||||||
receivers: this.receivers
|
receivers: this.receivers,
|
||||||
, defaultMediaType: this.defaultMediaType
|
defaultMediaType: this.defaultMediaType,
|
||||||
, availableMediaTypes: this.availableMediaTypes
|
availableMediaTypes: this.availableMediaTypes
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,17 +198,21 @@ export default class ReceiverSelector
|
|||||||
switch (message.subject) {
|
switch (message.subject) {
|
||||||
case "receiverSelector:selected": {
|
case "receiverSelector:selected": {
|
||||||
this.wasReceiverSelected = true;
|
this.wasReceiverSelected = true;
|
||||||
this.dispatchEvent(new CustomEvent("selected", {
|
this.dispatchEvent(
|
||||||
detail: message.data
|
new CustomEvent("selected", {
|
||||||
}));
|
detail: message.data
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "receiverSelector:stop": {
|
case "receiverSelector:stop": {
|
||||||
this.dispatchEvent(new CustomEvent("stop", {
|
this.dispatchEvent(
|
||||||
detail: message.data
|
new CustomEvent("stop", {
|
||||||
}));
|
detail: message.data
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -226,7 +231,8 @@ export default class ReceiverSelector
|
|||||||
|
|
||||||
browser.windows.onRemoved.removeListener(this.onWindowsRemoved);
|
browser.windows.onRemoved.removeListener(this.onWindowsRemoved);
|
||||||
browser.windows.onFocusChanged.removeListener(
|
browser.windows.onFocusChanged.removeListener(
|
||||||
this.onWindowsFocusChanged);
|
this.onWindowsFocusChanged
|
||||||
|
);
|
||||||
|
|
||||||
if (!this.wasReceiverSelected) {
|
if (!this.wasReceiverSelected) {
|
||||||
this.dispatchEvent(new CustomEvent("cancelled"));
|
this.dispatchEvent(new CustomEvent("cancelled"));
|
||||||
@@ -247,12 +253,14 @@ export default class ReceiverSelector
|
|||||||
* `WINDOW_ID_NONE` or if the popup window is re-focused.
|
* `WINDOW_ID_NONE` or if the popup window is re-focused.
|
||||||
*/
|
*/
|
||||||
private onWindowsFocusChanged(windowId: number) {
|
private onWindowsFocusChanged(windowId: number) {
|
||||||
if (windowId !== browser.windows.WINDOW_ID_NONE
|
if (
|
||||||
&& windowId !== this.windowId) {
|
windowId !== browser.windows.WINDOW_ID_NONE &&
|
||||||
|
windowId !== this.windowId
|
||||||
|
) {
|
||||||
// Only run once
|
// Only run once
|
||||||
browser.windows.onFocusChanged.removeListener(
|
browser.windows.onFocusChanged.removeListener(
|
||||||
this.onWindowsFocusChanged);
|
this.onWindowsFocusChanged
|
||||||
|
);
|
||||||
|
|
||||||
if (this.windowId) {
|
if (this.windowId) {
|
||||||
browser.windows.remove(this.windowId);
|
browser.windows.remove(this.windowId);
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ import receiverDevices from "../receiverDevices";
|
|||||||
|
|
||||||
import { getMediaTypesForPageUrl } from "../../lib/utils";
|
import { getMediaTypesForPageUrl } from "../../lib/utils";
|
||||||
|
|
||||||
import { ReceiverSelection
|
import {
|
||||||
, ReceiverSelectionActionType
|
ReceiverSelection,
|
||||||
, ReceiverSelectorMediaType } from "./index";
|
ReceiverSelectionActionType,
|
||||||
|
ReceiverSelectorMediaType
|
||||||
|
} from "./index";
|
||||||
|
|
||||||
import ReceiverSelector from "./ReceiverSelector";
|
import ReceiverSelector from "./ReceiverSelector";
|
||||||
|
|
||||||
|
|
||||||
async function createSelector() {
|
async function createSelector() {
|
||||||
return new ReceiverSelector();
|
return new ReceiverSelector();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let sharedSelector: ReceiverSelector;
|
let sharedSelector: ReceiverSelector;
|
||||||
|
|
||||||
async function getSelector() {
|
async function getSelector() {
|
||||||
@@ -34,7 +34,6 @@ async function getSelector() {
|
|||||||
return sharedSelector;
|
return sharedSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a receiver selector with the specified
|
* Opens a receiver selector with the specified
|
||||||
* default/available media types.
|
* default/available media types.
|
||||||
@@ -46,21 +45,18 @@ async function getSelector() {
|
|||||||
* - Rejects if the selection fails.
|
* - Rejects if the selection fails.
|
||||||
*/
|
*/
|
||||||
async function getSelection(
|
async function getSelection(
|
||||||
contextTabId: number
|
contextTabId: number,
|
||||||
, contextFrameId = 0
|
contextFrameId = 0,
|
||||||
, withMediaSender = false)
|
withMediaSender = false
|
||||||
: Promise<ReceiverSelection | null> {
|
): Promise<ReceiverSelection | null> {
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
let currentShim = ShimManager.getShim(
|
let currentShim = ShimManager.getShim(contextTabId, contextFrameId);
|
||||||
contextTabId, contextFrameId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the current context is running the mirroring app, pretend
|
* If the current context is running the mirroring app, pretend
|
||||||
* it doesn't exist because it shouldn't be launched like this.
|
* it doesn't exist because it shouldn't be launched like this.
|
||||||
*/
|
*/
|
||||||
if (currentShim?.appId ===
|
if (currentShim?.appId === (await options.get("mirroringAppId"))) {
|
||||||
await options.get("mirroringAppId")) {
|
|
||||||
currentShim = undefined;
|
currentShim = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,13 +65,15 @@ async function getSelection(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { url } = await browser.webNavigation.getFrame({
|
const { url } = await browser.webNavigation.getFrame({
|
||||||
tabId: contextTabId
|
tabId: contextTabId,
|
||||||
, frameId: contextFrameId
|
frameId: contextFrameId
|
||||||
});
|
});
|
||||||
|
|
||||||
availableMediaTypes = getMediaTypesForPageUrl(url);
|
availableMediaTypes = getMediaTypesForPageUrl(url);
|
||||||
} catch {
|
} 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;
|
availableMediaTypes = ReceiverSelectorMediaType.File;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +88,8 @@ async function getSelection(
|
|||||||
// Remove mirroring media types if mirroring is not enabled
|
// Remove mirroring media types if mirroring is not enabled
|
||||||
if (!opts.mirroringEnabled) {
|
if (!opts.mirroringEnabled) {
|
||||||
availableMediaTypes &= ~(
|
availableMediaTypes &= ~(
|
||||||
ReceiverSelectorMediaType.Tab
|
ReceiverSelectorMediaType.Tab | ReceiverSelectorMediaType.Screen
|
||||||
| ReceiverSelectorMediaType.Screen);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove file media type if local media is not enabled
|
// Remove file media type if local media is not enabled
|
||||||
@@ -107,26 +105,28 @@ async function getSelection(
|
|||||||
// Get a new selector for each selection
|
// Get a new selector for each selection
|
||||||
sharedSelector = await createSelector();
|
sharedSelector = await createSelector();
|
||||||
|
|
||||||
|
|
||||||
function onReceiverChange() {
|
function onReceiverChange() {
|
||||||
sharedSelector.update(receiverDevices.getDevices());
|
sharedSelector.update(receiverDevices.getDevices());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
receiverDevices.addEventListener("receiverDeviceUp", onReceiverChange);
|
||||||
receiverDevices.addEventListener(
|
receiverDevices.addEventListener(
|
||||||
"receiverDeviceUp", onReceiverChange);
|
"receiverDeviceDown",
|
||||||
|
onReceiverChange
|
||||||
|
);
|
||||||
receiverDevices.addEventListener(
|
receiverDevices.addEventListener(
|
||||||
"receiverDeviceDown", onReceiverChange);
|
"receiverDeviceUpdated",
|
||||||
receiverDevices.addEventListener(
|
onReceiverChange
|
||||||
"receiverDeviceUpdated", onReceiverChange);
|
);
|
||||||
|
|
||||||
|
|
||||||
let onSelected: any;
|
let onSelected: any;
|
||||||
let onCancelled: any;
|
let onCancelled: any;
|
||||||
let onError: any;
|
let onError: any;
|
||||||
let onStop: any;
|
let onStop: any;
|
||||||
|
|
||||||
type EvParamsType =
|
type EvParamsType = Parameters<
|
||||||
Parameters<typeof sharedSelector.addEventListener>[0];
|
typeof sharedSelector.addEventListener
|
||||||
|
>[0];
|
||||||
|
|
||||||
function storeListener<T>(type: EvParamsType, fn: T) {
|
function storeListener<T>(type: EvParamsType, fn: T) {
|
||||||
if (type === "selected") {
|
if (type === "selected") {
|
||||||
@@ -149,67 +149,78 @@ async function getSelection(
|
|||||||
sharedSelector.removeEventListener("stop", onStop);
|
sharedSelector.removeEventListener("stop", onStop);
|
||||||
|
|
||||||
receiverDevices.removeEventListener(
|
receiverDevices.removeEventListener(
|
||||||
"receiverDeviceUp", onReceiverChange);
|
"receiverDeviceUp",
|
||||||
|
onReceiverChange
|
||||||
|
);
|
||||||
receiverDevices.removeEventListener(
|
receiverDevices.removeEventListener(
|
||||||
"receiverDeviceDown", onReceiverChange);
|
"receiverDeviceDown",
|
||||||
|
onReceiverChange
|
||||||
|
);
|
||||||
receiverDevices.removeEventListener(
|
receiverDevices.removeEventListener(
|
||||||
"receiverDeviceUpdated", onReceiverChange);
|
"receiverDeviceUpdated",
|
||||||
|
onReceiverChange
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sharedSelector.addEventListener("selected"
|
sharedSelector.addEventListener(
|
||||||
, storeListener("selected", ev => {
|
"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);
|
sharedSelector.addEventListener(
|
||||||
resolve({
|
"cancelled",
|
||||||
actionType: ReceiverSelectionActionType.Cast
|
storeListener("cancelled", () => {
|
||||||
, receiver: ev.detail.receiver
|
logger.info("Cancelled receiver selection");
|
||||||
, mediaType: ev.detail.mediaType
|
resolve(null);
|
||||||
, filePath: ev.detail.filePath
|
removeListeners();
|
||||||
});
|
})
|
||||||
removeListeners();
|
);
|
||||||
}));
|
|
||||||
|
|
||||||
sharedSelector.addEventListener("cancelled"
|
sharedSelector.addEventListener(
|
||||||
, storeListener("cancelled", () => {
|
"error",
|
||||||
|
storeListener("error", () => {
|
||||||
|
reject();
|
||||||
|
removeListeners();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
logger.info("Cancelled receiver selection");
|
sharedSelector.addEventListener(
|
||||||
resolve(null);
|
"stop",
|
||||||
removeListeners();
|
storeListener("stop", async ev => {
|
||||||
}));
|
logger.info("Stopping receiver app...", ev.detail);
|
||||||
|
|
||||||
sharedSelector.addEventListener("error"
|
receiverDevices.stopReceiverApp(ev.detail.receiver.id);
|
||||||
, 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();
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
actionType: ReceiverSelectionActionType.Stop,
|
||||||
|
receiver: ev.detail.receiver
|
||||||
|
});
|
||||||
|
removeListeners();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Ensure status manager is initialized
|
// Ensure status manager is initialized
|
||||||
await receiverDevices.init();
|
await receiverDevices.init();
|
||||||
|
|
||||||
sharedSelector.open(
|
sharedSelector.open(
|
||||||
receiverDevices.getDevices()
|
receiverDevices.getDevices(),
|
||||||
, defaultMediaType
|
defaultMediaType,
|
||||||
, availableMediaTypes
|
availableMediaTypes,
|
||||||
, currentShim?.appId);
|
currentShim?.appId
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getSelection
|
getSelection,
|
||||||
, getSelector
|
getSelector
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
export enum ReceiverSelectorType {
|
export enum ReceiverSelectorType {
|
||||||
Popup
|
Popup,
|
||||||
, Native
|
Native
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReceiverSelectorMediaType {
|
export enum ReceiverSelectorMediaType {
|
||||||
App = 1
|
App = 1,
|
||||||
, Tab = 2
|
Tab = 2,
|
||||||
, Screen = 4
|
Screen = 4,
|
||||||
, File = 8
|
File = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReceiverSelectionActionType {
|
export enum ReceiverSelectionActionType {
|
||||||
Cast = 1
|
Cast = 1,
|
||||||
, Stop = 2
|
Stop = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReceiverSelectionCast {
|
export interface ReceiverSelectionCast {
|
||||||
|
|||||||
@@ -5,21 +5,23 @@ import options from "../lib/options";
|
|||||||
|
|
||||||
import { getChromeUserAgent } from "../lib/userAgents";
|
import { getChromeUserAgent } from "../lib/userAgents";
|
||||||
|
|
||||||
import { CAST_FRAMEWORK_LOADER_SCRIPT_URL
|
import {
|
||||||
, CAST_LOADER_SCRIPT_URL } from "../lib/endpoints";
|
CAST_FRAMEWORK_LOADER_SCRIPT_URL,
|
||||||
|
CAST_LOADER_SCRIPT_URL
|
||||||
|
} from "../lib/endpoints";
|
||||||
|
|
||||||
// Missing on @types/firefox-webext-browser
|
// Missing on @types/firefox-webext-browser
|
||||||
type OnBeforeSendHeadersDetails = Parameters<Parameters<
|
type OnBeforeSendHeadersDetails = Parameters<
|
||||||
typeof browser.webRequest.onBeforeSendHeaders.addListener>[0]>[0] & {
|
Parameters<typeof browser.webRequest.onBeforeSendHeaders.addListener>[0]
|
||||||
frameAncestors?: Array<{ url: string, frameId: number }>
|
>[0] & {
|
||||||
|
frameAncestors?: Array<{ url: string; frameId: number }>;
|
||||||
};
|
};
|
||||||
type OnBeforeRequestDetails = Parameters<Parameters<
|
type OnBeforeRequestDetails = Parameters<
|
||||||
typeof browser.webRequest.onBeforeRequest.addListener>[0]>[0] & {
|
Parameters<typeof browser.webRequest.onBeforeRequest.addListener>[0]
|
||||||
frameAncestors?: Array<{ url: string, frameId: number }>
|
>[0] & {
|
||||||
|
frameAncestors?: Array<{ url: string; frameId: number }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const originUrlCache: string[] = [];
|
const originUrlCache: string[] = [];
|
||||||
|
|
||||||
let platform: string;
|
let platform: string;
|
||||||
@@ -51,40 +53,43 @@ export async function initWhitelist() {
|
|||||||
options.addEventListener("changed", ev => {
|
options.addEventListener("changed", ev => {
|
||||||
const alteredOpts = ev.detail;
|
const alteredOpts = ev.detail;
|
||||||
|
|
||||||
if (alteredOpts.includes("userAgentWhitelist")
|
if (
|
||||||
|| alteredOpts.includes("userAgentWhitelistEnabled")) {
|
alteredOpts.includes("userAgentWhitelist") ||
|
||||||
|
alteredOpts.includes("userAgentWhitelistEnabled")
|
||||||
|
) {
|
||||||
unregisterUserAgentWhitelist();
|
unregisterUserAgentWhitelist();
|
||||||
registerUserAgentWhitelist();
|
registerUserAgentWhitelist();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Web apps usually only load the sender library and
|
* Web apps usually only load the sender library and
|
||||||
* provide cast functionality if the browser is detected
|
* provide cast functionality if the browser is detected
|
||||||
* as Chrome, so we should rewrite the User-Agent header
|
* as Chrome, so we should rewrite the User-Agent header
|
||||||
* to reflect this on whitelisted sites.
|
* to reflect this on whitelisted sites.
|
||||||
*/
|
*/
|
||||||
async function onWhitelistedBeforeSendHeaders(
|
async function onWhitelistedBeforeSendHeaders(
|
||||||
details: OnBeforeSendHeadersDetails) {
|
details: OnBeforeSendHeadersDetails
|
||||||
|
) {
|
||||||
if (!details.requestHeaders) {
|
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)) {
|
if (details.originUrl && !originUrlCache.includes(details.originUrl)) {
|
||||||
originUrlCache.push(details.originUrl);
|
originUrlCache.push(details.originUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = details.requestHeaders.find(
|
const host = details.requestHeaders.find(header => header.name === "Host");
|
||||||
header => header.name === "Host");
|
|
||||||
|
|
||||||
for (const header of details.requestHeaders) {
|
for (const header of details.requestHeaders) {
|
||||||
if (header.name === "User-Agent") {
|
if (header.name === "User-Agent") {
|
||||||
header.value = host?.value === "www.youtube.com"
|
header.value =
|
||||||
? chromeUserAgentHybrid
|
host?.value === "www.youtube.com"
|
||||||
: chromeUserAgent;
|
? chromeUserAgentHybrid
|
||||||
|
: chromeUserAgent;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,8 +106,8 @@ export async function initWhitelist() {
|
|||||||
* main site is whitelisted.
|
* main site is whitelisted.
|
||||||
*/
|
*/
|
||||||
function onWhitelistedChildBeforeSendHeaders(
|
function onWhitelistedChildBeforeSendHeaders(
|
||||||
details: OnBeforeSendHeadersDetails) {
|
details: OnBeforeSendHeadersDetails
|
||||||
|
) {
|
||||||
if (!details.requestHeaders || !details.frameAncestors) {
|
if (!details.requestHeaders || !details.frameAncestors) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,13 +115,15 @@ function onWhitelistedChildBeforeSendHeaders(
|
|||||||
for (const ancestor of details.frameAncestors) {
|
for (const ancestor of details.frameAncestors) {
|
||||||
if (originUrlCache.includes(ancestor.url)) {
|
if (originUrlCache.includes(ancestor.url)) {
|
||||||
const host = details.requestHeaders.find(
|
const host = details.requestHeaders.find(
|
||||||
header => header.name === "Host");
|
header => header.name === "Host"
|
||||||
|
);
|
||||||
|
|
||||||
for (const header of details.requestHeaders) {
|
for (const header of details.requestHeaders) {
|
||||||
if (header.name === "User-Agent") {
|
if (header.name === "User-Agent") {
|
||||||
header.value = host?.value === "www.youtube.com"
|
header.value =
|
||||||
? chromeUserAgentHybrid
|
host?.value === "www.youtube.com"
|
||||||
: chromeUserAgent;
|
? chromeUserAgentHybrid
|
||||||
|
: chromeUserAgent;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +135,6 @@ function onWhitelistedChildBeforeSendHeaders(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sender applications load a cast_sender.js script that
|
* Sender applications load a cast_sender.js script that
|
||||||
* functions as a loader for the internal chrome-extension:
|
* functions as a loader for the internal chrome-extension:
|
||||||
@@ -165,16 +171,17 @@ async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) {
|
|||||||
await browser.tabs.executeScript(details.tabId, {
|
await browser.tabs.executeScript(details.tabId, {
|
||||||
code: `
|
code: `
|
||||||
window.isFramework = ${
|
window.isFramework = ${
|
||||||
details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL};
|
details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL
|
||||||
`
|
};
|
||||||
, frameId: details.frameId
|
`,
|
||||||
, runAt: "document_start"
|
frameId: details.frameId,
|
||||||
|
runAt: "document_start"
|
||||||
});
|
});
|
||||||
|
|
||||||
await browser.tabs.executeScript(details.tabId, {
|
await browser.tabs.executeScript(details.tabId, {
|
||||||
file: "shim/contentBridge.js"
|
file: "shim/contentBridge.js",
|
||||||
, frameId: details.frameId
|
frameId: details.frameId,
|
||||||
, runAt: "document_start"
|
runAt: "document_start"
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -182,40 +189,41 @@ async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function registerUserAgentWhitelist() {
|
async function registerUserAgentWhitelist() {
|
||||||
const { userAgentWhitelist
|
const { userAgentWhitelist, userAgentWhitelistEnabled } =
|
||||||
, userAgentWhitelistEnabled } = await options.getAll();
|
await options.getAll();
|
||||||
|
|
||||||
browser.webRequest.onBeforeRequest.addListener(
|
browser.webRequest.onBeforeRequest.addListener(
|
||||||
onBeforeCastSDKRequest
|
onBeforeCastSDKRequest,
|
||||||
, { urls: [
|
{ urls: [CAST_LOADER_SCRIPT_URL, CAST_FRAMEWORK_LOADER_SCRIPT_URL] },
|
||||||
CAST_LOADER_SCRIPT_URL
|
["blocking"]
|
||||||
, CAST_FRAMEWORK_LOADER_SCRIPT_URL ]}
|
);
|
||||||
, [ "blocking" ]);
|
|
||||||
|
|
||||||
if (!userAgentWhitelistEnabled || !userAgentWhitelist.length) {
|
if (!userAgentWhitelistEnabled || !userAgentWhitelist.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||||
onWhitelistedBeforeSendHeaders
|
onWhitelistedBeforeSendHeaders,
|
||||||
, { urls: userAgentWhitelist }
|
{ urls: userAgentWhitelist },
|
||||||
, [ "blocking", "requestHeaders" ]);
|
["blocking", "requestHeaders"]
|
||||||
|
);
|
||||||
|
|
||||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||||
onWhitelistedChildBeforeSendHeaders
|
onWhitelistedChildBeforeSendHeaders,
|
||||||
, { urls: [ "<all_urls>" ]}
|
{ urls: ["<all_urls>"] },
|
||||||
, [ "blocking", "requestHeaders" ]);
|
["blocking", "requestHeaders"]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unregisterUserAgentWhitelist() {
|
function unregisterUserAgentWhitelist() {
|
||||||
originUrlCache.length = 0;
|
originUrlCache.length = 0;
|
||||||
|
|
||||||
browser.webRequest.onBeforeSendHeaders
|
browser.webRequest.onBeforeSendHeaders.removeListener(
|
||||||
.removeListener(onWhitelistedBeforeSendHeaders);
|
onWhitelistedBeforeSendHeaders
|
||||||
browser.webRequest.onBeforeSendHeaders
|
);
|
||||||
.removeListener(onWhitelistedChildBeforeSendHeaders);
|
browser.webRequest.onBeforeSendHeaders.removeListener(
|
||||||
browser.webRequest.onBeforeRequest
|
onWhitelistedChildBeforeSendHeaders
|
||||||
.removeListener(onBeforeCastSDKRequest);
|
);
|
||||||
|
browser.webRequest.onBeforeRequest.removeListener(onBeforeCastSDKRequest);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,22 @@
|
|||||||
import { ReceiverSelectorType } from "./background/receiverSelector";
|
import { ReceiverSelectorType } from "./background/receiverSelector";
|
||||||
import { Options } from "./lib/options";
|
import { Options } from "./lib/options";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
bridgeApplicationName: BRIDGE_NAME
|
bridgeApplicationName: BRIDGE_NAME,
|
||||||
, bridgeBackupEnabled: false
|
bridgeBackupEnabled: false,
|
||||||
, bridgeBackupHost: "localhost"
|
bridgeBackupHost: "localhost",
|
||||||
, bridgeBackupPort: 9556
|
bridgeBackupPort: 9556,
|
||||||
, mediaEnabled: true
|
mediaEnabled: true,
|
||||||
, mediaOverlayEnabled: false
|
mediaOverlayEnabled: false,
|
||||||
, mediaSyncElement: false
|
mediaSyncElement: false,
|
||||||
, mediaStopOnUnload: false
|
mediaStopOnUnload: false,
|
||||||
, localMediaEnabled: true
|
localMediaEnabled: true,
|
||||||
, localMediaServerPort: 9555
|
localMediaServerPort: 9555,
|
||||||
, mirroringEnabled: false
|
mirroringEnabled: false,
|
||||||
, mirroringAppId: MIRRORING_APP_ID
|
mirroringAppId: MIRRORING_APP_ID,
|
||||||
, receiverSelectorCloseIfFocusLost: true
|
receiverSelectorCloseIfFocusLost: true,
|
||||||
, receiverSelectorWaitForConnection: true
|
receiverSelectorWaitForConnection: true,
|
||||||
, userAgentWhitelistEnabled: true
|
userAgentWhitelistEnabled: true,
|
||||||
, userAgentWhitelistRestrictedEnabled: true
|
userAgentWhitelistRestrictedEnabled: true,
|
||||||
, userAgentWhitelist: [
|
userAgentWhitelist: ["https://www.netflix.com/*"]
|
||||||
"https://www.netflix.com/*"
|
|
||||||
]
|
|
||||||
} as Options;
|
} 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 Nullable<T> = T | null;
|
||||||
|
|
||||||
declare type DistributiveOmit<T, K extends keyof any> =
|
declare type DistributiveOmit<T, K extends keyof any> = T extends any
|
||||||
T extends any
|
? Omit<T, K>
|
||||||
? Omit<T, K>
|
: never;
|
||||||
: never;
|
|
||||||
|
|
||||||
|
|
||||||
declare interface Object {
|
declare interface Object {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
@@ -22,16 +20,19 @@ declare interface CanvasRenderingContext2D {
|
|||||||
DRAWWINDOW_USE_WIDGET_LAYERS: 0x08;
|
DRAWWINDOW_USE_WIDGET_LAYERS: 0x08;
|
||||||
DRAWWINDOW_ASYNC_DECODE_IMAGES: 0x10;
|
DRAWWINDOW_ASYNC_DECODE_IMAGES: 0x10;
|
||||||
|
|
||||||
drawWindow (
|
drawWindow(
|
||||||
window: Window
|
window: Window,
|
||||||
, x: number, y: number
|
x: number,
|
||||||
, w: number, h: number
|
y: number,
|
||||||
, bgColor: string
|
w: number,
|
||||||
, flags: number): void;
|
h: number,
|
||||||
|
bgColor: string,
|
||||||
|
flags: number
|
||||||
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface HTMLCanvasElement {
|
declare interface HTMLCanvasElement {
|
||||||
captureStream (frameRate?: number): MediaStream;
|
captureStream(frameRate?: number): MediaStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface MediaTrackConstraints {
|
declare interface MediaTrackConstraints {
|
||||||
@@ -39,25 +40,23 @@ declare interface MediaTrackConstraints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare interface RTCPeerConnection {
|
declare interface RTCPeerConnection {
|
||||||
addStream (mediaStream: MediaStream): void;
|
addStream(mediaStream: MediaStream): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface MediaDevices {
|
declare interface MediaDevices {
|
||||||
getDisplayMedia (constraints: MediaStreamConstraints)
|
getDisplayMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
||||||
: Promise<MediaStream>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface CloneIntoOptions {
|
interface CloneIntoOptions {
|
||||||
cloneFunctions?: boolean;
|
cloneFunctions?: boolean;
|
||||||
wrapReflectors?: boolean;
|
wrapReflectors?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare function cloneInto<T> (
|
declare function cloneInto<T>(
|
||||||
obj: T
|
obj: T,
|
||||||
, targetScope: Window
|
targetScope: Window,
|
||||||
, options?: CloneIntoOptions): T;
|
options?: CloneIntoOptions
|
||||||
|
): T;
|
||||||
|
|
||||||
interface ExportFunctionOptions {
|
interface ExportFunctionOptions {
|
||||||
defineAs: string;
|
defineAs: string;
|
||||||
@@ -67,11 +66,11 @@ interface ExportFunctionOptions {
|
|||||||
|
|
||||||
type ExportFunctionFunc = (...args: any[]) => any;
|
type ExportFunctionFunc = (...args: any[]) => any;
|
||||||
|
|
||||||
declare function exportFunction (
|
declare function exportFunction(
|
||||||
func: ExportFunctionFunc
|
func: ExportFunctionFunc,
|
||||||
, targetScope: any
|
targetScope: any,
|
||||||
, options?: ExportFunctionOptions): ExportFunctionFunc;
|
options?: ExportFunctionOptions
|
||||||
|
): ExportFunctionFunc;
|
||||||
|
|
||||||
// Fix issues with @types/firefox-webext-browser
|
// Fix issues with @types/firefox-webext-browser
|
||||||
declare namespace browser.events {
|
declare namespace browser.events {
|
||||||
@@ -80,8 +79,8 @@ declare namespace browser.events {
|
|||||||
* event types.
|
* event types.
|
||||||
*/
|
*/
|
||||||
interface Event {
|
interface Event {
|
||||||
addListener (...args: any[]): void | Promise<void>;
|
addListener(...args: any[]): void | Promise<void>;
|
||||||
removeListener (...args: any[]): void | Promise<void>;
|
removeListener(...args: any[]): void | Promise<void>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +96,7 @@ declare namespace browser.runtime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function connect(connectInfo: {
|
function connect(connectInfo: {
|
||||||
name?: string
|
name?: string;
|
||||||
, includeTlsChannelId?: boolean
|
includeTlsChannelId?: boolean;
|
||||||
}): browser.runtime.Port;
|
}): browser.runtime.Port;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,18 @@ interface TypedEvents {
|
|||||||
export class TypedEventTarget<T extends TypedEvents> extends EventTarget {
|
export class TypedEventTarget<T extends TypedEvents> extends EventTarget {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public addEventListener<K extends keyof T>(
|
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
|
// @ts-ignore
|
||||||
super.addEventListener(type as string, listener);
|
super.addEventListener(type as string, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public removeEventListener<K extends keyof T>(
|
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
|
// @ts-ignore
|
||||||
super.removeEventListener(type as string, listener);
|
super.removeEventListener(type as string, listener);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,21 @@
|
|||||||
* Provides a typed interface to runtime.Port objects.
|
* Provides a typed interface to runtime.Port objects.
|
||||||
*/
|
*/
|
||||||
export interface TypedPort<T>
|
export interface TypedPort<T>
|
||||||
extends Omit<browser.runtime.Port
|
extends Omit<
|
||||||
, "onDisconnect"
|
browser.runtime.Port,
|
||||||
| "onMessage"
|
"onDisconnect" | "onMessage" | "postMessage"
|
||||||
| "postMessage"> {
|
> {
|
||||||
|
|
||||||
onDisconnect: {
|
onDisconnect: {
|
||||||
addListener (cb: (port: TypedPort<T>) => void): void | Promise<void>
|
addListener(cb: (port: TypedPort<T>) => void): void | Promise<void>;
|
||||||
, removeListener (cb: (port: TypedPort<T>) => void): void | Promise<void>
|
removeListener(cb: (port: TypedPort<T>) => void): void | Promise<void>;
|
||||||
, hasListener (cb: (port: TypedPort<T>) => void): boolean
|
hasListener(cb: (port: TypedPort<T>) => void): boolean;
|
||||||
, hasListeners (): boolean
|
hasListeners(): boolean;
|
||||||
}
|
};
|
||||||
|
onMessage: {
|
||||||
, onMessage: {
|
addListener(cb: (message: T) => void): void | Promise<void>;
|
||||||
addListener (cb: (message: T) => void): void | Promise<void>
|
removeListener(cb: (message: T) => void): void | Promise<void>;
|
||||||
, removeListener (cb: (message: T) => void): void | Promise<void>
|
hasListener(cb: (message: T) => void): boolean;
|
||||||
, hasListener (cb: (message: T) => void): boolean
|
hasListeners(): boolean;
|
||||||
, hasListeners (): boolean
|
};
|
||||||
}
|
postMessage(message: T): void;
|
||||||
|
|
||||||
, postMessage (message: T): void
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,21 +13,18 @@ export class TypedStorageArea<Schema extends { [key: string]: any }> {
|
|||||||
this.storageArea = storageArea;
|
this.storageArea = storageArea;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get<SchemaKey extends keyof Schema
|
public async get<
|
||||||
, SchemaPartial extends Partial<Schema>>(
|
SchemaKey extends keyof Schema,
|
||||||
keys?: SchemaKey
|
SchemaPartial extends Partial<Schema>
|
||||||
| SchemaKey[]
|
>(
|
||||||
| SchemaPartial
|
keys?: SchemaKey | SchemaKey[] | SchemaPartial | null | undefined
|
||||||
| null | undefined)
|
): Promise<Pick<Schema, Extract<keyof SchemaPartial, SchemaKey>>> {
|
||||||
: Promise<Pick<Schema, Extract<
|
|
||||||
keyof SchemaPartial, SchemaKey>>> {
|
|
||||||
|
|
||||||
return await this.storageArea.get(keys);
|
return await this.storageArea.get(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBytesInUse<SchemaKey extends keyof Schema>(
|
public async getBytesInUse<SchemaKey extends keyof Schema>(
|
||||||
keys?: Schema | SchemaKey[]): Promise<number> {
|
keys?: Schema | SchemaKey[]
|
||||||
|
): Promise<number> {
|
||||||
return await this.storageArea.getBytesInUse(keys);
|
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>(
|
public async remove<SchemaKey extends keyof Schema>(
|
||||||
keys: SchemaKey | SchemaKey[]): Promise<void> {
|
keys: SchemaKey | SchemaKey[]
|
||||||
|
): Promise<void> {
|
||||||
await this.storageArea.remove(keys);
|
await this.storageArea.remove(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Port } from "../messaging";
|
|||||||
import nativeMessaging from "./nativeMessaging";
|
import nativeMessaging from "./nativeMessaging";
|
||||||
import options from "./options";
|
import options from "./options";
|
||||||
|
|
||||||
|
|
||||||
export const BRIDGE_TIMEOUT = 5000;
|
export const BRIDGE_TIMEOUT = 5000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,13 +14,16 @@ export const BRIDGE_TIMEOUT = 5000;
|
|||||||
*/
|
*/
|
||||||
async function connect(): Promise<Port> {
|
async function connect(): Promise<Port> {
|
||||||
const applicationName = await options.get("bridgeApplicationName");
|
const applicationName = await options.get("bridgeApplicationName");
|
||||||
const bridgePort = nativeMessaging.connectNative(applicationName) as
|
const bridgePort = nativeMessaging.connectNative(
|
||||||
unknown as Port;
|
applicationName
|
||||||
|
) as unknown as Port;
|
||||||
|
|
||||||
bridgePort.onDisconnect.addListener(() => {
|
bridgePort.onDisconnect.addListener(() => {
|
||||||
if (bridgePort.error) {
|
if (bridgePort.error) {
|
||||||
console.error(`${applicationName} disconnected:`
|
console.error(
|
||||||
, bridgePort.error.message);
|
`${applicationName} disconnected:`,
|
||||||
|
bridgePort.error.message
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.info(`${applicationName} disconnected`);
|
console.info(`${applicationName} disconnected`);
|
||||||
}
|
}
|
||||||
@@ -30,7 +32,6 @@ async function connect(): Promise<Port> {
|
|||||||
return bridgePort;
|
return bridgePort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface BridgeInfo {
|
export interface BridgeInfo {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -50,77 +51,81 @@ export class BridgeTimedOutError extends Error {}
|
|||||||
* rules to determine compatiblity, then returns a
|
* rules to determine compatiblity, then returns a
|
||||||
* BridgeInfo object.
|
* BridgeInfo object.
|
||||||
*/
|
*/
|
||||||
const getInfo = () => new Promise<BridgeInfo>(async (resolve, reject) => {
|
const getInfo = () =>
|
||||||
const applicationName = await options.get("bridgeApplicationName");
|
new Promise<BridgeInfo>(async (resolve, reject) => {
|
||||||
if (!applicationName) {
|
const applicationName = await options.get("bridgeApplicationName");
|
||||||
reject(logger.error("Bridge application name not found."));
|
if (!applicationName) {
|
||||||
return;
|
reject(logger.error("Bridge application name not found."));
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const bridgeTimeoutId = setTimeout(() => {
|
const bridgeTimeoutId = setTimeout(() => {
|
||||||
logger.error("Bridge timed out.");
|
logger.error("Bridge timed out.");
|
||||||
reject(new BridgeTimedOutError());
|
reject(new BridgeTimedOutError());
|
||||||
}, BRIDGE_TIMEOUT);
|
}, BRIDGE_TIMEOUT);
|
||||||
|
|
||||||
let applicationVersion: string;
|
let applicationVersion: string;
|
||||||
try {
|
try {
|
||||||
const { version } = browser.runtime.getManifest();
|
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);
|
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);
|
||||||
|
|
||||||
/**
|
// Print compatibility info to console
|
||||||
* If the target version is above 0.x.x range, API is stable
|
if (!isVersionCompatible) {
|
||||||
* and versions with minor or patch level changes should be
|
logger.error(
|
||||||
* compatible.
|
`Expecting ${applicationName} v${BRIDGE_VERSION}, found v${applicationVersion}. ${
|
||||||
*/
|
isVersionOlder
|
||||||
const isVersionCompatible =
|
? "Try updating the native app to the latest version."
|
||||||
semver.eq(applicationVersion, extensionVersion)
|
: "Try updating the extension to the latest version"
|
||||||
|| (versionDiff !== "major" && extensionVersionMajor !== 0)
|
}`
|
||||||
|| (versionDiff === "patch" && extensionVersionMajor === 0);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isVersionExact = semver.eq(applicationVersion, extensionVersion);
|
resolve({
|
||||||
const isVersionOlder = semver.lt(applicationVersion, extensionVersion);
|
name: applicationName,
|
||||||
const isVersionNewer = semver.gt(applicationVersion, extensionVersion);
|
version: applicationVersion,
|
||||||
|
expectedVersion: BRIDGE_VERSION,
|
||||||
|
|
||||||
// Print compatibility info to console
|
// Version info
|
||||||
if (!isVersionCompatible) {
|
isVersionExact,
|
||||||
logger.error(`Expecting ${applicationName} v${BRIDGE_VERSION}, found v${applicationVersion}. ${
|
isVersionCompatible,
|
||||||
isVersionOlder
|
isVersionOlder,
|
||||||
? "Try updating the native app to the latest version."
|
isVersionNewer
|
||||||
: "Try updating the extension to the latest version"}`);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
name: applicationName
|
|
||||||
, version: applicationVersion
|
|
||||||
, expectedVersion: BRIDGE_VERSION
|
|
||||||
|
|
||||||
// Version info
|
|
||||||
, isVersionExact
|
|
||||||
, isVersionCompatible
|
|
||||||
, isVersionOlder
|
|
||||||
, isVersionNewer
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
connect
|
connect,
|
||||||
, getInfo
|
getInfo
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* UA string checking.
|
* UA string checking.
|
||||||
*/
|
*/
|
||||||
export const CAST_LOADER_SCRIPT_URL =
|
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.
|
* 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
|
* the framework API script is conditionally loaded in
|
||||||
* addition to the regular SDK script.
|
* addition to the regular SDK script.
|
||||||
*/
|
*/
|
||||||
export const CAST_FRAMEWORK_LOADER_SCRIPT_URL =
|
export const CAST_FRAMEWORK_LOADER_SCRIPT_URL = `${CAST_LOADER_SCRIPT_URL}?loadCastFramework=1`;
|
||||||
`${CAST_LOADER_SCRIPT_URL}?loadCastFramework=1`;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cast extension URLs.
|
* Cast extension URLs.
|
||||||
@@ -29,8 +28,8 @@ export const CAST_FRAMEWORK_LOADER_SCRIPT_URL =
|
|||||||
* chrome-extension: URLs for compatibility reasons (?).
|
* chrome-extension: URLs for compatibility reasons (?).
|
||||||
*/
|
*/
|
||||||
export const CAST_SCRIPT_URLS = [
|
export const CAST_SCRIPT_URLS = [
|
||||||
"chrome-extension://pkedcjkdefgpdelpbcmbmeomcjbeemfm/cast_sender.js"
|
"chrome-extension://pkedcjkdefgpdelpbcmbmeomcjbeemfm/cast_sender.js",
|
||||||
, "chrome-extension://enhhojjnijigcajfphajepfemndkmdlo/cast_sender.js"
|
"chrome-extension://enhhojjnijigcajfphajepfemndkmdlo/cast_sender.js"
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,4 +40,4 @@ export const CAST_SCRIPT_URLS = [
|
|||||||
* opposed to within the cast extension.
|
* opposed to within the cast extension.
|
||||||
*/
|
*/
|
||||||
export const CAST_FRAMEWORK_SCRIPT_URL =
|
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 {
|
export default {
|
||||||
// Web-supported
|
// Web-supported
|
||||||
"CA5E8412": { name: "Netflix" , matches: "https://www.netflix.com/*" }
|
"CA5E8412": { name: "Netflix", matches: "https://www.netflix.com/*" },
|
||||||
, "233637DE": { name: "YouTube" , matches: "https://www.youtube.com/*" }
|
"233637DE": { name: "YouTube", matches: "https://www.youtube.com/*" },
|
||||||
, "CC32E753": { name: "Spotify" , matches: "https://open.spotify.com/*" }
|
"CC32E753": { name: "Spotify", matches: "https://open.spotify.com/*" },
|
||||||
, "5E81F6DB": { name: "BBC iPlayer" , matches: "https://www.bbc.co.uk/iplayer/*" }
|
"5E81F6DB": {
|
||||||
, "03977A48": { name: "BBC Sounds" , matches: "https://www.bbc.co.uk/sounds/*" }
|
name: "BBC iPlayer",
|
||||||
, "AA666EDD": { name: "Crunchyroll" , matches: "https://crunchyroll.com/*" }
|
matches: "https://www.bbc.co.uk/iplayer/*"
|
||||||
, "10AAD887": { name: "All 4" , matches: "https://www.channel4.com/*" }
|
},
|
||||||
, "B3DCF968": { name: "Twitch" , matches: "https://www.twitch.tv/*" }
|
"03977A48": {
|
||||||
, "B88B034A": { name: "Dailymotion" , matches: "https://www.dailymotion.com/*" }
|
name: "BBC Sounds",
|
||||||
, "C3DE6BC2": { name: "Disney+" , matches: "https://www.disneyplus.com/*" }
|
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
|
// Misc
|
||||||
, "17608BC8": { name: "Prime Video" }
|
"17608BC8": { name: "Prime Video" },
|
||||||
, "9AC194DC": { name: "Plex" }
|
"9AC194DC": { name: "Plex" },
|
||||||
, "CD7B9F59": { name: "Global Player Live" }
|
"CD7B9F59": { name: "Global Player Live" },
|
||||||
|
|
||||||
, "CC1AD845": { name: _("popupMediaTypeAppMedia") }
|
"CC1AD845": { name: _("popupMediaTypeAppMedia") }
|
||||||
} as Record<string, KnownApp>;
|
} as Record<string, KnownApp>;
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { stringify } from "./utils";
|
import { stringify } from "./utils";
|
||||||
|
|
||||||
import { ReceiverSelection
|
import {
|
||||||
, ReceiverSelectionActionType
|
ReceiverSelection,
|
||||||
, ReceiverSelectorMediaType } from "../background/receiverSelector";
|
ReceiverSelectionActionType,
|
||||||
|
ReceiverSelectorMediaType
|
||||||
|
} from "../background/receiverSelector";
|
||||||
|
|
||||||
import ShimManager from "../background/ShimManager";
|
import ShimManager from "../background/ShimManager";
|
||||||
|
|
||||||
|
|
||||||
interface LoadSenderOptions {
|
interface LoadSenderOptions {
|
||||||
tabId: number;
|
tabId: number;
|
||||||
frameId?: number;
|
frameId?: number;
|
||||||
@@ -34,13 +35,14 @@ export default async function loadSender(opts: LoadSenderOptions) {
|
|||||||
case ReceiverSelectorMediaType.App: {
|
case ReceiverSelectorMediaType.App: {
|
||||||
const shim = ShimManager.getShim(opts.tabId, opts.frameId);
|
const shim = ShimManager.getShim(opts.tabId, opts.frameId);
|
||||||
if (!shim) {
|
if (!shim) {
|
||||||
throw logger.error(`Shim not found at tabId ${
|
throw logger.error(
|
||||||
opts.tabId} / frameId ${opts.frameId}`);
|
`Shim not found at tabId ${opts.tabId} / frameId ${opts.frameId}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
shim.contentPort.postMessage({
|
shim.contentPort.postMessage({
|
||||||
subject: "shim:launchApp"
|
subject: "shim:launchApp",
|
||||||
, data: { receiver: opts.selection.receiver }
|
data: { receiver: opts.selection.receiver }
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -52,13 +54,13 @@ export default async function loadSender(opts: LoadSenderOptions) {
|
|||||||
code: stringify`
|
code: stringify`
|
||||||
window.selectedMedia = ${opts.selection.mediaType};
|
window.selectedMedia = ${opts.selection.mediaType};
|
||||||
window.selectedReceiver = ${opts.selection.receiver};
|
window.selectedReceiver = ${opts.selection.receiver};
|
||||||
`
|
`,
|
||||||
, frameId: opts.frameId
|
frameId: opts.frameId
|
||||||
});
|
});
|
||||||
|
|
||||||
await browser.tabs.executeScript(opts.tabId, {
|
await browser.tabs.executeScript(opts.tabId, {
|
||||||
file: "senders/mirroring.js"
|
file: "senders/mirroring.js",
|
||||||
, frameId: opts.frameId
|
frameId: opts.frameId
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -69,8 +71,8 @@ export default async function loadSender(opts: LoadSenderOptions) {
|
|||||||
const { init } = await import("../senders/media");
|
const { init } = await import("../senders/media");
|
||||||
|
|
||||||
init({
|
init({
|
||||||
mediaUrl: fileUrl.href
|
mediaUrl: fileUrl.href,
|
||||||
, receiver: opts.selection.receiver
|
receiver: opts.selection.receiver
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import options from "./options";
|
|||||||
|
|
||||||
import { Message, Port } from "../messaging";
|
import { Message, Port } from "../messaging";
|
||||||
|
|
||||||
|
|
||||||
type DisconnectListener = (port: Port) => void;
|
type DisconnectListener = (port: Port) => void;
|
||||||
type MessageListener = (message: Message) => void;
|
type MessageListener = (message: Message) => void;
|
||||||
|
|
||||||
@@ -26,7 +25,6 @@ function connectNative(application: string): Port {
|
|||||||
|
|
||||||
const port = browser.runtime.connectNative(application);
|
const port = browser.runtime.connectNative(application);
|
||||||
|
|
||||||
|
|
||||||
let socket: WebSocket;
|
let socket: WebSocket;
|
||||||
|
|
||||||
const onDisconnectListeners = new Set<DisconnectListener>();
|
const onDisconnectListeners = new Set<DisconnectListener>();
|
||||||
@@ -34,47 +32,47 @@ function connectNative(application: string): Port {
|
|||||||
|
|
||||||
// Port proxy API
|
// Port proxy API
|
||||||
const portObject: Port = {
|
const portObject: Port = {
|
||||||
error: null as any
|
error: null as any,
|
||||||
, name: ""
|
name: "",
|
||||||
|
|
||||||
, onDisconnect: {
|
onDisconnect: {
|
||||||
addListener(cb: DisconnectListener) {
|
addListener(cb: DisconnectListener) {
|
||||||
onDisconnectListeners.add(cb);
|
onDisconnectListeners.add(cb);
|
||||||
}
|
},
|
||||||
, removeListener(cb: DisconnectListener) {
|
removeListener(cb: DisconnectListener) {
|
||||||
onDisconnectListeners.delete(cb);
|
onDisconnectListeners.delete(cb);
|
||||||
}
|
},
|
||||||
, hasListener(cb: DisconnectListener) {
|
hasListener(cb: DisconnectListener) {
|
||||||
return onDisconnectListeners.has(cb);
|
return onDisconnectListeners.has(cb);
|
||||||
}
|
},
|
||||||
, hasListeners() {
|
hasListeners() {
|
||||||
return onDisconnectListeners.size > 0;
|
return onDisconnectListeners.size > 0;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
, onMessage: {
|
onMessage: {
|
||||||
addListener(cb: MessageListener) {
|
addListener(cb: MessageListener) {
|
||||||
onMessageListeners.add(cb);
|
onMessageListeners.add(cb);
|
||||||
}
|
},
|
||||||
, removeListener(cb: MessageListener) {
|
removeListener(cb: MessageListener) {
|
||||||
onMessageListeners.delete(cb);
|
onMessageListeners.delete(cb);
|
||||||
}
|
},
|
||||||
, hasListener(cb: MessageListener) {
|
hasListener(cb: MessageListener) {
|
||||||
return onMessageListeners.has(cb);
|
return onMessageListeners.has(cb);
|
||||||
}
|
},
|
||||||
, hasListeners() {
|
hasListeners() {
|
||||||
return onMessageListeners.size > 0;
|
return onMessageListeners.size > 0;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
, disconnect() {
|
disconnect() {
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.close();
|
socket.close();
|
||||||
} else {
|
} else {
|
||||||
port.disconnect();
|
port.disconnect();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
, postMessage(message) {
|
postMessage(message) {
|
||||||
if (socket) {
|
if (socket) {
|
||||||
switch (socket.readyState) {
|
switch (socket.readyState) {
|
||||||
case WebSocket.CONNECTING: {
|
case WebSocket.CONNECTING: {
|
||||||
@@ -99,11 +97,9 @@ function connectNative(application: string): Port {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
port.onDisconnect.addListener(async () => {
|
port.onDisconnect.addListener(async () => {
|
||||||
const { bridgeBackupEnabled
|
const { bridgeBackupEnabled, bridgeBackupHost, bridgeBackupPort } =
|
||||||
, bridgeBackupHost
|
await options.getAll();
|
||||||
, bridgeBackupPort } = await options.getAll();
|
|
||||||
|
|
||||||
if (!bridgeBackupEnabled) {
|
if (!bridgeBackupEnabled) {
|
||||||
portObject.error = {
|
portObject.error = {
|
||||||
@@ -114,14 +110,17 @@ function connectNative(application: string): Port {
|
|||||||
listener(portObject);
|
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) {
|
if (port.error && !isNativeHostStatusKnown) {
|
||||||
isNativeHostStatusKnown = true;
|
isNativeHostStatusKnown = true;
|
||||||
|
|
||||||
socket = new WebSocket(
|
socket = new WebSocket(
|
||||||
`ws://${bridgeBackupHost}:${bridgeBackupPort}`);
|
`ws://${bridgeBackupHost}:${bridgeBackupPort}`
|
||||||
|
);
|
||||||
|
|
||||||
socket.addEventListener("open", () => {
|
socket.addEventListener("open", () => {
|
||||||
// Send all messages in queue
|
// Send all messages in queue
|
||||||
@@ -163,30 +162,28 @@ function connectNative(application: string): Port {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return portObject;
|
return portObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendNativeMessage(
|
async function sendNativeMessage(application: string, message: Message) {
|
||||||
application: string
|
|
||||||
, message: Message) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await browser.runtime.sendNativeMessage(application, message);
|
return await browser.runtime.sendNativeMessage(application, message);
|
||||||
} catch {
|
} catch {
|
||||||
const { bridgeBackupEnabled
|
const { bridgeBackupEnabled, bridgeBackupHost, bridgeBackupPort } =
|
||||||
, bridgeBackupHost
|
await options.getAll();
|
||||||
, bridgeBackupPort } = await options.getAll();
|
|
||||||
|
|
||||||
if (!bridgeBackupEnabled) {
|
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");
|
const port = await options.get("bridgeBackupPort");
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const ws = new WebSocket(
|
const ws = new WebSocket(
|
||||||
`ws://${bridgeBackupHost}:${bridgeBackupPort}`);
|
`ws://${bridgeBackupHost}:${bridgeBackupPort}`
|
||||||
|
);
|
||||||
|
|
||||||
ws.addEventListener("open", () => {
|
ws.addEventListener("open", () => {
|
||||||
ws.send(JSON.stringify(message));
|
ws.send(JSON.stringify(message));
|
||||||
@@ -205,8 +202,7 @@ async function sendNativeMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
connectNative
|
connectNative,
|
||||||
, sendNativeMessage
|
sendNativeMessage
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import { ReceiverSelectorType } from "../background/receiverSelector";
|
|||||||
import { TypedEventTarget } from "./TypedEventTarget";
|
import { TypedEventTarget } from "./TypedEventTarget";
|
||||||
import { TypedStorageArea } from "./TypedStorageArea";
|
import { TypedStorageArea } from "./TypedStorageArea";
|
||||||
|
|
||||||
|
|
||||||
const storageArea = new TypedStorageArea<{
|
const storageArea = new TypedStorageArea<{
|
||||||
options: Options
|
options: Options;
|
||||||
}>(browser.storage.sync);
|
}>(browser.storage.sync);
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
@@ -35,12 +34,11 @@ export interface Options {
|
|||||||
[key: string]: Options[keyof Options];
|
[key: string]: Options[keyof Options];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface EventMap {
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.onStorageChanged = this.onStorageChanged.bind(this);
|
this.onStorageChanged = this.onStorageChanged.bind(this);
|
||||||
@@ -53,9 +51,9 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onStorageChanged(
|
private onStorageChanged(
|
||||||
changes: { [key: string]: browser.storage.StorageChange }
|
changes: { [key: string]: browser.storage.StorageChange },
|
||||||
, areaName: string) {
|
areaName: string
|
||||||
|
) {
|
||||||
if (areaName !== "sync") {
|
if (areaName !== "sync") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -80,11 +78,16 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Array comparison
|
// Array comparison
|
||||||
if (oldKeyValue instanceof Array
|
if (
|
||||||
&& newKeyValue instanceof Array) {
|
oldKeyValue instanceof Array &&
|
||||||
if (oldKeyValue.length === newKeyValue.length
|
newKeyValue instanceof Array
|
||||||
&& oldKeyValue.every((value, index) =>
|
) {
|
||||||
value === newKeyValue[index])) {
|
if (
|
||||||
|
oldKeyValue.length === newKeyValue.length &&
|
||||||
|
oldKeyValue.every(
|
||||||
|
(value, index) => value === newKeyValue[index]
|
||||||
|
)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,9 +96,11 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
changedKeys.push(key);
|
changedKeys.push(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent("changed", {
|
this.dispatchEvent(
|
||||||
detail: changedKeys as Array<keyof Options>
|
new CustomEvent("changed", {
|
||||||
}));
|
detail: changedKeys as Array<keyof Options>
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,15 +140,14 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
* promise.
|
* promise.
|
||||||
*/
|
*/
|
||||||
public async set<T extends keyof Options>(
|
public async set<T extends keyof Options>(
|
||||||
name: T
|
name: T,
|
||||||
, value: Options[T]): Promise<void> {
|
value: Options[T]
|
||||||
|
): Promise<void> {
|
||||||
const options = await this.getAll();
|
const options = await this.getAll();
|
||||||
options[name] = value;
|
options[name] = value;
|
||||||
return this.setAll(options);
|
return this.setAll(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets existing options from storage and compares it
|
* Gets existing options from storage and compares it
|
||||||
* against defaults. Any options in defaults and not in
|
* 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();
|
const newOpts = await this.getAll();
|
||||||
|
|
||||||
// Find options not already in storage
|
// 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)) {
|
if (!newOpts.hasOwnProperty(optName)) {
|
||||||
newOpts[optName] = optVal;
|
newOpts[optName] = optVal;
|
||||||
}
|
}
|
||||||
@@ -162,4 +166,4 @@ export default new class extends TypedEventTarget<EventMap> {
|
|||||||
// Update storage with default values of new options
|
// Update storage with default values of new options
|
||||||
return this.setAll(newOpts);
|
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_WIN = "Windows NT 10.0; Win64; x64";
|
||||||
const PLATFORM_LINUX = "X11; Linux x86_64";
|
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";
|
const UA_HYBRID = "Chrome/80.0.3987.87 Gecko/20100101 Firefox/72.0";
|
||||||
|
|
||||||
export function getChromeUserAgent(platform: string, hybrid = false) {
|
export function getChromeUserAgent(platform: string, hybrid = false) {
|
||||||
let platformComponent: string;
|
let platformComponent: string;
|
||||||
if (platform === "mac") {
|
if (platform === "mac") {
|
||||||
platformComponent = hybrid
|
platformComponent = hybrid ? PLATFORM_MAC_HYBRID : PLATFORM_MAC;
|
||||||
? PLATFORM_MAC_HYBRID
|
|
||||||
: PLATFORM_MAC;
|
|
||||||
} else if (platform === "win") {
|
} else if (platform === "win") {
|
||||||
platformComponent = PLATFORM_WIN;
|
platformComponent = PLATFORM_WIN;
|
||||||
} else if (platform === "linux") {
|
} else if (platform === "linux") {
|
||||||
@@ -22,9 +21,7 @@ export function getChromeUserAgent(platform: string, hybrid = false) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const browserComponent = hybrid
|
const browserComponent = hybrid ? UA_HYBRID : UA_CHROME;
|
||||||
? UA_HYBRID
|
|
||||||
: UA_CHROME;
|
|
||||||
|
|
||||||
return `Mozilla/5.0 (${platformComponent}) ${browserComponent}`;
|
return `Mozilla/5.0 (${platformComponent}) ${browserComponent}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import logger from "./logger";
|
|||||||
|
|
||||||
import { ReceiverSelectorMediaType } from "../background/receiverSelector";
|
import { ReceiverSelectorMediaType } from "../background/receiverSelector";
|
||||||
|
|
||||||
|
|
||||||
export function getNextEllipsis(ellipsis: string): string {
|
export function getNextEllipsis(ellipsis: string): string {
|
||||||
if (ellipsis === "") return ".";
|
if (ellipsis === "") return ".";
|
||||||
if (ellipsis === ".") return "..";
|
if (ellipsis === ".") return "..";
|
||||||
@@ -18,9 +17,9 @@ export function getNextEllipsis(ellipsis: string): string {
|
|||||||
* Template literal tag function, JSON-encodes substitutions.
|
* Template literal tag function, JSON-encodes substitutions.
|
||||||
*/
|
*/
|
||||||
export function stringify(
|
export function stringify(
|
||||||
templateStrings: TemplateStringsArray
|
templateStrings: TemplateStringsArray,
|
||||||
, ...substitutions: any[]) {
|
...substitutions: any[]
|
||||||
|
) {
|
||||||
let formattedString = "";
|
let formattedString = "";
|
||||||
|
|
||||||
for (const templateString of templateStrings) {
|
for (const templateString of templateStrings) {
|
||||||
@@ -35,8 +34,8 @@ export function stringify(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getMediaTypesForPageUrl(
|
export function getMediaTypesForPageUrl(
|
||||||
pageUrl: string): ReceiverSelectorMediaType {
|
pageUrl: string
|
||||||
|
): ReceiverSelectorMediaType {
|
||||||
const url = new URL(pageUrl);
|
const url = new URL(pageUrl);
|
||||||
let availableMediaTypes = ReceiverSelectorMediaType.File;
|
let availableMediaTypes = ReceiverSelectorMediaType.File;
|
||||||
|
|
||||||
@@ -45,18 +44,18 @@ export function getMediaTypesForPageUrl(
|
|||||||
* Mozilla domains.
|
* Mozilla domains.
|
||||||
*/
|
*/
|
||||||
const blockedHosts = [
|
const blockedHosts = [
|
||||||
"accounts-static.cdn.mozilla.net"
|
"accounts-static.cdn.mozilla.net",
|
||||||
, "accounts.firefox.com"
|
"accounts.firefox.com",
|
||||||
, "addons.cdn.mozilla.net"
|
"addons.cdn.mozilla.net",
|
||||||
, "addons.mozilla.org"
|
"addons.mozilla.org",
|
||||||
, "api.accounts.firefox.com"
|
"api.accounts.firefox.com",
|
||||||
, "content.cdn.mozilla.net"
|
"content.cdn.mozilla.net",
|
||||||
, "discovery.addons.mozilla.org"
|
"discovery.addons.mozilla.org",
|
||||||
, "install.mozilla.org"
|
"install.mozilla.org",
|
||||||
, "oauth.accounts.firefox.com"
|
"oauth.accounts.firefox.com",
|
||||||
, "profile.accounts.firefox.com"
|
"profile.accounts.firefox.com",
|
||||||
, "support.mozilla.org"
|
"support.mozilla.org",
|
||||||
, "sync.services.mozilla.com"
|
"sync.services.mozilla.com"
|
||||||
];
|
];
|
||||||
|
|
||||||
if (blockedHosts.includes(url.host)) {
|
if (blockedHosts.includes(url.host)) {
|
||||||
@@ -80,7 +79,6 @@ export function getMediaTypesForPageUrl(
|
|||||||
return availableMediaTypes;
|
return availableMediaTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface WindowCenteredProps {
|
export interface WindowCenteredProps {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
@@ -89,33 +87,37 @@ export interface WindowCenteredProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getWindowCenteredProps(
|
export function getWindowCenteredProps(
|
||||||
refWin: browser.windows.Window
|
refWin: browser.windows.Window,
|
||||||
, width: number
|
width: number,
|
||||||
, height: number): WindowCenteredProps {
|
height: number
|
||||||
|
): WindowCenteredProps {
|
||||||
if (refWin.left === undefined || refWin.width === undefined
|
if (
|
||||||
|| refWin.top === undefined || refWin.height === undefined) {
|
refWin.left === undefined ||
|
||||||
|
refWin.width === undefined ||
|
||||||
|
refWin.top === undefined ||
|
||||||
|
refWin.height === undefined
|
||||||
|
) {
|
||||||
throw logger.error("refWin missing positional attributes.");
|
throw logger.error("refWin missing positional attributes.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const centerX = refWin.left + (refWin.width / 2);
|
const centerX = refWin.left + refWin.width / 2;
|
||||||
const centerY = refWin.top + (refWin.height / 3);
|
const centerY = refWin.top + refWin.height / 3;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width, height
|
width,
|
||||||
, left: Math.floor(centerX - width / 2)
|
height,
|
||||||
, top: Math.floor(centerY - height / 2)
|
left: Math.floor(centerX - width / 2),
|
||||||
|
top: Math.floor(centerY - height / 2)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const REMOTE_MATCH_PATTERN_REGEX =
|
||||||
export const REMOTE_MATCH_PATTERN_REGEX = /^(?:(?:(\*|https?|ftp):\/\/(\*|(?:\*\.(?:[^\/\*:]\.?)+(?:[^\.])|[^\/\*:]*))?)(\/.*)|<all_urls>)$/;
|
/^(?:(?:(\*|https?|ftp):\/\/(\*|(?:\*\.(?:[^\/\*:]\.?)+(?:[^\.])|[^\/\*:]*))?)(\/.*)|<all_urls>)$/;
|
||||||
|
|
||||||
|
|
||||||
export function loadScript(
|
export function loadScript(
|
||||||
scriptUrl: string
|
scriptUrl: string,
|
||||||
, doc: Document = document): HTMLScriptElement {
|
doc: Document = document
|
||||||
|
): HTMLScriptElement {
|
||||||
const scriptElement = doc.createElement("script");
|
const scriptElement = doc.createElement("script");
|
||||||
scriptElement.src = scriptUrl;
|
scriptElement.src = scriptUrl;
|
||||||
(doc.head || doc.documentElement).append(scriptElement);
|
(doc.head || doc.documentElement).append(scriptElement);
|
||||||
|
|||||||
@@ -4,18 +4,21 @@ import { TypedPort } from "./lib/TypedPort";
|
|||||||
import { BridgeInfo } from "./lib/bridge";
|
import { BridgeInfo } from "./lib/bridge";
|
||||||
|
|
||||||
import { ReceiverSelectorMediaType } from "./background/receiverSelector";
|
import { ReceiverSelectorMediaType } from "./background/receiverSelector";
|
||||||
import { ReceiverSelection
|
import {
|
||||||
, ReceiverSelectionCast
|
ReceiverSelection,
|
||||||
, ReceiverSelectionStop } from "./background/receiverSelector";
|
ReceiverSelectionCast,
|
||||||
|
ReceiverSelectionStop
|
||||||
|
} from "./background/receiverSelector";
|
||||||
|
|
||||||
import { CastSessionCreated
|
import {
|
||||||
, CastSessionUpdated
|
CastSessionCreated,
|
||||||
, ReceiverStatus
|
CastSessionUpdated,
|
||||||
, SenderMessage } from "./shim/cast/types";
|
ReceiverStatus,
|
||||||
|
SenderMessage
|
||||||
|
} from "./shim/cast/types";
|
||||||
|
|
||||||
import { ReceiverDevice } from "./types";
|
import { ReceiverDevice } from "./types";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Messages are JSON objects with a `subject` string key and a
|
* Messages are JSON objects with a `subject` string key and a
|
||||||
* generic `data` key:
|
* generic `data` key:
|
||||||
@@ -29,38 +32,31 @@ import { ReceiverDevice } from "./types";
|
|||||||
* as the value in the message tables.
|
* as the value in the message tables.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Messages exclusively used internally between extension
|
* Messages exclusively used internally between extension
|
||||||
* components.
|
* components.
|
||||||
*/
|
*/
|
||||||
type ExtMessageDefinitions = {
|
type ExtMessageDefinitions = {
|
||||||
"popup:init": { appId?: string }
|
"popup:init": { appId?: string };
|
||||||
, "popup:update": {
|
"popup:update": {
|
||||||
receivers: ReceiverDevice[]
|
receivers: ReceiverDevice[];
|
||||||
, defaultMediaType?: ReceiverSelectorMediaType
|
defaultMediaType?: ReceiverSelectorMediaType;
|
||||||
, availableMediaTypes?: ReceiverSelectorMediaType
|
availableMediaTypes?: ReceiverSelectorMediaType;
|
||||||
}
|
};
|
||||||
, "popup:close": {}
|
"popup:close": {};
|
||||||
|
"receiverSelector:selected": ReceiverSelection;
|
||||||
, "receiverSelector:selected": ReceiverSelection
|
"receiverSelector:stop": ReceiverSelection;
|
||||||
, "receiverSelector:stop": ReceiverSelection
|
"main:shimReady": { appId: string };
|
||||||
|
"main:selectReceiver": {};
|
||||||
, "main:shimReady": { appId: string }
|
"shim:selectReceiver/selected": ReceiverSelectionCast;
|
||||||
|
"shim:selectReceiver/stopped": ReceiverSelectionStop;
|
||||||
, "main:selectReceiver": {}
|
"shim:selectReceiver/cancelled": {};
|
||||||
, "shim:selectReceiver/selected": ReceiverSelectionCast
|
"main:sessionCreated": {};
|
||||||
, "shim:selectReceiver/stopped": ReceiverSelectionStop
|
"shim:initialized": BridgeInfo;
|
||||||
, "shim:selectReceiver/cancelled": {}
|
"shim:serviceUp": { receiverDevice: ReceiverDevice };
|
||||||
|
"shim:serviceDown": { receiverDeviceId: ReceiverDevice["id"] };
|
||||||
, "main:sessionCreated": {}
|
"shim:launchApp": { receiver: ReceiverDevice };
|
||||||
|
};
|
||||||
, "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
|
* Messages that cross the native messaging channel. MUST keep
|
||||||
@@ -68,89 +64,76 @@ type ExtMessageDefinitions = {
|
|||||||
* app/bridge/messaging.ts > MessagesBase
|
* app/bridge/messaging.ts > MessagesBase
|
||||||
*/
|
*/
|
||||||
type AppMessageDefinitions = {
|
type AppMessageDefinitions = {
|
||||||
"shim:castSessionCreated": CastSessionCreated
|
"shim:castSessionCreated": CastSessionCreated;
|
||||||
, "shim:castSessionUpdated": CastSessionUpdated
|
"shim:castSessionUpdated": CastSessionUpdated;
|
||||||
, "shim:castSessionStopped": {
|
"shim:castSessionStopped": {
|
||||||
sessionId: string
|
sessionId: string;
|
||||||
}
|
};
|
||||||
|
"shim:receivedCastSessionMessage": {
|
||||||
, "shim:receivedCastSessionMessage": {
|
sessionId: string;
|
||||||
sessionId: string
|
namespace: string;
|
||||||
, namespace: string
|
messageData: string;
|
||||||
, messageData: string
|
};
|
||||||
}
|
"shim:impl_sendCastMessage": {
|
||||||
|
sessionId: string;
|
||||||
, "shim:impl_sendCastMessage": {
|
messageId: string;
|
||||||
sessionId: string
|
error?: string;
|
||||||
, messageId: string
|
};
|
||||||
, error?: string
|
"bridge:createCastSession": {
|
||||||
}
|
appId: string;
|
||||||
|
receiverDevice: ReceiverDevice;
|
||||||
, "bridge:createCastSession": {
|
};
|
||||||
appId: string
|
"bridge:sendCastReceiverMessage": {
|
||||||
, receiverDevice: ReceiverDevice
|
sessionId: string;
|
||||||
}
|
messageData: SenderMessage;
|
||||||
, "bridge:sendCastReceiverMessage": {
|
messageId: string;
|
||||||
sessionId: string
|
};
|
||||||
, messageData: SenderMessage
|
"bridge:sendCastSessionMessage": {
|
||||||
, messageId: string
|
sessionId: string;
|
||||||
}
|
namespace: string;
|
||||||
, "bridge:sendCastSessionMessage": {
|
messageData: object | string;
|
||||||
sessionId: string
|
messageId: string;
|
||||||
, namespace: string
|
};
|
||||||
, messageData: object | string
|
"bridge:stopCastApp": { receiverDevice: ReceiverDevice };
|
||||||
, messageId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
, "bridge:stopCastApp": { receiverDevice: ReceiverDevice }
|
|
||||||
|
|
||||||
// Bridge messages
|
// Bridge messages
|
||||||
, "main:receiverSelector/selected": ReceiverSelectionCast
|
"main:receiverSelector/selected": ReceiverSelectionCast;
|
||||||
, "main:receiverSelector/stopped": ReceiverSelectionStop
|
"main:receiverSelector/stopped": ReceiverSelectionStop;
|
||||||
, "main:receiverSelector/cancelled": {}
|
"main:receiverSelector/cancelled": {};
|
||||||
, "main:receiverSelector/error": string
|
"main:receiverSelector/error": string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getInfo uses the old :/ form for compat with old bridge
|
* getInfo uses the old :/ form for compat with old bridge
|
||||||
* versions.
|
* versions.
|
||||||
*/
|
*/
|
||||||
, "bridge:getInfo": string
|
"bridge:getInfo": string;
|
||||||
, "bridge:/getInfo": string
|
"bridge:/getInfo": string;
|
||||||
|
"bridge:startDiscovery": {
|
||||||
, "bridge:startDiscovery": {
|
shouldWatchStatus: boolean;
|
||||||
shouldWatchStatus: boolean
|
};
|
||||||
}
|
"bridge:openReceiverSelector": string;
|
||||||
|
"bridge:closeReceiverSelector": {};
|
||||||
, "bridge:openReceiverSelector": string
|
"bridge:startMediaServer": {
|
||||||
, "bridge:closeReceiverSelector": {}
|
filePath: string;
|
||||||
|
port: number;
|
||||||
, "bridge:startMediaServer": {
|
};
|
||||||
filePath: string
|
"bridge:stopMediaServer": {};
|
||||||
, port: number
|
"mediaCast:mediaServerStarted": {
|
||||||
}
|
mediaPath: string;
|
||||||
, "bridge:stopMediaServer": {}
|
subtitlePaths: string[];
|
||||||
|
localAddress: string;
|
||||||
, "mediaCast:mediaServerStarted": {
|
};
|
||||||
mediaPath: string
|
"mediaCast:mediaServerStopped": {};
|
||||||
, subtitlePaths: string[]
|
"mediaCast:mediaServerError": {};
|
||||||
, localAddress: string
|
"main:receiverDeviceUp": { receiverDevice: ReceiverDevice };
|
||||||
}
|
"main:receiverDeviceDown": { receiverDeviceId: string };
|
||||||
, "mediaCast:mediaServerStopped": {}
|
"main:receiverDeviceUpdated": {
|
||||||
, "mediaCast:mediaServerError": {}
|
receiverDeviceId: string;
|
||||||
|
status: ReceiverStatus;
|
||||||
|
};
|
||||||
, "main:receiverDeviceUp": { receiverDevice: ReceiverDevice }
|
};
|
||||||
, "main:receiverDeviceDown": { receiverDeviceId: string }
|
|
||||||
, "main:receiverDeviceUpdated": {
|
|
||||||
receiverDeviceId: string
|
|
||||||
, status: ReceiverStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageDefinitions =
|
|
||||||
ExtMessageDefinitions
|
|
||||||
& AppMessageDefinitions;
|
|
||||||
|
|
||||||
|
type MessageDefinitions = ExtMessageDefinitions & AppMessageDefinitions;
|
||||||
|
|
||||||
interface MessageBase<K extends keyof MessageDefinitions> {
|
interface MessageBase<K extends keyof MessageDefinitions> {
|
||||||
subject: K;
|
subject: K;
|
||||||
@@ -159,7 +142,7 @@ interface MessageBase<K extends keyof MessageDefinitions> {
|
|||||||
|
|
||||||
type Messages = {
|
type Messages = {
|
||||||
[K in keyof MessageDefinitions]: MessageBase<K>;
|
[K in keyof MessageDefinitions]: MessageBase<K>;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For better call semantics, make message data key optional if
|
* For better call semantics, make message data key optional if
|
||||||
@@ -172,39 +155,35 @@ type NarrowedMessage<L extends MessageBase<keyof MessageDefinitions>> =
|
|||||||
: L
|
: L
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
|
|
||||||
export type Port = TypedPort<Message>;
|
export type Port = TypedPort<Message>;
|
||||||
export type Message = NarrowedMessage<Messages[keyof Messages]>;
|
export type Message = NarrowedMessage<Messages[keyof Messages]>;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Typed WebExtension-style messaging utility class.
|
* Typed WebExtension-style messaging utility class.
|
||||||
*/
|
*/
|
||||||
class Messenger<T> {
|
class Messenger<T> {
|
||||||
connect(connectInfo: { name: string; }) {
|
connect(connectInfo: { name: string }) {
|
||||||
return browser.runtime.connect(connectInfo) as
|
return browser.runtime.connect(connectInfo) as unknown as TypedPort<T>;
|
||||||
unknown as TypedPort<T>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectTab(tabId: number
|
connectTab(tabId: number, connectInfo: { name: string; frameId: number }) {
|
||||||
, connectInfo: { name: string
|
return browser.tabs.connect(
|
||||||
, frameId: number }) {
|
tabId,
|
||||||
|
connectInfo
|
||||||
return browser.tabs.connect(tabId, connectInfo) as
|
) as unknown as TypedPort<T>;
|
||||||
unknown as TypedPort<T>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onConnect = {
|
onConnect = {
|
||||||
addListener(cb: (port: TypedPort<T>) => void) {
|
addListener(cb: (port: TypedPort<T>) => void) {
|
||||||
browser.runtime.onConnect.addListener(cb as any);
|
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);
|
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);
|
return browser.runtime.onConnect.hasListener(cb as any);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Messenger<Message>();
|
export default new Messenger<Message>();
|
||||||
|
|||||||
@@ -7,18 +7,20 @@ import cast, { ensureInit } from "../../shim/export";
|
|||||||
import { Message } from "../../messaging";
|
import { Message } from "../../messaging";
|
||||||
import { ReceiverDevice } from "../../types";
|
import { ReceiverDevice } from "../../types";
|
||||||
|
|
||||||
|
function startMediaServer(
|
||||||
function startMediaServer(filePath: string, port: number)
|
filePath: string,
|
||||||
: Promise<{ mediaPath: string
|
port: number
|
||||||
, subtitlePaths: string[]
|
): Promise<{
|
||||||
, localAddress: string }> {
|
mediaPath: string;
|
||||||
|
subtitlePaths: string[];
|
||||||
|
localAddress: string;
|
||||||
|
}> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
backgroundPort.postMessage({
|
backgroundPort.postMessage({
|
||||||
subject: "bridge:startMediaServer"
|
subject: "bridge:startMediaServer",
|
||||||
, data: {
|
data: {
|
||||||
filePath: decodeURI(filePath)
|
filePath: decodeURI(filePath),
|
||||||
, port
|
port
|
||||||
}
|
}
|
||||||
} as Message);
|
} as Message);
|
||||||
|
|
||||||
@@ -45,7 +47,6 @@ function startMediaServer(filePath: string, port: number)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let backgroundPort: MessagePort;
|
let backgroundPort: MessagePort;
|
||||||
|
|
||||||
let currentSession: cast.Session;
|
let currentSession: cast.Session;
|
||||||
@@ -53,7 +54,6 @@ let currentMedia: cast.media.Media;
|
|||||||
|
|
||||||
let targetElement: HTMLElement;
|
let targetElement: HTMLElement;
|
||||||
|
|
||||||
|
|
||||||
function getSession(opts: InitOptions): Promise<cast.Session> {
|
function getSession(opts: InitOptions): Promise<cast.Session> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
/**
|
/**
|
||||||
@@ -64,10 +64,11 @@ function getSession(opts: InitOptions): Promise<cast.Session> {
|
|||||||
function receiverListener(availability: string) {
|
function receiverListener(availability: string) {
|
||||||
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
||||||
cast.requestSession(
|
cast.requestSession(
|
||||||
onRequestSessionSuccess
|
onRequestSessionSuccess,
|
||||||
, onRequestSessionError
|
onRequestSessionError,
|
||||||
, undefined
|
undefined,
|
||||||
, opts.receiver);
|
opts.receiver
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,15 +83,15 @@ function getSession(opts: InitOptions): Promise<cast.Session> {
|
|||||||
reject(err.description);
|
reject(err.description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const sessionRequest = new cast.SessionRequest(
|
const sessionRequest = new cast.SessionRequest(
|
||||||
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
|
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID
|
||||||
|
);
|
||||||
|
|
||||||
const apiConfig = new cast.ApiConfig(
|
const apiConfig = new cast.ApiConfig(
|
||||||
sessionRequest
|
sessionRequest,
|
||||||
, sessionListener // sessionListener
|
sessionListener, // sessionListener
|
||||||
, receiverListener); // receiverListener
|
receiverListener
|
||||||
|
); // receiverListener
|
||||||
|
|
||||||
cast.initialize(apiConfig);
|
cast.initialize(apiConfig);
|
||||||
});
|
});
|
||||||
@@ -113,19 +114,18 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
|
|||||||
try {
|
try {
|
||||||
// Wait until media server is listening
|
// Wait until media server is listening
|
||||||
const { localAddress, mediaPath, subtitlePaths } =
|
const { localAddress, mediaPath, subtitlePaths } =
|
||||||
await startMediaServer(mediaTitle, port);
|
await startMediaServer(mediaTitle, port);
|
||||||
|
|
||||||
const baseUrl = new URL(`http://${localAddress}:${port}/`);
|
const baseUrl = new URL(`http://${localAddress}:${port}/`);
|
||||||
mediaUrl = new URL(mediaPath, baseUrl);
|
mediaUrl = new URL(mediaPath, baseUrl);
|
||||||
subtitleUrls = subtitlePaths.map(
|
subtitleUrls = subtitlePaths.map(
|
||||||
path => new URL(path, baseUrl));
|
path => new URL(path, baseUrl)
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw logger.error("Failed to start media server");
|
throw logger.error("Failed to start media server");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const activeTrackIds: number[] = [];
|
const activeTrackIds: number[] = [];
|
||||||
const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, "");
|
const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, "");
|
||||||
|
|
||||||
@@ -137,7 +137,9 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
|
|||||||
let trackIndex = 0;
|
let trackIndex = 0;
|
||||||
for (const subtitleUrl of subtitleUrls) {
|
for (const subtitleUrl of subtitleUrls) {
|
||||||
const castTrack = new cast.media.Track(
|
const castTrack = new cast.media.Track(
|
||||||
trackIndex, cast.media.TrackType.TEXT);
|
trackIndex,
|
||||||
|
cast.media.TrackType.TEXT
|
||||||
|
);
|
||||||
|
|
||||||
castTrack.name = subtitleUrl.pathname;
|
castTrack.name = subtitleUrl.pathname;
|
||||||
castTrack.trackContentId = subtitleUrl.href;
|
castTrack.trackContentId = subtitleUrl.href;
|
||||||
@@ -168,7 +170,9 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
|
|||||||
* and type as TrackType.TEXT.
|
* and type as TrackType.TEXT.
|
||||||
*/
|
*/
|
||||||
const castTrack = new cast.media.Track(
|
const castTrack = new cast.media.Track(
|
||||||
trackIndex, cast.media.TrackType.TEXT);
|
trackIndex,
|
||||||
|
cast.media.TrackType.TEXT
|
||||||
|
);
|
||||||
|
|
||||||
// Copy TextTrack properties
|
// Copy TextTrack properties
|
||||||
castTrack.name = track.label || `track-${trackIndex}`;
|
castTrack.name = track.label || `track-${trackIndex}`;
|
||||||
@@ -179,29 +183,29 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
|
|||||||
switch (track.kind) {
|
switch (track.kind) {
|
||||||
case "subtitles":
|
case "subtitles":
|
||||||
castTrack.subtype =
|
castTrack.subtype =
|
||||||
cast.media.TextTrackType.SUBTITLES;
|
cast.media.TextTrackType.SUBTITLES;
|
||||||
break;
|
break;
|
||||||
case "captions":
|
case "captions":
|
||||||
castTrack.subtype =
|
castTrack.subtype =
|
||||||
cast.media.TextTrackType.CAPTIONS;
|
cast.media.TextTrackType.CAPTIONS;
|
||||||
break;
|
break;
|
||||||
case "descriptions":
|
case "descriptions":
|
||||||
castTrack.subtype =
|
castTrack.subtype =
|
||||||
cast.media.TextTrackType.DESCRIPTIONS;
|
cast.media.TextTrackType.DESCRIPTIONS;
|
||||||
break;
|
break;
|
||||||
case "chapters":
|
case "chapters":
|
||||||
castTrack.subtype =
|
castTrack.subtype =
|
||||||
cast.media.TextTrackType.CHAPTERS;
|
cast.media.TextTrackType.CHAPTERS;
|
||||||
break;
|
break;
|
||||||
case "metadata":
|
case "metadata":
|
||||||
castTrack.subtype =
|
castTrack.subtype =
|
||||||
cast.media.TextTrackType.METADATA;
|
cast.media.TextTrackType.METADATA;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Default to subtitles
|
// Default to subtitles
|
||||||
default:
|
default:
|
||||||
castTrack.subtype =
|
castTrack.subtype =
|
||||||
cast.media.TextTrackType.SUBTITLES;
|
cast.media.TextTrackType.SUBTITLES;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add track to mediaInfo
|
// Add track to mediaInfo
|
||||||
@@ -225,7 +229,6 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let ignoreMediaEvents = false;
|
let ignoreMediaEvents = false;
|
||||||
|
|
||||||
async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
||||||
@@ -244,7 +247,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
|||||||
mediaElement.addEventListener("ratechange", checkIgnore, true);
|
mediaElement.addEventListener("ratechange", checkIgnore, true);
|
||||||
mediaElement.addEventListener("volumechange", checkIgnore, true);
|
mediaElement.addEventListener("volumechange", checkIgnore, true);
|
||||||
|
|
||||||
|
|
||||||
mediaElement.addEventListener("play", () => {
|
mediaElement.addEventListener("play", () => {
|
||||||
currentMedia.play();
|
currentMedia.play();
|
||||||
});
|
});
|
||||||
@@ -270,15 +272,15 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
|||||||
|
|
||||||
mediaElement.addEventListener("volumechange", () => {
|
mediaElement.addEventListener("volumechange", () => {
|
||||||
const newVolume = new cast.Volume(
|
const newVolume = new cast.Volume(
|
||||||
currentMedia.volume.level
|
currentMedia.volume.level,
|
||||||
, currentMedia.volume.muted);
|
currentMedia.volume.muted
|
||||||
|
);
|
||||||
|
|
||||||
const volumeRequest = new cast.media.VolumeRequest(newVolume);
|
const volumeRequest = new cast.media.VolumeRequest(newVolume);
|
||||||
|
|
||||||
currentMedia.setVolume(volumeRequest);
|
currentMedia.setVolume(volumeRequest);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
currentMedia.addUpdateListener(isAlive => {
|
currentMedia.addUpdateListener(isAlive => {
|
||||||
if (!isAlive) {
|
if (!isAlive) {
|
||||||
return;
|
return;
|
||||||
@@ -303,7 +305,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const localRepeatMode = mediaElement.loop
|
const localRepeatMode = mediaElement.loop
|
||||||
? cast.media.RepeatMode.SINGLE
|
? cast.media.RepeatMode.SINGLE
|
||||||
: cast.media.RepeatMode.OFF;
|
: cast.media.RepeatMode.OFF;
|
||||||
@@ -323,7 +324,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (currentMedia.currentTime !== mediaElement.currentTime) {
|
if (currentMedia.currentTime !== mediaElement.currentTime) {
|
||||||
ignoreMediaEvents = true;
|
ignoreMediaEvents = true;
|
||||||
mediaElement.currentTime = currentMedia.currentTime;
|
mediaElement.currentTime = currentMedia.currentTime;
|
||||||
@@ -332,7 +332,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface InitOptions {
|
interface InitOptions {
|
||||||
mediaUrl: string;
|
mediaUrl: string;
|
||||||
receiver?: ReceiverDevice;
|
receiver?: ReceiverDevice;
|
||||||
@@ -355,7 +354,8 @@ export async function init(opts: InitOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
targetElement = browser.menus.getTargetElement(
|
targetElement = browser.menus.getTargetElement(
|
||||||
opts.targetElementId) as HTMLMediaElement;
|
opts.targetElementId
|
||||||
|
) as HTMLMediaElement;
|
||||||
|
|
||||||
currentSession = await getSession(opts);
|
currentSession = await getSession(opts);
|
||||||
currentMedia = await getMedia(opts);
|
currentMedia = await getMedia(opts);
|
||||||
@@ -384,11 +384,11 @@ export async function init(opts: InitOptions) {
|
|||||||
* provided on the window object.
|
* provided on the window object.
|
||||||
*/
|
*/
|
||||||
if (window.location.protocol !== "moz-extension:") {
|
if (window.location.protocol !== "moz-extension:") {
|
||||||
const _window = (window as any);
|
const _window = window as any;
|
||||||
|
|
||||||
init({
|
init({
|
||||||
mediaUrl: _window.mediaUrl
|
mediaUrl: _window.mediaUrl,
|
||||||
, receiver: _window.receiver
|
receiver: _window.receiver,
|
||||||
, targetElementId: _window.targetElementId
|
targetElementId: _window.targetElementId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,13 @@
|
|||||||
* descriptor is found, otherwise return undefined.
|
* descriptor is found, otherwise return undefined.
|
||||||
*/
|
*/
|
||||||
export function getPropertyDescriptor(
|
export function getPropertyDescriptor(
|
||||||
target: any, prop: string | number | symbol)
|
target: any,
|
||||||
: PropertyDescriptor | undefined {
|
prop: string | number | symbol
|
||||||
|
): PropertyDescriptor | undefined {
|
||||||
let desc: PropertyDescriptor | undefined;
|
let desc: PropertyDescriptor | undefined;
|
||||||
while (!desc && target !== null) {
|
while (!desc && target !== null) {
|
||||||
desc = Object.getOwnPropertyDescriptor(target, prop);
|
desc = Object.getOwnPropertyDescriptor(target, prop);
|
||||||
if (!desc) {
|
if (!desc) target = Object.getPrototypeOf(target);
|
||||||
target = Object.getPrototypeOf(target);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return desc;
|
return desc;
|
||||||
@@ -24,14 +22,14 @@ export function getPropertyDescriptor(
|
|||||||
* to a target object.
|
* to a target object.
|
||||||
*/
|
*/
|
||||||
export function bindPropertyDescriptor(
|
export function bindPropertyDescriptor(
|
||||||
desc: PropertyDescriptor, target: any)
|
desc: PropertyDescriptor,
|
||||||
: PropertyDescriptor {
|
target: any
|
||||||
|
): PropertyDescriptor {
|
||||||
if (typeof desc.value === "function") {
|
if (typeof desc.value === "function") {
|
||||||
desc.value = desc.value.bind(target);
|
desc.value = desc.value.bind(target);
|
||||||
} else {
|
} else {
|
||||||
if (desc.get) { desc.get = desc.get.bind(target); }
|
if (desc.get) desc.get = desc.get.bind(target);
|
||||||
if (desc.set) { desc.set = desc.set.bind(target); }
|
if (desc.set) desc.set = desc.set.bind(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
return desc;
|
return desc;
|
||||||
@@ -43,9 +41,9 @@ export function bindPropertyDescriptor(
|
|||||||
* element and collect them into a property descriptor map.
|
* element and collect them into a property descriptor map.
|
||||||
*/
|
*/
|
||||||
export function clonePropsDescriptor<T>(
|
export function clonePropsDescriptor<T>(
|
||||||
target: T, props: any[])
|
target: T,
|
||||||
: PropertyDescriptorMap {
|
props: any[]
|
||||||
|
): PropertyDescriptorMap {
|
||||||
return props.reduce<PropertyDescriptorMap>((descriptorMap, prop) => {
|
return props.reduce<PropertyDescriptorMap>((descriptorMap, prop) => {
|
||||||
const desc = getPropertyDescriptor(target, prop);
|
const desc = getPropertyDescriptor(target, prop);
|
||||||
if (desc) {
|
if (desc) {
|
||||||
@@ -59,17 +57,19 @@ export function clonePropsDescriptor<T>(
|
|||||||
|
|
||||||
export function makeGetterDescriptor(val: any): PropertyDescriptor {
|
export function makeGetterDescriptor(val: any): PropertyDescriptor {
|
||||||
return {
|
return {
|
||||||
enumerable: true
|
enumerable: true,
|
||||||
, configurable: true
|
configurable: true,
|
||||||
, get() { return val; }
|
get() {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeValueDescriptor(val: any): PropertyDescriptor {
|
export function makeValueDescriptor(val: any): PropertyDescriptor {
|
||||||
return {
|
return {
|
||||||
enumerable: true
|
enumerable: true,
|
||||||
, configurable: true
|
configurable: true,
|
||||||
, writable: true
|
writable: true,
|
||||||
, value: val
|
value: val
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,19 @@
|
|||||||
|
|
||||||
import logger from "../../../lib/logger";
|
import logger from "../../../lib/logger";
|
||||||
|
|
||||||
import { bindPropertyDescriptor
|
import {
|
||||||
, clonePropsDescriptor
|
bindPropertyDescriptor,
|
||||||
, getPropertyDescriptor
|
clonePropsDescriptor,
|
||||||
, makeGetterDescriptor
|
getPropertyDescriptor,
|
||||||
, makeValueDescriptor } from "./descriptorUtils";
|
makeGetterDescriptor,
|
||||||
|
makeValueDescriptor
|
||||||
|
} from "./descriptorUtils";
|
||||||
|
|
||||||
// Injected by content loader
|
// Injected by content loader
|
||||||
declare const iconAirPlayAudio: string;
|
declare const iconAirPlayAudio: string;
|
||||||
declare const iconAirPlayVideo: string;
|
declare const iconAirPlayVideo: string;
|
||||||
declare const mediaOverlayTitle: string;
|
declare const mediaOverlayTitle: string;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intercept and store references to shadow root nodes created by
|
* Intercept and store references to shadow root nodes created by
|
||||||
* calls to `attachShadow`. Used to reference shadow roots, even when
|
* calls to `attachShadow`. Used to reference shadow roots, even when
|
||||||
@@ -27,7 +28,6 @@ Element.prototype.attachShadow = function (init) {
|
|||||||
return shadowRoot;
|
return shadowRoot;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function getShadowRootFromNode(node: Node): ShadowRoot | undefined {
|
function getShadowRootFromNode(node: Node): ShadowRoot | undefined {
|
||||||
// Don't touch our custom element
|
// Don't touch our custom element
|
||||||
if (node instanceof PlayerElement) {
|
if (node instanceof PlayerElement) {
|
||||||
@@ -37,7 +37,6 @@ function getShadowRootFromNode(node: Node): ShadowRoot | undefined {
|
|||||||
return internalShadowRoots.get(node as Element);
|
return internalShadowRoots.get(node as Element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const DQS_XPATH_EXPRESSION = `//*[contains(name(), "-")]`;
|
const DQS_XPATH_EXPRESSION = `//*[contains(name(), "-")]`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,12 +45,15 @@ const DQS_XPATH_EXPRESSION = `//*[contains(name(), "-")]`;
|
|||||||
*/
|
*/
|
||||||
function deepQuerySelector(selector: string): Element | null {
|
function deepQuerySelector(selector: string): Element | null {
|
||||||
const result = document.evaluate(
|
const result = document.evaluate(
|
||||||
DQS_XPATH_EXPRESSION, document, null
|
DQS_XPATH_EXPRESSION,
|
||||||
, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
|
document,
|
||||||
|
null,
|
||||||
|
XPathResult.ORDERED_NODE_ITERATOR_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
let node: Node | null;
|
let node: Node | null;
|
||||||
// eslint-disable-next-line no-cond-assign
|
// eslint-disable-next-line no-cond-assign
|
||||||
while (node = result.iterateNext()) {
|
while ((node = result.iterateNext())) {
|
||||||
const shadowRoot = getShadowRootFromNode(node);
|
const shadowRoot = getShadowRootFromNode(node);
|
||||||
if (!shadowRoot) {
|
if (!shadowRoot) {
|
||||||
continue;
|
continue;
|
||||||
@@ -72,14 +74,17 @@ function deepQuerySelector(selector: string): Element | null {
|
|||||||
*/
|
*/
|
||||||
function deepQuerySelectorAll(selector: string): Node[] {
|
function deepQuerySelectorAll(selector: string): Node[] {
|
||||||
const result = document.evaluate(
|
const result = document.evaluate(
|
||||||
DQS_XPATH_EXPRESSION, document, null
|
DQS_XPATH_EXPRESSION,
|
||||||
, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
|
document,
|
||||||
|
null,
|
||||||
|
XPathResult.ORDERED_NODE_ITERATOR_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
const nodes: Node[] = [];
|
const nodes: Node[] = [];
|
||||||
|
|
||||||
let node: Node | null;
|
let node: Node | null;
|
||||||
// eslint-disable-next-line no-cond-assign
|
// eslint-disable-next-line no-cond-assign
|
||||||
while (node = result.iterateNext()) {
|
while ((node = result.iterateNext())) {
|
||||||
const shadowRoot = getShadowRootFromNode(node);
|
const shadowRoot = getShadowRootFromNode(node);
|
||||||
if (shadowRoot) {
|
if (shadowRoot) {
|
||||||
nodes.push(...shadowRoot.querySelectorAll(selector));
|
nodes.push(...shadowRoot.querySelectorAll(selector));
|
||||||
@@ -89,26 +94,45 @@ function deepQuerySelectorAll(selector: string): Node[] {
|
|||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const mediaElementTypes = [
|
const mediaElementTypes = [
|
||||||
HTMLMediaElement
|
HTMLMediaElement,
|
||||||
, HTMLVideoElement
|
HTMLVideoElement,
|
||||||
, HTMLAudioElement
|
HTMLAudioElement
|
||||||
];
|
];
|
||||||
|
|
||||||
const mediaElementEvents = [
|
const mediaElementEvents = [
|
||||||
"abort", "canplay", "canplaythrough", "durationchange", "emptied"
|
"abort",
|
||||||
, "encrypted", "ended", "error", "interruptbegin", "interruptend"
|
"canplay",
|
||||||
, "loadeddata", "loadedmetadata", "loadstart", "mozaudioavailable", "pause"
|
"canplaythrough",
|
||||||
, "play", "playing", "progress", "ratechange", "seeked", "seeking", "stalled"
|
"durationchange",
|
||||||
, "suspend", "timeupdate", "volumechange", "waiting"
|
"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
|
const mediaElementAttributes = mediaElementTypes
|
||||||
.flatMap(type => Object.getOwnPropertyNames(type.prototype))
|
.flatMap(type => Object.getOwnPropertyNames(type.prototype))
|
||||||
.concat(mediaElementEvents.map(ev => `on${ev}`));
|
.concat(mediaElementEvents.map(ev => `on${ev}`));
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opaque wrapper around the media element to provide an overlay without
|
* Opaque wrapper around the media element to provide an overlay without
|
||||||
* author interference. Relevant properties, attributes, events and
|
* author interference. Relevant properties, attributes, events and
|
||||||
@@ -125,8 +149,14 @@ class PlayerElement extends HTMLElement {
|
|||||||
switch (this.constructor) {
|
switch (this.constructor) {
|
||||||
// URL variables injected ahead of current script
|
// URL variables injected ahead of current script
|
||||||
|
|
||||||
case AudioPlayerElement: { iconUrl = iconAirPlayAudio; break; }
|
case AudioPlayerElement: {
|
||||||
case VideoPlayerElement: { iconUrl = iconAirPlayVideo; break; }
|
iconUrl = iconAirPlayAudio;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case VideoPlayerElement: {
|
||||||
|
iconUrl = iconAirPlayVideo;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shadowRoot.innerHTML = `
|
shadowRoot.innerHTML = `
|
||||||
@@ -192,10 +222,19 @@ class PlayerElement extends HTMLElement {
|
|||||||
* listeners, etc... on the media element, but since it's hidden
|
* listeners, etc... on the media element, but since it's hidden
|
||||||
* within the shadow DOM, these properties must be proxied.
|
* within the shadow DOM, these properties must be proxied.
|
||||||
*/
|
*/
|
||||||
Object.defineProperties(host, clonePropsDescriptor(videoElement, [
|
Object.defineProperties(
|
||||||
"attributes", "setAttribute", "removeAttribute", "setAttribute"
|
host,
|
||||||
, "addEventListener", "removeEventListener", "hasEventListener"
|
clonePropsDescriptor(videoElement, [
|
||||||
, ...mediaElementAttributes as any ]));
|
"attributes",
|
||||||
|
"setAttribute",
|
||||||
|
"removeAttribute",
|
||||||
|
"setAttribute",
|
||||||
|
"addEventListener",
|
||||||
|
"removeEventListener",
|
||||||
|
"hasEventListener",
|
||||||
|
...(mediaElementAttributes as any)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
shadowRoot.prepend(videoElement);
|
shadowRoot.prepend(videoElement);
|
||||||
}
|
}
|
||||||
@@ -217,13 +256,14 @@ try {
|
|||||||
customElements.define("audio-player-element", AudioPlayerElement);
|
customElements.define("audio-player-element", AudioPlayerElement);
|
||||||
customElements.define("video-player-element", VideoPlayerElement);
|
customElements.define("video-player-element", VideoPlayerElement);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof DOMException
|
if (
|
||||||
&& err.code === DOMException.NOT_SUPPORTED_ERR) {
|
err instanceof DOMException &&
|
||||||
|
err.code === DOMException.NOT_SUPPORTED_ERR
|
||||||
|
) {
|
||||||
// Script already injected
|
// Script already injected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Original functions
|
// Original functions
|
||||||
const _createElement = document.createElement;
|
const _createElement = document.createElement;
|
||||||
const _createElementNS = document.createElementNS;
|
const _createElementNS = document.createElementNS;
|
||||||
@@ -233,23 +273,22 @@ const _createElementNS = document.createElementNS;
|
|||||||
* custom element version that imitates the original. Otherwise, returns
|
* custom element version that imitates the original. Otherwise, returns
|
||||||
* the result of the original.
|
* the result of the original.
|
||||||
*/
|
*/
|
||||||
function createElement(
|
function createElement(tagName: string, options?: ElementCreationOptions) {
|
||||||
tagName: string
|
|
||||||
, options?: ElementCreationOptions) {
|
|
||||||
|
|
||||||
// Normalize formatting
|
// Normalize formatting
|
||||||
const lowerTagName = tagName.toLowerCase();
|
const lowerTagName = tagName.toLowerCase();
|
||||||
const upperTagName = tagName.toUpperCase();
|
const upperTagName = tagName.toUpperCase();
|
||||||
|
|
||||||
if (lowerTagName === "audio" || lowerTagName === "video") {
|
if (lowerTagName === "audio" || lowerTagName === "video") {
|
||||||
const fakeElement = _createElement.call(document
|
const fakeElement = _createElement.call(
|
||||||
, `${lowerTagName}-player-element`) as HTMLMediaElement;
|
document,
|
||||||
|
`${lowerTagName}-player-element`
|
||||||
|
) as HTMLMediaElement;
|
||||||
|
|
||||||
// Ensure all references to the element name match tagName
|
// Ensure all references to the element name match tagName
|
||||||
Object.defineProperties(fakeElement, {
|
Object.defineProperties(fakeElement, {
|
||||||
tagName: makeGetterDescriptor(upperTagName)
|
tagName: makeGetterDescriptor(upperTagName),
|
||||||
, nodeName: makeGetterDescriptor(upperTagName)
|
nodeName: makeGetterDescriptor(upperTagName),
|
||||||
, localName: makeGetterDescriptor(lowerTagName)
|
localName: makeGetterDescriptor(lowerTagName)
|
||||||
});
|
});
|
||||||
|
|
||||||
return fakeElement;
|
return fakeElement;
|
||||||
@@ -264,34 +303,41 @@ function createElement(
|
|||||||
* original.
|
* original.
|
||||||
*/
|
*/
|
||||||
function createElementNS(
|
function createElementNS(
|
||||||
namespaceURI: string
|
namespaceURI: string,
|
||||||
, qualifiedName: string
|
qualifiedName: string,
|
||||||
, options?: ElementCreationOptions) {
|
options?: ElementCreationOptions
|
||||||
|
) {
|
||||||
if (namespaceURI === document.namespaceURI) {
|
if (namespaceURI === document.namespaceURI) {
|
||||||
return createElement(qualifiedName, options);
|
return createElement(qualifiedName, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _createElementNS.call(document
|
return _createElementNS.call(
|
||||||
, namespaceURI, qualifiedName, options);
|
document,
|
||||||
|
namespaceURI,
|
||||||
|
qualifiedName,
|
||||||
|
options
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to hide function source from page scripts by returning the
|
* Attempt to hide function source from page scripts by returning the
|
||||||
* toString/toSource values of the native function.
|
* toString/toSource values of the native function.
|
||||||
*/
|
*/
|
||||||
Object.defineProperties(createElement, clonePropsDescriptor(
|
Object.defineProperties(
|
||||||
_createElement, [ "toString", "toSource" ]));
|
createElement,
|
||||||
Object.defineProperties(createElementNS, clonePropsDescriptor(
|
clonePropsDescriptor(_createElement, ["toString", "toSource"])
|
||||||
_createElementNS, [ "toString", "toSource" ]));
|
);
|
||||||
|
Object.defineProperties(
|
||||||
|
createElementNS,
|
||||||
|
clonePropsDescriptor(_createElementNS, ["toString", "toSource"])
|
||||||
|
);
|
||||||
|
|
||||||
// Re-define element creation functions
|
// Re-define element creation functions
|
||||||
Object.defineProperties(document, {
|
Object.defineProperties(document, {
|
||||||
createElement: makeValueDescriptor(createElement)
|
createElement: makeValueDescriptor(createElement),
|
||||||
, createElementNS: makeValueDescriptor(createElementNS)
|
createElementNS: makeValueDescriptor(createElementNS)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a media element, creates a `PlayerElement` via the patched
|
* Takes a media element, creates a `PlayerElement` via the patched
|
||||||
* `createElement` function, fetches the shadow root and copies any
|
* `createElement` function, fetches the shadow root and copies any
|
||||||
@@ -321,7 +367,10 @@ function wrapMediaElement(mediaElement: HTMLMediaElement) {
|
|||||||
* internal media element instead.
|
* internal media element instead.
|
||||||
*/
|
*/
|
||||||
HTMLElement.prototype.setAttribute.call(
|
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 mediaElements = document.querySelectorAll(mediaSelector);
|
||||||
const deepMediaElements = deepQuerySelectorAll(mediaSelector);
|
const deepMediaElements = deepQuerySelectorAll(mediaSelector);
|
||||||
|
|
||||||
for (const mediaElement of [ ...mediaElements, ...deepMediaElements ]) {
|
for (const mediaElement of [...mediaElements, ...deepMediaElements]) {
|
||||||
wrapMediaElement(mediaElement as HTMLMediaElement);
|
wrapMediaElement(mediaElement as HTMLMediaElement);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,17 +7,22 @@ const _ = browser.i18n.getMessage;
|
|||||||
* scripts from loading before its execution.
|
* scripts from loading before its execution.
|
||||||
*/
|
*/
|
||||||
const req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
req.open("GET", browser.runtime.getURL(
|
req.open(
|
||||||
"senders/media/overlay/overlayContent.js"), false);
|
"GET",
|
||||||
|
browser.runtime.getURL("senders/media/overlay/overlayContent.js"),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
req.send();
|
req.send();
|
||||||
|
|
||||||
if (req.status === 200) {
|
if (req.status === 200) {
|
||||||
// TODO: Replace with cast icons until AirPlay support is ready
|
// TODO: Replace with cast icons until AirPlay support is ready
|
||||||
const iconAirPlayAudio = browser.runtime.getURL(
|
const iconAirPlayAudio = browser.runtime.getURL(
|
||||||
"senders/media/overlay/AirPlay_Audio.svg");
|
"senders/media/overlay/AirPlay_Audio.svg"
|
||||||
|
);
|
||||||
const iconAirPlayVideo = browser.runtime.getURL(
|
const iconAirPlayVideo = browser.runtime.getURL(
|
||||||
"senders/media/overlay/AirPlay_Audio.svg");
|
"senders/media/overlay/AirPlay_Audio.svg"
|
||||||
|
);
|
||||||
|
|
||||||
const scriptElement = document.createElement("script");
|
const scriptElement = document.createElement("script");
|
||||||
scriptElement.textContent = `(function(){
|
scriptElement.textContent = `(function(){
|
||||||
|
|||||||
@@ -6,23 +6,22 @@ import cast, { ensureInit } from "../shim/export";
|
|||||||
import { ReceiverSelectorMediaType } from "../background/receiverSelector";
|
import { ReceiverSelectorMediaType } from "../background/receiverSelector";
|
||||||
import { ReceiverDevice } from "../types";
|
import { ReceiverDevice } from "../types";
|
||||||
|
|
||||||
|
|
||||||
// Variables passed from background
|
// Variables passed from background
|
||||||
const { selectedMedia
|
const {
|
||||||
, selectedReceiver }
|
selectedMedia,
|
||||||
: { selectedMedia: ReceiverSelectorMediaType
|
selectedReceiver
|
||||||
, selectedReceiver: ReceiverDevice } = (window as any);
|
}: {
|
||||||
|
selectedMedia: ReceiverSelectorMediaType;
|
||||||
|
selectedReceiver: ReceiverDevice;
|
||||||
|
} = window as any;
|
||||||
|
|
||||||
const FX_CAST_RECEIVER_APP_NAMESPACE = "urn:x-cast:fx_cast";
|
const FX_CAST_RECEIVER_APP_NAMESPACE = "urn:x-cast:fx_cast";
|
||||||
|
|
||||||
|
|
||||||
let session: cast.Session;
|
let session: cast.Session;
|
||||||
let wasSessionRequested = false;
|
let wasSessionRequested = false;
|
||||||
|
|
||||||
let peerConnection: RTCPeerConnection;
|
let peerConnection: RTCPeerConnection;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message to the fx_cast app running on the
|
* Sends a message to the fx_cast app running on the
|
||||||
* receiver device.
|
* receiver device.
|
||||||
@@ -33,40 +32,39 @@ function sendAppMessage(subject: string, data: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.sendMessage(FX_CAST_RECEIVER_APP_NAMESPACE, {
|
session.sendMessage(FX_CAST_RECEIVER_APP_NAMESPACE, {
|
||||||
subject
|
subject,
|
||||||
, data
|
data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", () => {
|
window.addEventListener("beforeunload", () => {
|
||||||
sendAppMessage("close", null);
|
sendAppMessage("close", null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
async function onRequestSessionSuccess(newSession: cast.Session) {
|
async function onRequestSessionSuccess(newSession: cast.Session) {
|
||||||
cast.logMessage("onRequestSessionSuccess");
|
cast.logMessage("onRequestSessionSuccess");
|
||||||
|
|
||||||
session = newSession;
|
session = newSession;
|
||||||
session.addMessageListener(FX_CAST_RECEIVER_APP_NAMESPACE
|
session.addMessageListener(
|
||||||
, async (_namespace, message) => {
|
FX_CAST_RECEIVER_APP_NAMESPACE,
|
||||||
|
async (_namespace, message) => {
|
||||||
|
const { subject, data } = JSON.parse(message);
|
||||||
|
|
||||||
const { subject, data } = JSON.parse(message);
|
switch (subject) {
|
||||||
|
case "peerConnectionAnswer": {
|
||||||
switch (subject) {
|
peerConnection.setRemoteDescription(data);
|
||||||
case "peerConnectionAnswer": {
|
break;
|
||||||
peerConnection.setRemoteDescription(data);
|
}
|
||||||
break;
|
case "iceCandidate": {
|
||||||
}
|
peerConnection.addIceCandidate(data);
|
||||||
case "iceCandidate": {
|
break;
|
||||||
peerConnection.addIceCandidate(data);
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
peerConnection = new RTCPeerConnection();
|
peerConnection = new RTCPeerConnection();
|
||||||
peerConnection.addEventListener("icecandidate", (ev) => {
|
peerConnection.addEventListener("icecandidate", ev => {
|
||||||
sendAppMessage("iceCandidate", ev.candidate);
|
sendAppMessage("iceCandidate", ev.candidate);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,27 +93,29 @@ async function onRequestSessionSuccess(newSession: cast.Session) {
|
|||||||
|
|
||||||
// TODO: Test performance
|
// TODO: Test performance
|
||||||
const drawFlags =
|
const drawFlags =
|
||||||
ctx.DRAWWINDOW_DRAW_CARET
|
ctx.DRAWWINDOW_DRAW_CARET |
|
||||||
| ctx.DRAWWINDOW_DRAW_VIEW
|
ctx.DRAWWINDOW_DRAW_VIEW |
|
||||||
| ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES
|
ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
|
||||||
| ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
|
ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
|
||||||
|
|
||||||
let lastFrame: DOMHighResTimeStamp;
|
let lastFrame: DOMHighResTimeStamp;
|
||||||
window.requestAnimationFrame(
|
window.requestAnimationFrame(function draw(
|
||||||
function draw(now: DOMHighResTimeStamp) {
|
now: DOMHighResTimeStamp
|
||||||
|
) {
|
||||||
if (!lastFrame) {
|
if (!lastFrame) {
|
||||||
lastFrame = now;
|
lastFrame = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((now - lastFrame) > (1000 / 30)) {
|
if (now - lastFrame > 1000 / 30) {
|
||||||
ctx.drawWindow(
|
ctx.drawWindow(
|
||||||
window // window
|
window, // window
|
||||||
, 0, 0 // x, y
|
0,
|
||||||
, canvas.width // w
|
0, // x, y
|
||||||
, canvas.height // h
|
canvas.width, // w
|
||||||
, "white" // bgColor
|
canvas.height, // h
|
||||||
, drawFlags); // flags
|
"white", // bgColor
|
||||||
|
drawFlags
|
||||||
|
); // flags
|
||||||
|
|
||||||
lastFrame = now;
|
lastFrame = now;
|
||||||
}
|
}
|
||||||
@@ -134,8 +134,8 @@ async function onRequestSessionSuccess(newSession: cast.Session) {
|
|||||||
|
|
||||||
case ReceiverSelectorMediaType.Screen: {
|
case ReceiverSelectorMediaType.Screen: {
|
||||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
video: { cursor: "motion" }
|
video: { cursor: "motion" },
|
||||||
, audio: false
|
audio: false
|
||||||
});
|
});
|
||||||
|
|
||||||
peerConnection.addStream(stream);
|
peerConnection.addStream(stream);
|
||||||
@@ -152,7 +152,6 @@ async function onRequestSessionSuccess(newSession: cast.Session) {
|
|||||||
sendAppMessage("peerConnectionOffer", offer);
|
sendAppMessage("peerConnectionOffer", offer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function receiverListener(availability: string) {
|
function receiverListener(availability: string) {
|
||||||
cast.logMessage("receiverListener");
|
cast.logMessage("receiverListener");
|
||||||
|
|
||||||
@@ -162,14 +161,15 @@ function receiverListener(availability: string) {
|
|||||||
|
|
||||||
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
||||||
wasSessionRequested = true;
|
wasSessionRequested = true;
|
||||||
cast.requestSession(onRequestSessionSuccess
|
cast.requestSession(
|
||||||
, onRequestSessionError
|
onRequestSessionSuccess,
|
||||||
, undefined
|
onRequestSessionError,
|
||||||
, selectedReceiver);
|
undefined,
|
||||||
|
selectedReceiver
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onRequestSessionError() {
|
function onRequestSessionError() {
|
||||||
cast.logMessage("onRequestSessionError");
|
cast.logMessage("onRequestSessionError");
|
||||||
}
|
}
|
||||||
@@ -183,18 +183,17 @@ function onInitializeError() {
|
|||||||
cast.logMessage("onInitializeError");
|
cast.logMessage("onInitializeError");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ensureInit().then(async () => {
|
ensureInit().then(async () => {
|
||||||
const mirroringAppId = await options.get("mirroringAppId");
|
const mirroringAppId = await options.get("mirroringAppId");
|
||||||
const sessionRequest = new cast.SessionRequest(mirroringAppId);
|
const sessionRequest = new cast.SessionRequest(mirroringAppId);
|
||||||
|
|
||||||
const apiConfig = new cast.ApiConfig(
|
const apiConfig = new cast.ApiConfig(
|
||||||
sessionRequest
|
sessionRequest,
|
||||||
, sessionListener
|
sessionListener,
|
||||||
, receiverListener
|
receiverListener,
|
||||||
, undefined, undefined);
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
cast.initialize(apiConfig
|
cast.initialize(apiConfig, onInitializeSuccess, onInitializeError);
|
||||||
, onInitializeSuccess
|
|
||||||
, onInitializeError);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,32 +6,34 @@ import logger from "../../lib/logger";
|
|||||||
|
|
||||||
import { sendMessageResponse } from "../eventMessageChannel";
|
import { sendMessageResponse } from "../eventMessageChannel";
|
||||||
|
|
||||||
import { ErrorCallback
|
import {
|
||||||
, LoadSuccessCallback
|
ErrorCallback,
|
||||||
, MediaListener
|
LoadSuccessCallback,
|
||||||
, MessageListener
|
MediaListener,
|
||||||
, SuccessCallback
|
MessageListener,
|
||||||
, UpdateListener } from "../types";
|
SuccessCallback,
|
||||||
|
UpdateListener
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
import { MediaStatus
|
import {
|
||||||
, ReceiverMediaMessage
|
MediaStatus,
|
||||||
, SenderMediaMessage
|
ReceiverMediaMessage,
|
||||||
, SenderMessage } from "./types";
|
SenderMediaMessage,
|
||||||
|
SenderMessage
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
import { Image, Receiver, SenderApplication } from "./dataClasses";
|
import { Image, Receiver, SenderApplication } from "./dataClasses";
|
||||||
import { SessionStatus } from "./enums";
|
import { SessionStatus } from "./enums";
|
||||||
import { Media, LoadRequest, QueueLoadRequest, QueueItem } from "./media";
|
import { Media, LoadRequest, QueueLoadRequest, QueueItem } from "./media";
|
||||||
|
|
||||||
|
|
||||||
const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a media object and a media status object and merges
|
* Takes a media object and a media status object and merges
|
||||||
* the status with the existing media object, updating it with
|
* the status with the existing media object, updating it with
|
||||||
* new properties.
|
* new properties.
|
||||||
*/
|
*/
|
||||||
function updateMedia(media: Media, status: MediaStatus) {
|
function updateMedia(media: Media, status: MediaStatus) {
|
||||||
if (status.currentTime) {
|
if (status.currentTime) {
|
||||||
media._lastUpdateTime = Date.now();
|
media._lastUpdateTime = Date.now();
|
||||||
}
|
}
|
||||||
@@ -51,7 +53,8 @@ const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
|||||||
if (!newItem.media) {
|
if (!newItem.media) {
|
||||||
// Existing queue item with the same ID
|
// Existing queue item with the same ID
|
||||||
const existingItem = media.items?.find(
|
const existingItem = media.items?.find(
|
||||||
item => item.itemId === newItem.itemId);
|
item => item.itemId === newItem.itemId
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use existing queue item's media info if available
|
* 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) {
|
if (existingItem?.media) {
|
||||||
newItem.media = existingItem.media;
|
newItem.media = existingItem.media;
|
||||||
} else if (media.media
|
} else if (
|
||||||
&& newItem.itemId === media.currentItemId) {
|
media.media &&
|
||||||
|
newItem.itemId === media.currentItemId
|
||||||
|
) {
|
||||||
newItem.media = media.media;
|
newItem.media = media.media;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +78,6 @@ const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default class Session {
|
export default class Session {
|
||||||
#id = uuid();
|
#id = uuid();
|
||||||
|
|
||||||
@@ -86,14 +90,15 @@ export default class Session {
|
|||||||
_messageListeners = new Map<string, Set<MessageListener>>();
|
_messageListeners = new Map<string, Set<MessageListener>>();
|
||||||
_updateListeners = new Set<UpdateListener>();
|
_updateListeners = new Set<UpdateListener>();
|
||||||
|
|
||||||
|
_sendMessageCallbacks = new Map<
|
||||||
_sendMessageCallbacks =
|
string,
|
||||||
new Map<string, [ SuccessCallback?, ErrorCallback? ]>();
|
[SuccessCallback?, ErrorCallback?]
|
||||||
|
>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
#mediaMessageListener = (namespace: string, messageString: string) => {
|
#mediaMessageListener = (namespace: string, messageString: string) => {
|
||||||
if (namespace !== NS_MEDIA) return;
|
if (namespace !== NS_MEDIA) return;
|
||||||
|
|
||||||
const message: ReceiverMediaMessage = JSON.parse(messageString);
|
const message: ReceiverMediaMessage = JSON.parse(messageString);
|
||||||
@@ -102,17 +107,19 @@ export default class Session {
|
|||||||
// Update media
|
// Update media
|
||||||
for (const mediaStatus of message.status) {
|
for (const mediaStatus of message.status) {
|
||||||
let media = this.media.find(
|
let media = this.media.find(
|
||||||
media => media.mediaSessionId ===
|
media =>
|
||||||
mediaStatus.mediaSessionId);
|
media.mediaSessionId === mediaStatus.mediaSessionId
|
||||||
|
);
|
||||||
|
|
||||||
console.info(media);
|
console.info(media);
|
||||||
|
|
||||||
// Handle Media creation
|
// Handle Media creation
|
||||||
if (!media) {
|
if (!media) {
|
||||||
media = new Media(
|
media = new Media(
|
||||||
this.sessionId
|
this.sessionId,
|
||||||
, mediaStatus.mediaSessionId
|
mediaStatus.mediaSessionId,
|
||||||
, this.#sendMediaMessage);
|
this.#sendMediaMessage
|
||||||
|
);
|
||||||
|
|
||||||
this.media.push(media);
|
this.media.push(media);
|
||||||
this.#loadMediaSuccessCallback?.(media);
|
this.#loadMediaSuccessCallback?.(media);
|
||||||
@@ -128,44 +135,43 @@ export default class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a media message to the app receiver.
|
* Sends a media message to the app receiver.
|
||||||
* urn:x-cast:com.google.cast.media
|
* urn:x-cast:com.google.cast.media
|
||||||
*/
|
*/
|
||||||
#sendMediaMessage = (message: DistributiveOmit<
|
#sendMediaMessage = (
|
||||||
SenderMediaMessage, "requestId">) => {
|
message: DistributiveOmit<SenderMediaMessage, "requestId">
|
||||||
|
) => {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
this.sendMessage(
|
this.sendMessage(
|
||||||
"urn:x-cast:com.google.cast.media"
|
"urn:x-cast:com.google.cast.media",
|
||||||
, { ...message, requestId: 0 }
|
{ ...message, requestId: 0 },
|
||||||
, resolve, reject);
|
resolve,
|
||||||
|
reject
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
#sendReceiverMessage = (message: DistributiveOmit<
|
|
||||||
SenderMessage, "requestId">) => {
|
|
||||||
|
|
||||||
|
#sendReceiverMessage = (
|
||||||
|
message: DistributiveOmit<SenderMessage, "requestId">
|
||||||
|
) => {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const messageId = uuid();
|
const messageId = uuid();
|
||||||
|
|
||||||
sendMessageResponse({
|
sendMessageResponse({
|
||||||
subject: "bridge:sendCastReceiverMessage"
|
subject: "bridge:sendCastReceiverMessage",
|
||||||
, data: {
|
data: {
|
||||||
sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
, messageData: message as SenderMessage
|
messageData: message as SenderMessage,
|
||||||
, messageId
|
messageId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._sendMessageCallbacks.set(
|
this._sendMessageCallbacks.set(messageId, [resolve, reject]);
|
||||||
messageId, [ resolve, reject ]);
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
media: Media[] = [];
|
media: Media[] = [];
|
||||||
namespaces: Array<{ name: string }> = [];
|
namespaces: Array<{ name: string }> = [];
|
||||||
@@ -174,18 +180,18 @@ export default class Session {
|
|||||||
statusText: Nullable<string> = null;
|
statusText: Nullable<string> = null;
|
||||||
transportId: string;
|
transportId: string;
|
||||||
|
|
||||||
constructor(public sessionId: string
|
constructor(
|
||||||
, public appId: string
|
public sessionId: string,
|
||||||
, public displayName: string
|
public appId: string,
|
||||||
, public appImages: Image[]
|
public displayName: string,
|
||||||
, public receiver: Receiver) {
|
public appImages: Image[],
|
||||||
|
public receiver: Receiver
|
||||||
|
) {
|
||||||
this.transportId = sessionId || "";
|
this.transportId = sessionId || "";
|
||||||
|
|
||||||
this.addMessageListener(NS_MEDIA, this.#mediaMessageListener);
|
this.addMessageListener(NS_MEDIA, this.#mediaMessageListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
addMediaListener(_mediaListener: MediaListener) {
|
addMediaListener(_mediaListener: MediaListener) {
|
||||||
logger.info("STUB :: Session#addMediaListener");
|
logger.info("STUB :: Session#addMediaListener");
|
||||||
}
|
}
|
||||||
@@ -211,83 +217,80 @@ export default class Session {
|
|||||||
this._updateListeners.delete(listener);
|
this._updateListeners.delete(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
leave(_successCallback?: SuccessCallback
|
leave(_successCallback?: SuccessCallback, _errorCallback?: ErrorCallback) {
|
||||||
, _errorCallback?: ErrorCallback) {
|
|
||||||
|
|
||||||
logger.info("STUB :: Session#leave");
|
logger.info("STUB :: Session#leave");
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMedia(loadRequest: LoadRequest
|
loadMedia(
|
||||||
, successCallback?: LoadSuccessCallback
|
loadRequest: LoadRequest,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: LoadSuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
this.#loadMediaSuccessCallback = successCallback;
|
this.#loadMediaSuccessCallback = successCallback;
|
||||||
this.#loadMediaErrorCallback = errorCallback;
|
this.#loadMediaErrorCallback = errorCallback;
|
||||||
this.#loadMediaRequest = loadRequest;
|
this.#loadMediaRequest = loadRequest;
|
||||||
|
|
||||||
loadRequest.sessionId = this.sessionId;
|
loadRequest.sessionId = this.sessionId;
|
||||||
this.#sendMediaMessage(loadRequest)
|
this.#sendMediaMessage(loadRequest).catch(errorCallback);
|
||||||
.catch(errorCallback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queueLoad(_queueLoadRequest: QueueLoadRequest
|
queueLoad(
|
||||||
, _successCallback?: LoadSuccessCallback
|
_queueLoadRequest: QueueLoadRequest,
|
||||||
, _errorCallback?: ErrorCallback) {
|
_successCallback?: LoadSuccessCallback,
|
||||||
|
_errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
logger.info("STUB :: Session#queueLoad");
|
logger.info("STUB :: Session#queueLoad");
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(namespace: string
|
sendMessage(
|
||||||
, message: object | string
|
namespace: string,
|
||||||
, successCallback?: SuccessCallback
|
message: object | string,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
const messageId = uuid();
|
const messageId = uuid();
|
||||||
|
|
||||||
sendMessageResponse({
|
sendMessageResponse({
|
||||||
subject: "bridge:sendCastSessionMessage"
|
subject: "bridge:sendCastSessionMessage",
|
||||||
, data: {
|
data: {
|
||||||
sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
, namespace
|
namespace,
|
||||||
, messageData: message
|
messageData: message,
|
||||||
, messageId
|
messageId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._sendMessageCallbacks.set(messageId, [
|
this._sendMessageCallbacks.set(messageId, [
|
||||||
successCallback
|
successCallback,
|
||||||
, errorCallback
|
errorCallback
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setReceiverMuted(muted: boolean
|
setReceiverMuted(
|
||||||
, successCallback?: SuccessCallback
|
muted: boolean,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this.#sendReceiverMessage(
|
) {
|
||||||
{ type: "SET_VOLUME"
|
this.#sendReceiverMessage({ type: "SET_VOLUME", volume: { muted } })
|
||||||
, volume: { muted }})
|
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
setReceiverVolumeLevel(newLevel: number
|
setReceiverVolumeLevel(
|
||||||
, successCallback?: SuccessCallback
|
newLevel: number,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this.#sendReceiverMessage(
|
) {
|
||||||
{ type: "SET_VOLUME"
|
this.#sendReceiverMessage({
|
||||||
, volume: { level: newLevel }})
|
type: "SET_VOLUME",
|
||||||
|
volume: { level: newLevel }
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(successCallback?: SuccessCallback
|
stop(successCallback?: SuccessCallback, errorCallback?: ErrorCallback) {
|
||||||
, errorCallback?: ErrorCallback) {
|
this.#sendReceiverMessage({ type: "STOP", sessionId: this.sessionId })
|
||||||
|
|
||||||
this.#sendReceiverMessage(
|
|
||||||
{ type: "STOP"
|
|
||||||
, sessionId: this.sessionId })
|
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,48 +2,44 @@
|
|||||||
|
|
||||||
import Session from "./Session";
|
import Session from "./Session";
|
||||||
|
|
||||||
import { AutoJoinPolicy
|
import {
|
||||||
, Capability
|
AutoJoinPolicy,
|
||||||
, DefaultActionPolicy
|
Capability,
|
||||||
, ReceiverType
|
DefaultActionPolicy,
|
||||||
, VolumeControlType } from "./enums";
|
ReceiverType,
|
||||||
|
VolumeControlType
|
||||||
|
} from "./enums";
|
||||||
|
|
||||||
export class ApiConfig {
|
export class ApiConfig {
|
||||||
constructor(
|
constructor(
|
||||||
public sessionRequest: SessionRequest
|
public sessionRequest: SessionRequest,
|
||||||
, public sessionListener: (session: Session) => void
|
public sessionListener: (session: Session) => void,
|
||||||
, public receiverListener: (availability: string) => void
|
public receiverListener: (availability: string) => void,
|
||||||
|
|
||||||
, public autoJoinPolicy: string
|
public autoJoinPolicy: string = AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED,
|
||||||
= AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED
|
public defaultActionPolicy: string = DefaultActionPolicy.CREATE_SESSION
|
||||||
, public defaultActionPolicy: string
|
) {}
|
||||||
= DefaultActionPolicy.CREATE_SESSION) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class CredentialsData {
|
export class CredentialsData {
|
||||||
constructor(
|
constructor(public credentials: string, public credentialsData: string) {}
|
||||||
public credentials: string
|
|
||||||
, public credentialsData: string) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class DialRequest {
|
export class DialRequest {
|
||||||
constructor(
|
constructor(
|
||||||
public appName: string
|
public appName: string,
|
||||||
, public launchParameter: Nullable<string> = null) {}
|
public launchParameter: Nullable<string> = null
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Error {
|
export class Error {
|
||||||
constructor(
|
constructor(
|
||||||
public code: string
|
public code: string,
|
||||||
, public description: Nullable<string> = null
|
public description: Nullable<string> = null,
|
||||||
, public details: any = null) {}
|
public details: any = null
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Image {
|
export class Image {
|
||||||
width: Nullable<number> = null;
|
width: Nullable<number> = null;
|
||||||
height: Nullable<number> = null;
|
height: Nullable<number> = null;
|
||||||
@@ -51,29 +47,25 @@ export class Image {
|
|||||||
constructor(public url: string) {}
|
constructor(public url: string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Receiver {
|
export class Receiver {
|
||||||
displayStatus: Nullable<ReceiverDisplayStatus> = null;
|
displayStatus: Nullable<ReceiverDisplayStatus> = null;
|
||||||
isActiveInput: Nullable<boolean> = null;
|
isActiveInput: Nullable<boolean> = null;
|
||||||
receiverType = ReceiverType.CAST;
|
receiverType = ReceiverType.CAST;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public label: string
|
public label: string,
|
||||||
, public friendlyName: string
|
public friendlyName: string,
|
||||||
, public capabilities: Capability[] = []
|
public capabilities: Capability[] = [],
|
||||||
, public volume: Nullable<Volume> = null) {}
|
public volume: Nullable<Volume> = null
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class ReceiverDisplayStatus {
|
export class ReceiverDisplayStatus {
|
||||||
showStop: Nullable<boolean> = null;
|
showStop: Nullable<boolean> = null;
|
||||||
|
|
||||||
constructor(
|
constructor(public statusText: string, public appImages: Image[]) {}
|
||||||
public statusText: string
|
|
||||||
, public appImages: Image[]) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class SenderApplication {
|
export class SenderApplication {
|
||||||
packageId: Nullable<string> = null;
|
packageId: Nullable<string> = null;
|
||||||
url: Nullable<string> = null;
|
url: Nullable<string> = null;
|
||||||
@@ -81,20 +73,18 @@ export class SenderApplication {
|
|||||||
constructor(public platform: string) {}
|
constructor(public platform: string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class SessionRequest {
|
export class SessionRequest {
|
||||||
language: Nullable<string> = null;
|
language: Nullable<string> = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public appId: string
|
public appId: string,
|
||||||
, public capabilities = [ Capability.VIDEO_OUT
|
public capabilities = [Capability.VIDEO_OUT, Capability.AUDIO_OUT],
|
||||||
, Capability.AUDIO_OUT ]
|
public requestSessionTimeout = new Timeout().requestSession,
|
||||||
, public requestSessionTimeout = (new Timeout()).requestSession
|
public androidReceiverCompatible = false,
|
||||||
, public androidReceiverCompatible = false
|
public credentialsData: Nullable<CredentialsData> = null
|
||||||
, public credentialsData: Nullable<CredentialsData> = null) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Timeout {
|
export class Timeout {
|
||||||
leaveSession = 3000;
|
leaveSession = 3000;
|
||||||
requestSession = 60000;
|
requestSession = 60000;
|
||||||
@@ -103,12 +93,12 @@ export class Timeout {
|
|||||||
stopSession = 3000;
|
stopSession = 3000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Volume {
|
export class Volume {
|
||||||
controlType?: VolumeControlType;
|
controlType?: VolumeControlType;
|
||||||
stepInterval?: number;
|
stepInterval?: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public level: Nullable<number> = null
|
public level: Nullable<number> = null,
|
||||||
, public muted: Nullable<boolean> = null) {}
|
public muted: Nullable<boolean> = null
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,75 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
export enum AutoJoinPolicy {
|
export enum AutoJoinPolicy {
|
||||||
TAB_AND_ORIGIN_SCOPED = "tab_and_origin_scoped"
|
TAB_AND_ORIGIN_SCOPED = "tab_and_origin_scoped",
|
||||||
, ORIGIN_SCOPED = "origin_scoped"
|
ORIGIN_SCOPED = "origin_scoped",
|
||||||
, PAGE_SCOPED = "page_scoped"
|
PAGE_SCOPED = "page_scoped",
|
||||||
, CUSTOM_CONTROLLER_SCOPED = "custom_controller_scoped"
|
CUSTOM_CONTROLLER_SCOPED = "custom_controller_scoped"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Capability {
|
export enum Capability {
|
||||||
VIDEO_OUT = "video_out"
|
VIDEO_OUT = "video_out",
|
||||||
, AUDIO_OUT = "audio_out"
|
AUDIO_OUT = "audio_out",
|
||||||
, VIDEO_IN = "video_in"
|
VIDEO_IN = "video_in",
|
||||||
, AUDIO_IN = "audio_in"
|
AUDIO_IN = "audio_in",
|
||||||
, MULTIZONE_GROUP = "multizone_group"
|
MULTIZONE_GROUP = "multizone_group"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DefaultActionPolicy {
|
export enum DefaultActionPolicy {
|
||||||
CREATE_SESSION = "create_session"
|
CREATE_SESSION = "create_session",
|
||||||
, CAST_THIS_TAB = "cast_this_tab"
|
CAST_THIS_TAB = "cast_this_tab"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DialAppState {
|
export enum DialAppState {
|
||||||
RUNNING = "running"
|
RUNNING = "running",
|
||||||
, STOPPED = "stopped"
|
STOPPED = "stopped",
|
||||||
, ERROR = "error"
|
ERROR = "error"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ErrorCode {
|
export enum ErrorCode {
|
||||||
CANCEL = "cancel"
|
CANCEL = "cancel",
|
||||||
, TIMEOUT = "timeout"
|
TIMEOUT = "timeout",
|
||||||
, API_NOT_INITIALIZED = "api_not_initialized"
|
API_NOT_INITIALIZED = "api_not_initialized",
|
||||||
, INVALID_PARAMETER = "invalid_parameter"
|
INVALID_PARAMETER = "invalid_parameter",
|
||||||
, EXTENSION_NOT_COMPATIBLE = "extension_not_compatible"
|
EXTENSION_NOT_COMPATIBLE = "extension_not_compatible",
|
||||||
, EXTENSION_MISSING = "extension_missing"
|
EXTENSION_MISSING = "extension_missing",
|
||||||
, RECEIVER_UNAVAILABLE = "receiver_unavailable"
|
RECEIVER_UNAVAILABLE = "receiver_unavailable",
|
||||||
, SESSION_ERROR = "session_error"
|
SESSION_ERROR = "session_error",
|
||||||
, CHANNEL_ERROR = "channel_error"
|
CHANNEL_ERROR = "channel_error",
|
||||||
, LOAD_MEDIA_FAILED = "load_media_failed"
|
LOAD_MEDIA_FAILED = "load_media_failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReceiverAction {
|
export enum ReceiverAction {
|
||||||
CAST = "cast"
|
CAST = "cast",
|
||||||
, STOP = "stop"
|
STOP = "stop"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReceiverAvailability {
|
export enum ReceiverAvailability {
|
||||||
AVAILABLE = "available"
|
AVAILABLE = "available",
|
||||||
, UNAVAILABLE = "unavailable"
|
UNAVAILABLE = "unavailable"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReceiverType {
|
export enum ReceiverType {
|
||||||
CAST = "cast"
|
CAST = "cast",
|
||||||
, DIAL = "dial"
|
DIAL = "dial",
|
||||||
, HANGOUT = "hangout"
|
HANGOUT = "hangout",
|
||||||
, CUSTOM = "custom"
|
CUSTOM = "custom"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SenderPlatform {
|
export enum SenderPlatform {
|
||||||
CHROME = "chrome"
|
CHROME = "chrome",
|
||||||
, IOS = "ios"
|
IOS = "ios",
|
||||||
, ANDROID = "android"
|
ANDROID = "android"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SessionStatus {
|
export enum SessionStatus {
|
||||||
CONNECTED = "connected"
|
CONNECTED = "connected",
|
||||||
, DISCONNECTED = "disconnected"
|
DISCONNECTED = "disconnected",
|
||||||
, STOPPED = "stopped"
|
STOPPED = "stopped"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum VolumeControlType {
|
export enum VolumeControlType {
|
||||||
ATTENUATION = "attenuation"
|
ATTENUATION = "attenuation",
|
||||||
, FIXED = "fixed"
|
FIXED = "fixed",
|
||||||
, MASTER = "master"
|
MASTER = "master"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,29 +7,47 @@ import { ErrorCallback, SuccessCallback } from "../types";
|
|||||||
|
|
||||||
import { onMessage, sendMessageResponse } from "../eventMessageChannel";
|
import { onMessage, sendMessageResponse } from "../eventMessageChannel";
|
||||||
|
|
||||||
import { AutoJoinPolicy, Capability, DefaultActionPolicy, DialAppState
|
import {
|
||||||
, ErrorCode, ReceiverAction, ReceiverAvailability, ReceiverType
|
AutoJoinPolicy,
|
||||||
, SenderPlatform, SessionStatus, VolumeControlType } from "./enums";
|
Capability,
|
||||||
|
DefaultActionPolicy,
|
||||||
|
DialAppState,
|
||||||
|
ErrorCode,
|
||||||
|
ReceiverAction,
|
||||||
|
ReceiverAvailability,
|
||||||
|
ReceiverType,
|
||||||
|
SenderPlatform,
|
||||||
|
SessionStatus,
|
||||||
|
VolumeControlType
|
||||||
|
} from "./enums";
|
||||||
|
|
||||||
import { ApiConfig, CredentialsData, DialRequest, Error as Error_, Image
|
import {
|
||||||
, Receiver, ReceiverDisplayStatus, SenderApplication, SessionRequest
|
ApiConfig,
|
||||||
, Timeout, Volume } from "./dataClasses";
|
CredentialsData,
|
||||||
|
DialRequest,
|
||||||
|
Error as Error_,
|
||||||
|
Image,
|
||||||
|
Receiver,
|
||||||
|
ReceiverDisplayStatus,
|
||||||
|
SenderApplication,
|
||||||
|
SessionRequest,
|
||||||
|
Timeout,
|
||||||
|
Volume
|
||||||
|
} from "./dataClasses";
|
||||||
|
|
||||||
import Session from "./Session";
|
import Session from "./Session";
|
||||||
|
|
||||||
|
|
||||||
type ReceiverActionListener = (
|
type ReceiverActionListener = (
|
||||||
receiver: Receiver
|
receiver: Receiver,
|
||||||
, receiverAction: string) => void;
|
receiverAction: string
|
||||||
|
) => void;
|
||||||
|
|
||||||
type RequestSessionSuccessCallback = (session: Session) => void;
|
type RequestSessionSuccessCallback = (session: Session) => void;
|
||||||
|
|
||||||
|
|
||||||
let apiConfig: Nullable<ApiConfig>;
|
let apiConfig: Nullable<ApiConfig>;
|
||||||
let sessionRequest: Nullable<SessionRequest>;
|
let sessionRequest: Nullable<SessionRequest>;
|
||||||
|
|
||||||
let requestSessionSuccessCallback: Nullable<
|
let requestSessionSuccessCallback: Nullable<RequestSessionSuccessCallback>;
|
||||||
RequestSessionSuccessCallback>;
|
|
||||||
let requestSessionErrorCallback: Nullable<ErrorCallback>;
|
let requestSessionErrorCallback: Nullable<ErrorCallback>;
|
||||||
|
|
||||||
const receiverActionListeners = new Set<ReceiverActionListener>();
|
const receiverActionListeners = new Set<ReceiverActionListener>();
|
||||||
@@ -37,16 +55,36 @@ const receiverActionListeners = new Set<ReceiverActionListener>();
|
|||||||
const receiverDevices = new Map<string, ReceiverDevice>();
|
const receiverDevices = new Map<string, ReceiverDevice>();
|
||||||
const sessions = new Map<string, Session>();
|
const sessions = new Map<string, Session>();
|
||||||
|
|
||||||
|
export {
|
||||||
|
AutoJoinPolicy,
|
||||||
|
Capability,
|
||||||
|
DefaultActionPolicy,
|
||||||
|
DialAppState,
|
||||||
|
ErrorCode,
|
||||||
|
ReceiverAction,
|
||||||
|
ReceiverAvailability,
|
||||||
|
ReceiverType,
|
||||||
|
SenderPlatform,
|
||||||
|
SessionStatus,
|
||||||
|
VolumeControlType
|
||||||
|
};
|
||||||
|
|
||||||
export { AutoJoinPolicy, Capability, DefaultActionPolicy, DialAppState
|
export {
|
||||||
, ErrorCode, ReceiverAction, ReceiverAvailability, ReceiverType
|
ApiConfig,
|
||||||
, SenderPlatform, SessionStatus, VolumeControlType };
|
CredentialsData,
|
||||||
|
DialRequest,
|
||||||
|
Error_ as Error,
|
||||||
|
Image,
|
||||||
|
Receiver,
|
||||||
|
ReceiverDisplayStatus,
|
||||||
|
SenderApplication,
|
||||||
|
SessionRequest,
|
||||||
|
Timeout,
|
||||||
|
Volume,
|
||||||
|
Session
|
||||||
|
};
|
||||||
|
|
||||||
export { ApiConfig, CredentialsData, DialRequest, Error_ as Error, Image
|
export const VERSION = [1, 2];
|
||||||
, Receiver, ReceiverDisplayStatus, SenderApplication, SessionRequest
|
|
||||||
, Timeout, Volume, Session };
|
|
||||||
|
|
||||||
export const VERSION = [ 1, 2 ];
|
|
||||||
export let isAvailable = false;
|
export let isAvailable = false;
|
||||||
|
|
||||||
export const timeout = new Timeout();
|
export const timeout = new Timeout();
|
||||||
@@ -54,31 +92,33 @@ export const timeout = new Timeout();
|
|||||||
// chrome.cast.media namespace
|
// chrome.cast.media namespace
|
||||||
export * as media from "./media";
|
export * as media from "./media";
|
||||||
|
|
||||||
|
function sendSessionRequest(
|
||||||
function sendSessionRequest(sessionRequest: SessionRequest
|
sessionRequest: SessionRequest,
|
||||||
, receiverDevice: ReceiverDevice) {
|
receiverDevice: ReceiverDevice
|
||||||
|
) {
|
||||||
for (const listener of receiverActionListeners) {
|
for (const listener of receiverActionListeners) {
|
||||||
const receiver = new Receiver(
|
const receiver = new Receiver(
|
||||||
receiverDevice.id
|
receiverDevice.id,
|
||||||
, receiverDevice.friendlyName);
|
receiverDevice.friendlyName
|
||||||
|
);
|
||||||
|
|
||||||
listener(receiver, ReceiverAction.CAST);
|
listener(receiver, ReceiverAction.CAST);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessageResponse({
|
sendMessageResponse({
|
||||||
subject: "bridge:createCastSession"
|
subject: "bridge:createCastSession",
|
||||||
, data: {
|
data: {
|
||||||
appId: sessionRequest.appId
|
appId: sessionRequest.appId,
|
||||||
, receiverDevice: receiverDevice
|
receiverDevice: receiverDevice
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initialize(newApiConfig: ApiConfig
|
export function initialize(
|
||||||
, successCallback?: SuccessCallback
|
newApiConfig: ApiConfig,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
logger.info("cast.initialize");
|
logger.info("cast.initialize");
|
||||||
|
|
||||||
// Already initialized
|
// Already initialized
|
||||||
@@ -90,22 +130,25 @@ export function initialize(newApiConfig: ApiConfig
|
|||||||
apiConfig = newApiConfig;
|
apiConfig = newApiConfig;
|
||||||
|
|
||||||
sendMessageResponse({
|
sendMessageResponse({
|
||||||
subject: "main:shimReady"
|
subject: "main:shimReady",
|
||||||
, data: { appId: apiConfig.sessionRequest.appId }
|
data: { appId: apiConfig.sessionRequest.appId }
|
||||||
});
|
});
|
||||||
|
|
||||||
successCallback?.();
|
successCallback?.();
|
||||||
|
|
||||||
apiConfig.receiverListener(receiverDevices.size
|
apiConfig.receiverListener(
|
||||||
? ReceiverAvailability.AVAILABLE
|
receiverDevices.size
|
||||||
: ReceiverAvailability.UNAVAILABLE);
|
? ReceiverAvailability.AVAILABLE
|
||||||
|
: ReceiverAvailability.UNAVAILABLE
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestSession(successCallback: RequestSessionSuccessCallback
|
export function requestSession(
|
||||||
, errorCallback: ErrorCallback
|
successCallback: RequestSessionSuccessCallback,
|
||||||
, newSessionRequest?: SessionRequest
|
errorCallback: ErrorCallback,
|
||||||
, receiverDevice?: ReceiverDevice) {
|
newSessionRequest?: SessionRequest,
|
||||||
|
receiverDevice?: ReceiverDevice
|
||||||
|
) {
|
||||||
logger.info("cast.requestSession");
|
logger.info("cast.requestSession");
|
||||||
|
|
||||||
// Not yet initialized
|
// Not yet initialized
|
||||||
@@ -116,9 +159,12 @@ export function requestSession(successCallback: RequestSessionSuccessCallback
|
|||||||
|
|
||||||
// Already requesting session
|
// Already requesting session
|
||||||
if (sessionRequest) {
|
if (sessionRequest) {
|
||||||
errorCallback?.(new Error_(
|
errorCallback?.(
|
||||||
ErrorCode.INVALID_PARAMETER
|
new Error_(
|
||||||
, "Session request already in progress."));
|
ErrorCode.INVALID_PARAMETER,
|
||||||
|
"Session request already in progress."
|
||||||
|
)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,9 +203,11 @@ export function requestSessionById(_sessionId: string): void {
|
|||||||
logger.info("STUB :: cast.requestSessionById");
|
logger.info("STUB :: cast.requestSessionById");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setCustomReceivers(_receivers: Receiver[]
|
export function setCustomReceivers(
|
||||||
, _successCallback?: SuccessCallback
|
_receivers: Receiver[],
|
||||||
, _errorCallback?: ErrorCallback): void {
|
_successCallback?: SuccessCallback,
|
||||||
|
_errorCallback?: ErrorCallback
|
||||||
|
): void {
|
||||||
logger.info("STUB :: cast.setCustomReceivers");
|
logger.info("STUB :: cast.setCustomReceivers");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +238,6 @@ export function precache(_data: string) {
|
|||||||
logger.info("STUB :: cast.precache");
|
logger.info("STUB :: cast.precache");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onMessage(message => {
|
onMessage(message => {
|
||||||
switch (message.subject) {
|
switch (message.subject) {
|
||||||
case "shim:initialized": {
|
case "shim:initialized": {
|
||||||
@@ -212,18 +259,19 @@ onMessage(message => {
|
|||||||
|
|
||||||
// TODO: Implement persistent per-origin receiver IDs
|
// TODO: Implement persistent per-origin receiver IDs
|
||||||
const receiver = new Receiver(
|
const receiver = new Receiver(
|
||||||
status.receiverFriendlyName // label
|
status.receiverFriendlyName, // label
|
||||||
, status.receiverFriendlyName // friendlyName
|
status.receiverFriendlyName, // friendlyName
|
||||||
, [ Capability.VIDEO_OUT
|
[Capability.VIDEO_OUT, Capability.AUDIO_OUT], // capabilities
|
||||||
, Capability.AUDIO_OUT ] // capabilities
|
status.volume // volume
|
||||||
, status.volume); // volume
|
);
|
||||||
|
|
||||||
const session = new Session(
|
const session = new Session(
|
||||||
status.sessionId // sessionId
|
status.sessionId, // sessionId
|
||||||
, status.appId // appId
|
status.appId, // appId
|
||||||
, status.displayName // displayName
|
status.displayName, // displayName
|
||||||
, status.appImages // appImages
|
status.appImages, // appImages
|
||||||
, receiver); // receiver
|
receiver // receiver
|
||||||
|
);
|
||||||
|
|
||||||
session.senderApps = status.senderApps;
|
session.senderApps = status.senderApps;
|
||||||
session.transportId = status.transportId;
|
session.transportId = status.transportId;
|
||||||
@@ -293,7 +341,7 @@ onMessage(message => {
|
|||||||
|
|
||||||
const callbacks = session._sendMessageCallbacks.get(messageId);
|
const callbacks = session._sendMessageCallbacks.get(messageId);
|
||||||
if (callbacks) {
|
if (callbacks) {
|
||||||
const [ successCallback, errorCallback ] = callbacks;
|
const [successCallback, errorCallback] = callbacks;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
errorCallback?.(new Error_(error));
|
errorCallback?.(new Error_(error));
|
||||||
@@ -316,8 +364,7 @@ onMessage(message => {
|
|||||||
|
|
||||||
if (apiConfig) {
|
if (apiConfig) {
|
||||||
// Notify listeners of new cast destination
|
// Notify listeners of new cast destination
|
||||||
apiConfig.receiverListener(
|
apiConfig.receiverListener(ReceiverAvailability.AVAILABLE);
|
||||||
ReceiverAvailability.AVAILABLE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -331,7 +378,8 @@ onMessage(message => {
|
|||||||
if (receiverDevices.size === 0) {
|
if (receiverDevices.size === 0) {
|
||||||
if (apiConfig) {
|
if (apiConfig) {
|
||||||
apiConfig.receiverListener(
|
apiConfig.receiverListener(
|
||||||
ReceiverAvailability.UNAVAILABLE);
|
ReceiverAvailability.UNAVAILABLE
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,8 +407,9 @@ onMessage(message => {
|
|||||||
|
|
||||||
for (const listener of receiverActionListeners) {
|
for (const listener of receiverActionListeners) {
|
||||||
const castReceiver = new Receiver(
|
const castReceiver = new Receiver(
|
||||||
receiver.id
|
receiver.id,
|
||||||
, receiver.friendlyName);
|
receiver.friendlyName
|
||||||
|
);
|
||||||
|
|
||||||
listener(castReceiver, ReceiverAction.STOP);
|
listener(castReceiver, ReceiverAction.STOP);
|
||||||
}
|
}
|
||||||
@@ -376,12 +425,10 @@ onMessage(message => {
|
|||||||
if (sessionRequest) {
|
if (sessionRequest) {
|
||||||
sessionRequest = null;
|
sessionRequest = null;
|
||||||
|
|
||||||
requestSessionErrorCallback?.(
|
requestSessionErrorCallback?.(new Error_(ErrorCode.CANCEL));
|
||||||
new Error_(ErrorCode.CANCEL));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,22 +5,34 @@ import { v1 as uuid } from "uuid";
|
|||||||
import logger from "../../../lib/logger";
|
import logger from "../../../lib/logger";
|
||||||
|
|
||||||
import { Volume, Error as _Error } from "../dataClasses";
|
import { Volume, Error as _Error } from "../dataClasses";
|
||||||
import { BreakStatus, EditTracksInfoRequest, GetStatusRequest, LiveSeekableRange
|
import {
|
||||||
, MediaInfo, PauseRequest, PlayRequest, QueueData, QueueJumpRequest
|
BreakStatus,
|
||||||
, QueueInsertItemsRequest, QueueItem, QueueSetPropertiesRequest
|
EditTracksInfoRequest,
|
||||||
, QueueRemoveItemsRequest, QueueReorderItemsRequest
|
GetStatusRequest,
|
||||||
, QueueUpdateItemsRequest, SeekRequest, StopRequest, VideoInformation
|
LiveSeekableRange,
|
||||||
, VolumeRequest } from "./dataClasses";
|
MediaInfo,
|
||||||
|
PauseRequest,
|
||||||
|
PlayRequest,
|
||||||
|
QueueData,
|
||||||
|
QueueJumpRequest,
|
||||||
|
QueueInsertItemsRequest,
|
||||||
|
QueueItem,
|
||||||
|
QueueSetPropertiesRequest,
|
||||||
|
QueueRemoveItemsRequest,
|
||||||
|
QueueReorderItemsRequest,
|
||||||
|
QueueUpdateItemsRequest,
|
||||||
|
SeekRequest,
|
||||||
|
StopRequest,
|
||||||
|
VideoInformation,
|
||||||
|
VolumeRequest
|
||||||
|
} from "./dataClasses";
|
||||||
|
|
||||||
import { PlayerState, RepeatMode } from "./enums";
|
import { PlayerState, RepeatMode } from "./enums";
|
||||||
import { ErrorCode } from "../enums";
|
import { ErrorCode } from "../enums";
|
||||||
|
|
||||||
import { ErrorCallback
|
import { ErrorCallback, SuccessCallback, UpdateListener } from "../../types";
|
||||||
, SuccessCallback
|
|
||||||
, UpdateListener } from "../../types";
|
|
||||||
import { SenderMediaMessage } from "../types";
|
import { SenderMediaMessage } from "../types";
|
||||||
|
|
||||||
|
|
||||||
export default class Media {
|
export default class Media {
|
||||||
#id = uuid();
|
#id = uuid();
|
||||||
|
|
||||||
@@ -49,12 +61,13 @@ export default class Media {
|
|||||||
preloadedItemId: Nullable<number> = null;
|
preloadedItemId: Nullable<number> = null;
|
||||||
queueData?: QueueData;
|
queueData?: QueueData;
|
||||||
|
|
||||||
|
constructor(
|
||||||
constructor(public sessionId: string
|
public sessionId: string,
|
||||||
, public mediaSessionId: number
|
public mediaSessionId: number,
|
||||||
, public _sendMediaMessage: (message: DistributiveOmit<
|
public _sendMediaMessage: (
|
||||||
SenderMediaMessage, "requestId">) => Promise<void>) {
|
message: DistributiveOmit<SenderMediaMessage, "requestId">
|
||||||
}
|
) => Promise<void>
|
||||||
|
) {}
|
||||||
|
|
||||||
addUpdateListener(listener: UpdateListener) {
|
addUpdateListener(listener: UpdateListener) {
|
||||||
this._updateListeners.add(listener);
|
this._updateListeners.add(listener);
|
||||||
@@ -63,14 +76,16 @@ export default class Media {
|
|||||||
this._updateListeners.delete(listener);
|
this._updateListeners.delete(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
editTracksInfo(editTracksInfoRequest: EditTracksInfoRequest
|
editTracksInfo(
|
||||||
, successCallback?: SuccessCallback
|
editTracksInfoRequest: EditTracksInfoRequest,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this._sendMediaMessage(
|
) {
|
||||||
{ ...editTracksInfoRequest
|
this._sendMediaMessage({
|
||||||
, type: "EDIT_TRACKS_INFO"
|
...editTracksInfoRequest,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
type: "EDIT_TRACKS_INFO",
|
||||||
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
@@ -92,14 +107,16 @@ export default class Media {
|
|||||||
*/
|
*/
|
||||||
getEstimatedTime(): number {
|
getEstimatedTime(): number {
|
||||||
if (this.playerState === PlayerState.PLAYING && this._lastUpdateTime) {
|
if (this.playerState === PlayerState.PLAYING && this._lastUpdateTime) {
|
||||||
let estimatedTime = this.currentTime +
|
let estimatedTime =
|
||||||
(((Date.now() - this._lastUpdateTime) / 1000));
|
this.currentTime + (Date.now() - this._lastUpdateTime) / 1000;
|
||||||
|
|
||||||
// Enforce valid range
|
// Enforce valid range
|
||||||
if (estimatedTime < 0) {
|
if (estimatedTime < 0) {
|
||||||
estimatedTime = 0;
|
estimatedTime = 0;
|
||||||
} else if (this.media?.duration &&
|
} else if (
|
||||||
estimatedTime > this.media.duration) {
|
this.media?.duration &&
|
||||||
|
estimatedTime > 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
|
* Request media status from the receiver application. This
|
||||||
* will also trigger any added media update listeners.
|
* will also trigger any added media update listeners.
|
||||||
*/
|
*/
|
||||||
getStatus(getStatusRequest = new GetStatusRequest()
|
getStatus(
|
||||||
, successCallback?: SuccessCallback
|
getStatusRequest = new GetStatusRequest(),
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this._sendMediaMessage(
|
) {
|
||||||
{ ...getStatusRequest
|
this._sendMediaMessage({
|
||||||
, type: "MEDIA_GET_STATUS"
|
...getStatusRequest,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
type: "MEDIA_GET_STATUS",
|
||||||
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
pause(pauseRequest = new PauseRequest()
|
pause(
|
||||||
, successCallback?: SuccessCallback
|
pauseRequest = new PauseRequest(),
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this._sendMediaMessage(
|
) {
|
||||||
{ ...pauseRequest
|
this._sendMediaMessage({
|
||||||
, type: "PAUSE"
|
...pauseRequest,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
type: "PAUSE",
|
||||||
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
play(playRequest = new PlayRequest()
|
play(
|
||||||
, successCallback?: SuccessCallback
|
playRequest = new PlayRequest(),
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this._sendMediaMessage(
|
) {
|
||||||
{ ...playRequest
|
this._sendMediaMessage({
|
||||||
, type: "PLAY"
|
...playRequest,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
type: "PLAY",
|
||||||
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueAppendItem(item: QueueItem
|
queueAppendItem(
|
||||||
, successCallback?: SuccessCallback
|
item: QueueItem,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this._sendMediaMessage(
|
) {
|
||||||
{ ...new QueueInsertItemsRequest([ item ])
|
this._sendMediaMessage({
|
||||||
, type: "QUEUE_INSERT"
|
...new QueueInsertItemsRequest([item]),
|
||||||
, sessionId: this.sessionId
|
type: "QUEUE_INSERT",
|
||||||
, mediaSessionId: this.mediaSessionId })
|
sessionId: this.sessionId,
|
||||||
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueInsertItems(queueInsertItemsRequest: QueueInsertItemsRequest
|
queueInsertItems(
|
||||||
, successCallback?: SuccessCallback
|
queueInsertItemsRequest: QueueInsertItemsRequest,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this._sendMediaMessage(
|
) {
|
||||||
{ ...queueInsertItemsRequest
|
this._sendMediaMessage({
|
||||||
, type: "QUEUE_INSERT"
|
...queueInsertItemsRequest,
|
||||||
, sessionId: this.sessionId
|
type: "QUEUE_INSERT",
|
||||||
, mediaSessionId: this.mediaSessionId })
|
sessionId: this.sessionId,
|
||||||
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queueJumpToItem(itemId: number
|
queueJumpToItem(
|
||||||
, successCallback?: SuccessCallback
|
itemId: number,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
if (this.items?.find(item => item.itemId === itemId)) {
|
if (this.items?.find(item => item.itemId === itemId)) {
|
||||||
const jumpRequest = new QueueJumpRequest();
|
const jumpRequest = new QueueJumpRequest();
|
||||||
jumpRequest.currentItemId = itemId;
|
jumpRequest.currentItemId = itemId;
|
||||||
|
|
||||||
this._sendMediaMessage(
|
this._sendMediaMessage({
|
||||||
{ ...jumpRequest
|
...jumpRequest,
|
||||||
, type: "QUEUE_UPDATE"
|
type: "QUEUE_UPDATE",
|
||||||
, sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueMoveItemToNewIndex(itemId: number
|
queueMoveItemToNewIndex(
|
||||||
, newIndex: number
|
itemId: number,
|
||||||
, successCallback?: SuccessCallback
|
newIndex: number,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
// Return early if not in queue
|
// Return early if not in queue
|
||||||
if (!this.items) {
|
if (!this.items) {
|
||||||
return;
|
return;
|
||||||
@@ -213,170 +242,194 @@ export default class Media {
|
|||||||
errorCallback(new _Error(ErrorCode.INVALID_PARAMETER));
|
errorCallback(new _Error(ErrorCode.INVALID_PARAMETER));
|
||||||
}
|
}
|
||||||
} else if (newIndex == itemIndex) {
|
} else if (newIndex == itemIndex) {
|
||||||
if (successCallback) { successCallback(); }
|
if (successCallback) {
|
||||||
|
successCallback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (newIndex > itemIndex) {
|
if (newIndex > itemIndex) {
|
||||||
newIndex++;
|
newIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reorderItemsRequest =
|
const reorderItemsRequest = new QueueReorderItemsRequest([itemId]);
|
||||||
new QueueReorderItemsRequest([ itemId ]);
|
|
||||||
if (newIndex < this.items.length) {
|
if (newIndex < this.items.length) {
|
||||||
const existingItem = this.items[newIndex];
|
const existingItem = this.items[newIndex];
|
||||||
reorderItemsRequest.insertBefore = existingItem.itemId;
|
reorderItemsRequest.insertBefore = existingItem.itemId;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._sendMediaMessage(
|
this._sendMediaMessage({
|
||||||
{ ...reorderItemsRequest
|
...reorderItemsRequest,
|
||||||
, type: "QUEUE_REORDER"
|
type: "QUEUE_REORDER",
|
||||||
, sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueNext(successCallback?: SuccessCallback
|
queueNext(
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
const jumpRequest = new QueueJumpRequest();
|
const jumpRequest = new QueueJumpRequest();
|
||||||
jumpRequest.jump = 1;
|
jumpRequest.jump = 1;
|
||||||
|
|
||||||
this._sendMediaMessage(
|
this._sendMediaMessage({
|
||||||
{ ...jumpRequest
|
...jumpRequest,
|
||||||
, type: "QUEUE_UPDATE"
|
type: "QUEUE_UPDATE",
|
||||||
, sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queuePrev(successCallback?: SuccessCallback
|
queuePrev(
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
const jumpRequest = new QueueJumpRequest();
|
const jumpRequest = new QueueJumpRequest();
|
||||||
jumpRequest.jump = -1;
|
jumpRequest.jump = -1;
|
||||||
|
|
||||||
this._sendMediaMessage(
|
this._sendMediaMessage({
|
||||||
{ ...jumpRequest
|
...jumpRequest,
|
||||||
, type: "QUEUE_UPDATE"
|
type: "QUEUE_UPDATE",
|
||||||
, sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueRemoveItem(itemId: number
|
queueRemoveItem(
|
||||||
, successCallback?: SuccessCallback
|
itemId: number,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
const item = this.items?.find(item => item.itemId === itemId);
|
const item = this.items?.find(item => item.itemId === itemId);
|
||||||
if (item) {
|
if (item) {
|
||||||
this.queueRemoveItems(
|
this.queueRemoveItems(
|
||||||
new QueueRemoveItemsRequest([ itemId ])
|
new QueueRemoveItemsRequest([itemId]),
|
||||||
, successCallback, errorCallback);
|
successCallback,
|
||||||
|
errorCallback
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueRemoveItems(queueRemoveItemsRequest: QueueRemoveItemsRequest
|
queueRemoveItems(
|
||||||
, successCallback?: SuccessCallback
|
queueRemoveItemsRequest: QueueRemoveItemsRequest,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
|
this._sendMediaMessage({
|
||||||
|
...queueRemoveItemsRequest,
|
||||||
|
|
||||||
this._sendMediaMessage(
|
mediaSessionId: this.mediaSessionId,
|
||||||
{ ...queueRemoveItemsRequest
|
type: "QUEUE_REMOVE",
|
||||||
|
sessionId: this.sessionId
|
||||||
, mediaSessionId: this.mediaSessionId
|
})
|
||||||
, type: "QUEUE_REMOVE"
|
|
||||||
, sessionId: this.sessionId })
|
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueReorderItems(queueReorderItemsRequest: QueueReorderItemsRequest
|
queueReorderItems(
|
||||||
, successCallback?: SuccessCallback
|
queueReorderItemsRequest: QueueReorderItemsRequest,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
|
this._sendMediaMessage({
|
||||||
|
...queueReorderItemsRequest,
|
||||||
|
|
||||||
this._sendMediaMessage(
|
mediaSessionId: this.mediaSessionId,
|
||||||
{ ...queueReorderItemsRequest
|
type: "QUEUE_REORDER",
|
||||||
|
sessionId: this.sessionId
|
||||||
, mediaSessionId: this.mediaSessionId
|
})
|
||||||
, type: "QUEUE_REORDER"
|
|
||||||
, sessionId: this.sessionId })
|
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueSetRepeatMode(repeatMode: string
|
queueSetRepeatMode(
|
||||||
, successCallback?: SuccessCallback
|
repeatMode: string,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
const setPropertiesRequest = new QueueSetPropertiesRequest();
|
const setPropertiesRequest = new QueueSetPropertiesRequest();
|
||||||
setPropertiesRequest.repeatMode = repeatMode;
|
setPropertiesRequest.repeatMode = repeatMode;
|
||||||
|
|
||||||
this._sendMediaMessage(
|
this._sendMediaMessage({
|
||||||
{ ...setPropertiesRequest
|
...setPropertiesRequest,
|
||||||
, type: "QUEUE_UPDATE"
|
type: "QUEUE_UPDATE",
|
||||||
, sessionId: this.sessionId
|
sessionId: this.sessionId,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
queueUpdateItems(queueUpdateItemsRequest: QueueUpdateItemsRequest
|
queueUpdateItems(
|
||||||
, successCallback?: SuccessCallback
|
queueUpdateItemsRequest: QueueUpdateItemsRequest,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this._sendMediaMessage(
|
) {
|
||||||
{ ...queueUpdateItemsRequest
|
this._sendMediaMessage({
|
||||||
, type: "QUEUE_UPDATE"
|
...queueUpdateItemsRequest,
|
||||||
, sessionId: this.sessionId
|
type: "QUEUE_UPDATE",
|
||||||
, mediaSessionId: this.mediaSessionId })
|
sessionId: this.sessionId,
|
||||||
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
seek(seekRequest: SeekRequest
|
seek(
|
||||||
, successCallback?: SuccessCallback
|
seekRequest: SeekRequest,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this._sendMediaMessage(
|
) {
|
||||||
{ ...seekRequest
|
this._sendMediaMessage({
|
||||||
, type: "SEEK"
|
...seekRequest,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
type: "SEEK",
|
||||||
|
mediaSessionId: this.mediaSessionId
|
||||||
|
})
|
||||||
.then(successCallback)
|
.then(successCallback)
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
setVolume(volumeRequest: VolumeRequest
|
setVolume(
|
||||||
, successCallback?: SuccessCallback
|
volumeRequest: VolumeRequest,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
this._sendMediaMessage(
|
) {
|
||||||
{ ...volumeRequest
|
this._sendMediaMessage({
|
||||||
, type: "MEDIA_SET_VOLUME"
|
...volumeRequest,
|
||||||
, mediaSessionId: this.mediaSessionId })
|
type: "MEDIA_SET_VOLUME",
|
||||||
.then(successCallback)
|
mediaSessionId: this.mediaSessionId
|
||||||
.catch(errorCallback);
|
})
|
||||||
|
.then(successCallback)
|
||||||
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(stopRequest?: StopRequest
|
stop(
|
||||||
, successCallback?: SuccessCallback
|
stopRequest?: StopRequest,
|
||||||
, errorCallback?: ErrorCallback) {
|
successCallback?: SuccessCallback,
|
||||||
|
errorCallback?: ErrorCallback
|
||||||
|
) {
|
||||||
if (!stopRequest) {
|
if (!stopRequest) {
|
||||||
stopRequest = new StopRequest();
|
stopRequest = new StopRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._sendMediaMessage({
|
this._sendMediaMessage({
|
||||||
...stopRequest
|
...stopRequest,
|
||||||
, type: "STOP"
|
type: "STOP",
|
||||||
, mediaSessionId: this.mediaSessionId
|
mediaSessionId: this.mediaSessionId
|
||||||
}).then(() => {
|
})
|
||||||
if (successCallback) {
|
.then(() => {
|
||||||
successCallback();
|
if (successCallback) {
|
||||||
}
|
successCallback();
|
||||||
}).catch(errorCallback);
|
}
|
||||||
|
})
|
||||||
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsCommand(command: string): boolean {
|
supportsCommand(command: string): boolean {
|
||||||
|
|||||||
@@ -2,15 +2,18 @@
|
|||||||
|
|
||||||
import { Image, Volume } from "../dataClasses";
|
import { Image, Volume } from "../dataClasses";
|
||||||
|
|
||||||
import { ContainerType
|
import {
|
||||||
, HdrType
|
ContainerType,
|
||||||
, HlsSegmentFormat
|
HdrType,
|
||||||
, HlsVideoSegmentFormat
|
HlsSegmentFormat,
|
||||||
, MetadataType
|
HlsVideoSegmentFormat,
|
||||||
, RepeatMode
|
MetadataType,
|
||||||
, ResumeState, StreamType
|
RepeatMode,
|
||||||
, TrackType, UserAction } from "./enums";
|
ResumeState,
|
||||||
|
StreamType,
|
||||||
|
TrackType,
|
||||||
|
UserAction
|
||||||
|
} from "./enums";
|
||||||
|
|
||||||
export class AudiobookChapterMediaMetadata {
|
export class AudiobookChapterMediaMetadata {
|
||||||
bookTitle?: string;
|
bookTitle?: string;
|
||||||
@@ -35,9 +38,10 @@ export class Break {
|
|||||||
isWatched = false;
|
isWatched = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public id: string
|
public id: string,
|
||||||
, public breakClipIds: string[]
|
public breakClipIds: string[],
|
||||||
, public position: number) {}
|
public position: number
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BreakClip {
|
export class BreakClip {
|
||||||
@@ -71,17 +75,17 @@ export class ContainerMetadata {
|
|||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public containerType: ContainerType =
|
public containerType: ContainerType = ContainerType.GENERIC_CONTAINER
|
||||||
ContainerType.GENERIC_CONTAINER) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EditTracksInfoRequest {
|
export class EditTracksInfoRequest {
|
||||||
requestId = 0;
|
requestId = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public activeTrackIds: Nullable<number[]> = null
|
public activeTrackIds: Nullable<number[]> = null,
|
||||||
, public textTrackStyle: Nullable<string> = null) {
|
public textTrackStyle: Nullable<string> = null
|
||||||
}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GenericMediaMetadata {
|
export class GenericMediaMetadata {
|
||||||
@@ -100,10 +104,11 @@ export class GetStatusRequest {
|
|||||||
|
|
||||||
export class LiveSeekableRange {
|
export class LiveSeekableRange {
|
||||||
constructor(
|
constructor(
|
||||||
public start?: number
|
public start?: number,
|
||||||
, public end?: number
|
public end?: number,
|
||||||
, public isMovingWindow?: boolean
|
public isMovingWindow?: boolean,
|
||||||
, public isLiveDone?: boolean) {}
|
public isLiveDone?: boolean
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoadRequest {
|
export class LoadRequest {
|
||||||
@@ -123,13 +128,12 @@ export class LoadRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type Metadata =
|
export type Metadata =
|
||||||
GenericMediaMetadata
|
| GenericMediaMetadata
|
||||||
| MovieMediaMetadata
|
| MovieMediaMetadata
|
||||||
| MusicTrackMediaMetadata
|
| MusicTrackMediaMetadata
|
||||||
| PhotoMediaMetadata
|
| PhotoMediaMetadata
|
||||||
| TvShowMediaMetadata;
|
| TvShowMediaMetadata;
|
||||||
|
|
||||||
export class MediaInfo {
|
export class MediaInfo {
|
||||||
atvEntity?: string;
|
atvEntity?: string;
|
||||||
@@ -149,12 +153,9 @@ export class MediaInfo {
|
|||||||
userActionStates?: UserActionState[];
|
userActionStates?: UserActionState[];
|
||||||
vmapAdsRequest?: VastAdsRequest;
|
vmapAdsRequest?: VastAdsRequest;
|
||||||
|
|
||||||
constructor(
|
constructor(public contentId: string, public contentType: string) {}
|
||||||
public contentId: string
|
|
||||||
, public contentType: string) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class MediaMetadata {
|
export class MediaMetadata {
|
||||||
queueItemId?: number;
|
queueItemId?: number;
|
||||||
sectionDuration?: number;
|
sectionDuration?: number;
|
||||||
@@ -224,13 +225,14 @@ export class QueueData {
|
|||||||
shuffle = false;
|
shuffle = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public id?: string
|
public id?: string,
|
||||||
, public name?: string
|
public name?: string,
|
||||||
, public description?: string
|
public description?: string,
|
||||||
, public repeatMode?: RepeatMode
|
public repeatMode?: RepeatMode,
|
||||||
, public items?: QueueItem[]
|
public items?: QueueItem[],
|
||||||
, public startIndex?: number
|
public startIndex?: number,
|
||||||
, public startTime?: number) {}
|
public startTime?: number
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueueInsertItemsRequest {
|
export class QueueInsertItemsRequest {
|
||||||
@@ -240,8 +242,7 @@ export class QueueInsertItemsRequest {
|
|||||||
sessionId: Nullable<string> = null;
|
sessionId: Nullable<string> = null;
|
||||||
type = "QUEUE_INSERT";
|
type = "QUEUE_INSERT";
|
||||||
|
|
||||||
constructor(
|
constructor(public items: QueueItem[]) {}
|
||||||
public items: QueueItem[]) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueueItem {
|
export class QueueItem {
|
||||||
@@ -302,7 +303,6 @@ export class QueueUpdateItemsRequest {
|
|||||||
constructor(public items: QueueItem[]) {}
|
constructor(public items: QueueItem[]) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class SeekRequest {
|
export class SeekRequest {
|
||||||
currentTime: Nullable<number> = null;
|
currentTime: Nullable<number> = null;
|
||||||
customData: any = null;
|
customData: any = null;
|
||||||
@@ -336,9 +336,7 @@ export class Track {
|
|||||||
trackContentId: Nullable<string> = null;
|
trackContentId: Nullable<string> = null;
|
||||||
trackContentType: Nullable<string> = null;
|
trackContentType: Nullable<string> = null;
|
||||||
|
|
||||||
constructor(
|
constructor(public trackId: number, public type: TrackType) {}
|
||||||
public trackId: number
|
|
||||||
, public type: TrackType) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TvShowMediaMetadata {
|
export class TvShowMediaMetadata {
|
||||||
@@ -359,8 +357,7 @@ export class TvShowMediaMetadata {
|
|||||||
export class UserActionState {
|
export class UserActionState {
|
||||||
customData: any = null;
|
customData: any = null;
|
||||||
|
|
||||||
constructor(
|
constructor(public userAction: UserAction) {}
|
||||||
public userAction: UserAction) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VastAdsRequest {
|
export class VastAdsRequest {
|
||||||
@@ -370,14 +367,14 @@ export class VastAdsRequest {
|
|||||||
|
|
||||||
export class VideoInformation {
|
export class VideoInformation {
|
||||||
constructor(
|
constructor(
|
||||||
public width: number
|
public width: number,
|
||||||
, public height: number
|
public height: number,
|
||||||
, public hdrType: HdrType) {}
|
public hdrType: HdrType
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VolumeRequest {
|
export class VolumeRequest {
|
||||||
customData: any = null;
|
customData: any = null;
|
||||||
|
|
||||||
constructor(
|
constructor(public volume: Volume) {}
|
||||||
public volume: Volume) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +1,139 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
export enum ContainerType {
|
export enum ContainerType {
|
||||||
GENERIC_CONTAINER
|
GENERIC_CONTAINER,
|
||||||
, AUDIOBOOK_CONTAINER
|
AUDIOBOOK_CONTAINER
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum HdrType {
|
export enum HdrType {
|
||||||
SDR = "sdr"
|
SDR = "sdr",
|
||||||
, HDR = "hdr"
|
HDR = "hdr",
|
||||||
, DV = "dv"
|
DV = "dv"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum HlsSegmentFormat {
|
export enum HlsSegmentFormat {
|
||||||
AAC = "aac"
|
AAC = "aac",
|
||||||
, AC3 = "ac3"
|
AC3 = "ac3",
|
||||||
, MP3 = "mp3"
|
MP3 = "mp3",
|
||||||
, TS = "ts"
|
TS = "ts",
|
||||||
, TS_AAC = "ts_aac"
|
TS_AAC = "ts_aac",
|
||||||
, E_AC3 = "e_ac3"
|
E_AC3 = "e_ac3",
|
||||||
, FMP4 = "fmp4"
|
FMP4 = "fmp4"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum HlsVideoSegmentFormat {
|
export enum HlsVideoSegmentFormat {
|
||||||
MPEG2_TS = "mpeg2_ts"
|
MPEG2_TS = "mpeg2_ts",
|
||||||
, FMP4 = "fmp4"
|
FMP4 = "fmp4"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum IdleReason {
|
export enum IdleReason {
|
||||||
CANCELLED = "CANCELLED"
|
CANCELLED = "CANCELLED",
|
||||||
, INTERRUPTED = "INTERRUPTED"
|
INTERRUPTED = "INTERRUPTED",
|
||||||
, FINISHED = "FINISHED"
|
FINISHED = "FINISHED",
|
||||||
, ERROR = "ERROR"
|
ERROR = "ERROR"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MediaCommand {
|
export enum MediaCommand {
|
||||||
PAUSE = "pause"
|
PAUSE = "pause",
|
||||||
, SEEK = "seek"
|
SEEK = "seek",
|
||||||
, STREAM_VOLUME = "stream_volume"
|
STREAM_VOLUME = "stream_volume",
|
||||||
, STREAM_MUTE = "stream_mute"
|
STREAM_MUTE = "stream_mute"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MetadataType {
|
export enum MetadataType {
|
||||||
GENERIC
|
GENERIC,
|
||||||
, MOVIE
|
MOVIE,
|
||||||
, TV_SHOW
|
TV_SHOW,
|
||||||
, MUSIC_TRACK
|
MUSIC_TRACK,
|
||||||
, PHOTO
|
PHOTO,
|
||||||
, AUDIOBOOK_CHAPTER
|
AUDIOBOOK_CHAPTER
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PlayerState {
|
export enum PlayerState {
|
||||||
IDLE = "IDLE"
|
IDLE = "IDLE",
|
||||||
, PLAYING = "PLAYING"
|
PLAYING = "PLAYING",
|
||||||
, PAUSED = "PAUSED"
|
PAUSED = "PAUSED",
|
||||||
, BUFFERING = "BUFFERING"
|
BUFFERING = "BUFFERING"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueType {
|
export enum QueueType {
|
||||||
ALBUM = "ALBUM"
|
ALBUM = "ALBUM",
|
||||||
, PLAYLIST = "PLAYLIST"
|
PLAYLIST = "PLAYLIST",
|
||||||
, AUDIOBOOK = "AUDIOBOOK"
|
AUDIOBOOK = "AUDIOBOOK",
|
||||||
, RADIO_STATION = "RADIO_STATION"
|
RADIO_STATION = "RADIO_STATION",
|
||||||
, PODCAST_SERIES = "PODCAST_SERIES"
|
PODCAST_SERIES = "PODCAST_SERIES",
|
||||||
, TV_SERIES = "TV_SERIES"
|
TV_SERIES = "TV_SERIES",
|
||||||
, VIDEO_PLAYLIST = "VIDEO_PLAYLIST"
|
VIDEO_PLAYLIST = "VIDEO_PLAYLIST",
|
||||||
, LIVE_TV = "LIVETV"
|
LIVE_TV = "LIVETV",
|
||||||
, MOVIE = "MOVIE"
|
MOVIE = "MOVIE"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RepeatMode {
|
export enum RepeatMode {
|
||||||
OFF = "REPEAT_OFF"
|
OFF = "REPEAT_OFF",
|
||||||
, ALL = "REPEAT_ALL"
|
ALL = "REPEAT_ALL",
|
||||||
, SINGLE = "REPEAT_SINGLE"
|
SINGLE = "REPEAT_SINGLE",
|
||||||
, ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
|
ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ResumeState {
|
export enum ResumeState {
|
||||||
PLAYBACK_START = "PLAYBACK_START"
|
PLAYBACK_START = "PLAYBACK_START",
|
||||||
, PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
|
PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum StreamType {
|
export enum StreamType {
|
||||||
BUFFERED = "BUFFERED"
|
BUFFERED = "BUFFERED",
|
||||||
, LIVE = "LIVE"
|
LIVE = "LIVE",
|
||||||
, OTHER = "OTHER"
|
OTHER = "OTHER"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TextTrackEdgeType {
|
export enum TextTrackEdgeType {
|
||||||
NONE = "NONE"
|
NONE = "NONE",
|
||||||
, OUTLINE = "OUTLINE"
|
OUTLINE = "OUTLINE",
|
||||||
, DROP_SHADOW = "DROP_SHADOW"
|
DROP_SHADOW = "DROP_SHADOW",
|
||||||
, RAISED = "RAISED"
|
RAISED = "RAISED",
|
||||||
, DEPRESSED = "DEPRESSED"
|
DEPRESSED = "DEPRESSED"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TextTrackFontGenericFamily {
|
export enum TextTrackFontGenericFamily {
|
||||||
SANS_SERIF = "SANS_SERIF"
|
SANS_SERIF = "SANS_SERIF",
|
||||||
, MONOSPACED_SANS_SERIF = "MONOSPACED_SANS_SERIF"
|
MONOSPACED_SANS_SERIF = "MONOSPACED_SANS_SERIF",
|
||||||
, SERIF = "SERIF"
|
SERIF = "SERIF",
|
||||||
, MONOSPACED_SERIF = "MONOSPACED_SERIF"
|
MONOSPACED_SERIF = "MONOSPACED_SERIF",
|
||||||
, CASUAL = "CASUAL"
|
CASUAL = "CASUAL",
|
||||||
, CURSIVE = "CURSIVE"
|
CURSIVE = "CURSIVE",
|
||||||
, SMALL_CAPITALS = "SMALL_CAPITALS"
|
SMALL_CAPITALS = "SMALL_CAPITALS"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TextTrackFontStyle {
|
export enum TextTrackFontStyle {
|
||||||
NORMAL = "NORMAL"
|
NORMAL = "NORMAL",
|
||||||
, BOLD = "BOLD"
|
BOLD = "BOLD",
|
||||||
, BOLD_ITALIC = "BOLD_ITALIC"
|
BOLD_ITALIC = "BOLD_ITALIC",
|
||||||
, ITALIC = "ITALIC"
|
ITALIC = "ITALIC"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TextTrackType {
|
export enum TextTrackType {
|
||||||
SUBTITLES = "SUBTITLES"
|
SUBTITLES = "SUBTITLES",
|
||||||
, CAPTIONS = "CAPTIONS"
|
CAPTIONS = "CAPTIONS",
|
||||||
, DESCRIPTIONS = "DESCRIPTIONS"
|
DESCRIPTIONS = "DESCRIPTIONS",
|
||||||
, CHAPTERS = "CHAPTERS"
|
CHAPTERS = "CHAPTERS",
|
||||||
, METADATA = "METADATA"
|
METADATA = "METADATA"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TextTrackWindowType {
|
export enum TextTrackWindowType {
|
||||||
NONE = "NONE"
|
NONE = "NONE",
|
||||||
, NORMAL = "NORMAL"
|
NORMAL = "NORMAL",
|
||||||
, ROUNDED_CORNERS = "ROUNDED_CORNERS"
|
ROUNDED_CORNERS = "ROUNDED_CORNERS"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TrackType {
|
export enum TrackType {
|
||||||
TEXT = "TEXT"
|
TEXT = "TEXT",
|
||||||
, AUDIO = "AUDIO"
|
AUDIO = "AUDIO",
|
||||||
, VIDEO = "VIDEO"
|
VIDEO = "VIDEO"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserAction {
|
export enum UserAction {
|
||||||
LIKE = "LIKE"
|
LIKE = "LIKE",
|
||||||
, DISLIKE = "DISLIKE"
|
DISLIKE = "DISLIKE",
|
||||||
, FOLLOW = "FOLLOW"
|
FOLLOW = "FOLLOW",
|
||||||
, UNFOLLOW = "UNFOLLOW"
|
UNFOLLOW = "UNFOLLOW"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,21 @@
|
|||||||
|
|
||||||
import Media from "./Media";
|
import Media from "./Media";
|
||||||
|
|
||||||
|
|
||||||
export { Media };
|
export { Media };
|
||||||
|
|
||||||
export * from "./dataClasses";
|
export * from "./dataClasses";
|
||||||
export * from "./enums";
|
export * from "./enums";
|
||||||
|
|
||||||
export const timeout = {
|
export const timeout = {
|
||||||
editTracksInfo: 0
|
editTracksInfo: 0,
|
||||||
, getStatus: 0
|
getStatus: 0,
|
||||||
, load: 0
|
load: 0,
|
||||||
, pause: 0
|
pause: 0,
|
||||||
, play: 0
|
play: 0,
|
||||||
, queue: 0
|
queue: 0,
|
||||||
, seek: 0
|
seek: 0,
|
||||||
, setVolume: 0
|
setVolume: 0,
|
||||||
, stop: 0
|
stop: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_MEDIA_RECEIVER_APP_ID = "CC1AD845";
|
export const DEFAULT_MEDIA_RECEIVER_APP_ID = "CC1AD845";
|
||||||
|
|||||||
@@ -7,11 +7,12 @@
|
|||||||
|
|
||||||
import { SenderApplication, Volume, Image } from "./dataClasses";
|
import { SenderApplication, Volume, Image } from "./dataClasses";
|
||||||
import { MediaInfo, QueueItem } from "./media/dataClasses";
|
import { MediaInfo, QueueItem } from "./media/dataClasses";
|
||||||
import { IdleReason
|
import {
|
||||||
, PlayerState
|
IdleReason,
|
||||||
, RepeatMode
|
PlayerState,
|
||||||
, ResumeState } from "./media/enums";
|
RepeatMode,
|
||||||
|
ResumeState
|
||||||
|
} from "./media/enums";
|
||||||
|
|
||||||
export interface MediaStatus {
|
export interface MediaStatus {
|
||||||
mediaSessionId: number;
|
mediaSessionId: number;
|
||||||
@@ -23,65 +24,62 @@ export interface MediaStatus {
|
|||||||
currentTime: number;
|
currentTime: number;
|
||||||
supportedMediaCommands: number;
|
supportedMediaCommands: number;
|
||||||
repeatMode: RepeatMode;
|
repeatMode: RepeatMode;
|
||||||
volume: Volume
|
volume: Volume;
|
||||||
customData: unknown;
|
customData: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReceiverApplication {
|
export interface ReceiverApplication {
|
||||||
appId: string
|
appId: string;
|
||||||
, appType?: string
|
appType?: string;
|
||||||
, displayName: string
|
displayName: string;
|
||||||
, iconUrl: string
|
iconUrl: string;
|
||||||
, isIdleScreen: boolean
|
isIdleScreen: boolean;
|
||||||
, launchedFromCloud: boolean
|
launchedFromCloud: boolean;
|
||||||
, namespaces: Array<{ name: string }>
|
namespaces: Array<{ name: string }>;
|
||||||
, sessionId: string
|
sessionId: string;
|
||||||
, statusText: string
|
statusText: string;
|
||||||
, transportId: string
|
transportId: string;
|
||||||
, universalAppId: string
|
universalAppId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReceiverStatus {
|
export interface ReceiverStatus {
|
||||||
applications?: ReceiverApplication[]
|
applications?: ReceiverApplication[];
|
||||||
, isActiveInput?: boolean
|
isActiveInput?: boolean;
|
||||||
, isStandBy?: boolean
|
isStandBy?: boolean;
|
||||||
, volume: Volume
|
volume: Volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface CastSessionUpdated {
|
export interface CastSessionUpdated {
|
||||||
sessionId: string
|
sessionId: string;
|
||||||
, statusText: string
|
statusText: string;
|
||||||
, namespaces: Array<{ name: string }>
|
namespaces: Array<{ name: string }>;
|
||||||
, volume: Volume
|
volume: Volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CastSessionCreated extends CastSessionUpdated {
|
export interface CastSessionCreated extends CastSessionUpdated {
|
||||||
appId: string
|
appId: string;
|
||||||
, appImages: Image[]
|
appImages: Image[];
|
||||||
, displayName: string
|
displayName: string;
|
||||||
, receiverFriendlyName: string
|
receiverFriendlyName: string;
|
||||||
, senderApps: SenderApplication[]
|
senderApps: SenderApplication[];
|
||||||
, transportId: string
|
transportId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface ReqBase {
|
interface ReqBase {
|
||||||
requestId: number;
|
requestId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NS: urn:x-cast:com.google.cast.receiver
|
// NS: urn:x-cast:com.google.cast.receiver
|
||||||
export type SenderMessage =
|
export type SenderMessage =
|
||||||
ReqBase & { type: "LAUNCH", appId: string }
|
| (ReqBase & { type: "LAUNCH"; appId: string })
|
||||||
| ReqBase & { type: "STOP", sessionId: string }
|
| (ReqBase & { type: "STOP"; sessionId: string })
|
||||||
| ReqBase & { type: "GET_STATUS" }
|
| (ReqBase & { type: "GET_STATUS" })
|
||||||
| ReqBase & { type: "GET_APP_AVAILABILITY", appId: string[] }
|
| (ReqBase & { type: "GET_APP_AVAILABILITY"; appId: string[] })
|
||||||
| ReqBase & { type: "SET_VOLUME", volume: Partial<Volume> };
|
| (ReqBase & { type: "SET_VOLUME"; volume: Partial<Volume> });
|
||||||
|
|
||||||
export type ReceiverMessage =
|
export type ReceiverMessage =
|
||||||
ReqBase & { type: "RECEIVER_STATUS", status: ReceiverStatus }
|
| (ReqBase & { type: "RECEIVER_STATUS"; status: ReceiverStatus })
|
||||||
| ReqBase & { type: "LAUNCH_ERROR", reason: string }
|
| (ReqBase & { type: "LAUNCH_ERROR"; reason: string });
|
||||||
|
|
||||||
|
|
||||||
interface MediaReqBase extends ReqBase {
|
interface MediaReqBase extends ReqBase {
|
||||||
mediaSessionId: number;
|
mediaSessionId: number;
|
||||||
@@ -90,84 +88,84 @@ interface MediaReqBase extends ReqBase {
|
|||||||
|
|
||||||
// NS: urn:x-cast:com.google.cast.media
|
// NS: urn:x-cast:com.google.cast.media
|
||||||
export type SenderMediaMessage =
|
export type SenderMediaMessage =
|
||||||
| MediaReqBase & { type: "PLAY" }
|
| (MediaReqBase & { type: "PLAY" })
|
||||||
| MediaReqBase & { type: "PAUSE" }
|
| (MediaReqBase & { type: "PAUSE" })
|
||||||
| MediaReqBase & { type: "MEDIA_GET_STATUS" }
|
| (MediaReqBase & { type: "MEDIA_GET_STATUS" })
|
||||||
| MediaReqBase & { type: "STOP" }
|
| (MediaReqBase & { type: "STOP" })
|
||||||
| MediaReqBase & { type: "MEDIA_SET_VOLUME", volume: Partial<Volume> }
|
| (MediaReqBase & { type: "MEDIA_SET_VOLUME"; volume: Partial<Volume> })
|
||||||
| MediaReqBase & { type: "SET_PLAYBACK_RATE" , playbackRate: number }
|
| (MediaReqBase & { type: "SET_PLAYBACK_RATE"; playbackRate: number })
|
||||||
| ReqBase & {
|
| (ReqBase & {
|
||||||
type: "LOAD"
|
type: "LOAD";
|
||||||
, activeTrackIds: Nullable<number[]>
|
activeTrackIds: Nullable<number[]>;
|
||||||
, atvCredentials?: string
|
atvCredentials?: string;
|
||||||
, atvCredentialsType?: string
|
atvCredentialsType?: string;
|
||||||
, autoplay: Nullable<boolean>
|
autoplay: Nullable<boolean>;
|
||||||
, currentTime: Nullable<number>
|
currentTime: Nullable<number>;
|
||||||
, customData?: unknown
|
customData?: unknown;
|
||||||
, media: MediaInfo
|
media: MediaInfo;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "SEEK"
|
type: "SEEK";
|
||||||
, resumeState: Nullable<ResumeState>
|
resumeState: Nullable<ResumeState>;
|
||||||
, currentTime: Nullable<number>
|
currentTime: Nullable<number>;
|
||||||
}
|
})
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "EDIT_TRACKS_INFO"
|
type: "EDIT_TRACKS_INFO";
|
||||||
, activeTrackIds: Nullable<number[]>
|
activeTrackIds: Nullable<number[]>;
|
||||||
, textTrackStyle: Nullable<string>
|
textTrackStyle: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueLoadRequest
|
// QueueLoadRequest
|
||||||
| ReqBase & {
|
| (ReqBase & {
|
||||||
type: "QUEUE_LOAD"
|
type: "QUEUE_LOAD";
|
||||||
, items: QueueItem[]
|
items: QueueItem[];
|
||||||
, startIndex: number
|
startIndex: number;
|
||||||
, repeatMode: string
|
repeatMode: string;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueInsertItemsRequest
|
// QueueInsertItemsRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_INSERT"
|
type: "QUEUE_INSERT";
|
||||||
, items: QueueItem[]
|
items: QueueItem[];
|
||||||
, insertBefore: Nullable<number>
|
insertBefore: Nullable<number>;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueUpdateItemsRequest
|
// QueueUpdateItemsRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_UPDATE"
|
type: "QUEUE_UPDATE";
|
||||||
, items: QueueItem[]
|
items: QueueItem[];
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueJumpRequest
|
// QueueJumpRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_UPDATE"
|
type: "QUEUE_UPDATE";
|
||||||
, jump: Nullable<number>
|
jump: Nullable<number>;
|
||||||
, currentItemId: Nullable<number>
|
currentItemId: Nullable<number>;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueRemoveItemsRequest
|
// QueueRemoveItemsRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_REMOVE"
|
type: "QUEUE_REMOVE";
|
||||||
, itemIds: number[]
|
itemIds: number[];
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueReorderItemsRequest
|
// QueueReorderItemsRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_REORDER"
|
type: "QUEUE_REORDER";
|
||||||
, itemIds: number[]
|
itemIds: number[];
|
||||||
, insertBefore: Nullable<number>
|
insertBefore: Nullable<number>;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
}
|
})
|
||||||
// QueueSetPropertiesRequest
|
// QueueSetPropertiesRequest
|
||||||
| MediaReqBase & {
|
| (MediaReqBase & {
|
||||||
type: "QUEUE_UPDATE"
|
type: "QUEUE_UPDATE";
|
||||||
, repeatMode: Nullable<string>
|
repeatMode: Nullable<string>;
|
||||||
, sessionId: Nullable<string>
|
sessionId: Nullable<string>;
|
||||||
};
|
});
|
||||||
|
|
||||||
export type ReceiverMediaMessage =
|
export type ReceiverMediaMessage =
|
||||||
MediaReqBase & { type: "MEDIA_STATUS", status: MediaStatus[] }
|
| (MediaReqBase & { type: "MEDIA_STATUS"; status: MediaStatus[] })
|
||||||
| MediaReqBase & { type: "INVALID_PLAYER_STATE" }
|
| (MediaReqBase & { type: "INVALID_PLAYER_STATE" })
|
||||||
| MediaReqBase & { type: "LOAD_FAILED" }
|
| (MediaReqBase & { type: "LOAD_FAILED" })
|
||||||
| MediaReqBase & { type: "LOAD_CANCELLED" }
|
| (MediaReqBase & { type: "LOAD_CANCELLED" })
|
||||||
| MediaReqBase & { type: "INVALID_REQUEST" };
|
| (MediaReqBase & { type: "INVALID_REQUEST" });
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { CAST_LOADER_SCRIPT_URL
|
import { CAST_LOADER_SCRIPT_URL, CAST_SCRIPT_URLS } from "../lib/endpoints";
|
||||||
, CAST_SCRIPT_URLS } from "../lib/endpoints";
|
|
||||||
|
|
||||||
|
const _window = window.wrappedJSObject as any;
|
||||||
const _window = (window.wrappedJSObject as any);
|
|
||||||
|
|
||||||
_window.chrome = cloneInto({}, window);
|
_window.chrome = cloneInto({}, window);
|
||||||
|
|
||||||
@@ -16,7 +14,6 @@ if (window.location.host === "www.youtube.com") {
|
|||||||
_window.navigator.presentation = cloneInto({}, window);
|
_window.navigator.presentation = cloneInto({}, window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the src property setter on <script> elements to
|
* Replace the src property setter on <script> elements to
|
||||||
* intercept the new value.
|
* intercept the new value.
|
||||||
@@ -26,16 +23,16 @@ if (window.location.host === "www.youtube.com") {
|
|||||||
* which is handled in the main script.
|
* which is handled in the main script.
|
||||||
*/
|
*/
|
||||||
const desc = Reflect.getOwnPropertyDescriptor(
|
const desc = Reflect.getOwnPropertyDescriptor(
|
||||||
HTMLScriptElement.prototype.wrappedJSObject, "src");
|
HTMLScriptElement.prototype.wrappedJSObject,
|
||||||
|
"src"
|
||||||
|
);
|
||||||
|
|
||||||
Reflect.defineProperty(
|
Reflect.defineProperty(HTMLScriptElement.prototype.wrappedJSObject, "src", {
|
||||||
HTMLScriptElement.prototype.wrappedJSObject, "src", {
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
get: desc?.get,
|
||||||
|
|
||||||
configurable: true
|
set: exportFunction(function setFunc(this: HTMLScriptElement, value) {
|
||||||
, enumerable: true
|
|
||||||
, get: desc?.get
|
|
||||||
|
|
||||||
, set: exportFunction(function setFunc(this: HTMLScriptElement, value) {
|
|
||||||
if (CAST_SCRIPT_URLS.includes(value)) {
|
if (CAST_SCRIPT_URLS.includes(value)) {
|
||||||
return desc?.set?.call(this, CAST_LOADER_SCRIPT_URL);
|
return desc?.set?.call(this, CAST_LOADER_SCRIPT_URL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { onMessageResponse, sendMessage } from "./eventMessageChannel";
|
|||||||
|
|
||||||
import messaging, { Message } from "../messaging";
|
import messaging, { Message } from "../messaging";
|
||||||
|
|
||||||
|
|
||||||
// Message port to background script
|
// Message port to background script
|
||||||
export const backgroundPort = messaging.connect({ name: "shim" });
|
export const backgroundPort = messaging.connect({ name: "shim" });
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,12 @@
|
|||||||
|
|
||||||
import { Message } from "../messaging";
|
import { Message } from "../messaging";
|
||||||
|
|
||||||
|
|
||||||
type ListenerFunc = (message: Message) => void;
|
type ListenerFunc = (message: Message) => void;
|
||||||
|
|
||||||
export interface ListenerObject {
|
export interface ListenerObject {
|
||||||
disconnect (): void;
|
disconnect(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function onMessage(listener: ListenerFunc): ListenerObject {
|
export function onMessage(listener: ListenerFunc): ListenerObject {
|
||||||
function on__castMessage(ev: CustomEvent) {
|
function on__castMessage(ev: CustomEvent) {
|
||||||
listener(JSON.parse(ev.detail));
|
listener(JSON.parse(ev.detail));
|
||||||
@@ -26,16 +24,16 @@ export function onMessage(listener: ListenerFunc): ListenerObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
document.addEventListener(
|
document.addEventListener("__castMessage", on__castMessage, true);
|
||||||
"__castMessage"
|
|
||||||
, on__castMessage, true);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
disconnect() {
|
disconnect() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
document.removeEventListener(
|
document.removeEventListener(
|
||||||
"__castMessage"
|
"__castMessage",
|
||||||
, on__castMessage, true);
|
on__castMessage,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -48,7 +46,6 @@ export function sendMessageResponse(message: Message) {
|
|||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function onMessageResponse(listener: ListenerFunc): ListenerObject {
|
export function onMessageResponse(listener: ListenerFunc): ListenerObject {
|
||||||
function on__castMessageResponse(ev: CustomEvent) {
|
function on__castMessageResponse(ev: CustomEvent) {
|
||||||
listener(JSON.parse(ev.detail));
|
listener(JSON.parse(ev.detail));
|
||||||
@@ -56,15 +53,19 @@ export function onMessageResponse(listener: ListenerFunc): ListenerObject {
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"__castMessageResponse"
|
"__castMessageResponse",
|
||||||
, on__castMessageResponse, true);
|
on__castMessageResponse,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
disconnect() {
|
disconnect() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
document.removeEventListener(
|
document.removeEventListener(
|
||||||
"__castMessageResponse"
|
"__castMessageResponse",
|
||||||
, on__castMessageResponse, true);
|
on__castMessageResponse,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { Message } from "../messaging";
|
|||||||
import { BridgeInfo } from "../lib/bridge";
|
import { BridgeInfo } from "../lib/bridge";
|
||||||
import { TypedMessagePort } from "../lib/TypedMessagePort";
|
import { TypedMessagePort } from "../lib/TypedMessagePort";
|
||||||
|
|
||||||
import { onMessage
|
import {
|
||||||
, onMessageResponse
|
onMessage,
|
||||||
, sendMessage } from "./eventMessageChannel";
|
onMessageResponse,
|
||||||
|
sendMessage
|
||||||
|
} from "./eventMessageChannel";
|
||||||
|
|
||||||
let initializedBridgeInfo: BridgeInfo;
|
let initializedBridgeInfo: BridgeInfo;
|
||||||
let initializedBackgroundPort: MessagePort;
|
let initializedBackgroundPort: MessagePort;
|
||||||
@@ -23,7 +24,6 @@ let initializedBackgroundPort: MessagePort;
|
|||||||
*/
|
*/
|
||||||
export function ensureInit(): Promise<TypedMessagePort<Message>> {
|
export function ensureInit(): Promise<TypedMessagePort<Message>> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
|
||||||
// If already initialized, just return existing bridge info
|
// If already initialized, just return existing bridge info
|
||||||
if (initializedBridgeInfo) {
|
if (initializedBridgeInfo) {
|
||||||
if (initializedBridgeInfo.isVersionCompatible) {
|
if (initializedBridgeInfo.isVersionCompatible) {
|
||||||
@@ -45,8 +45,9 @@ export function ensureInit(): Promise<TypedMessagePort<Message>> {
|
|||||||
* URL.
|
* URL.
|
||||||
*/
|
*/
|
||||||
if (window.location.protocol === "moz-extension:") {
|
if (window.location.protocol === "moz-extension:") {
|
||||||
const { default: ShimManager } =
|
const { default: ShimManager } = await import(
|
||||||
await import("../background/ShimManager");
|
"../background/ShimManager"
|
||||||
|
);
|
||||||
|
|
||||||
// port2 will post bridge messages to port 1
|
// port2 will post bridge messages to port 1
|
||||||
await ShimManager.init();
|
await ShimManager.init();
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import logger from "../../lib/logger";
|
import logger from "../../lib/logger";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom element for a cast button used by sites that injects
|
* Custom element for a cast button used by sites that injects
|
||||||
* a cast icon and manages visibility state and event handling.
|
* 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.classList.add("cast_caf_state_d");
|
||||||
iconArch1.setAttribute("id", "cast_caf_icon_arch1");
|
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.classList.add("cast_caf_state_d");
|
||||||
iconArch2.setAttribute("id", "cast_caf_icon_arch2");
|
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.classList.add("cast_caf_state_d");
|
||||||
iconBox.setAttribute("id", "cast_caf_icon_box");
|
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.classList.add("cast_caf_state_h");
|
||||||
iconBoxFill.setAttribute("id", "cast_caf_icon_boxfill");
|
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
|
// Add icon paths to SVG
|
||||||
icon.append(iconArch0, iconArch1, iconArch2, iconBox, iconBoxFill);
|
icon.append(iconArch0, iconArch1, iconArch2, iconBox, iconBoxFill);
|
||||||
@@ -68,7 +79,6 @@ export default class GoogleCastLauncher extends HTMLElement {
|
|||||||
const shadow = this.attachShadow({ mode: "open" });
|
const shadow = this.attachShadow({ mode: "open" });
|
||||||
shadow.append(icon, style);
|
shadow.append(icon, style);
|
||||||
|
|
||||||
|
|
||||||
this.addEventListener("click", () => {
|
this.addEventListener("click", () => {
|
||||||
logger.info("<google-cast-launcher> onClick");
|
logger.info("<google-cast-launcher> onClick");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ import EventData from "./EventData";
|
|||||||
|
|
||||||
import { SessionEventType } from "../enums";
|
import { SessionEventType } from "../enums";
|
||||||
|
|
||||||
|
|
||||||
export default class ActiveInputStateEventData extends EventData {
|
export default class ActiveInputStateEventData extends EventData {
|
||||||
constructor(
|
constructor(public activeInputState: number) {
|
||||||
public activeInputState: number) {
|
|
||||||
|
|
||||||
super(SessionEventType.ACTIVE_INPUT_STATE_CHANGED);
|
super(SessionEventType.ACTIVE_INPUT_STATE_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import * as cast from "../../cast";
|
import * as cast from "../../cast";
|
||||||
|
|
||||||
|
|
||||||
export default class ApplicationMetadata {
|
export default class ApplicationMetadata {
|
||||||
public applicationId: string;
|
public applicationId: string;
|
||||||
public images: cast.Image[];
|
public images: cast.Image[];
|
||||||
@@ -16,6 +15,7 @@ export default class ApplicationMetadata {
|
|||||||
|
|
||||||
// Convert [{ name: <ns> }, ...] to [ <ns>, ... ]
|
// Convert [{ name: <ns> }, ...] to [ <ns>, ... ]
|
||||||
this.namespaces = sessionObj.namespaces.map(
|
this.namespaces = sessionObj.namespaces.map(
|
||||||
namespaceObj => namespaceObj.name);
|
namespaceObj => namespaceObj.name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ import EventData from "./EventData";
|
|||||||
|
|
||||||
import { SessionEventType } from "../enums";
|
import { SessionEventType } from "../enums";
|
||||||
|
|
||||||
|
|
||||||
export default class ApplicationMetadataEventData extends EventData {
|
export default class ApplicationMetadataEventData extends EventData {
|
||||||
constructor(
|
constructor(public metadata: ApplicationMetadata) {
|
||||||
public metadata: ApplicationMetadata) {
|
|
||||||
|
|
||||||
super(SessionEventType.APPLICATION_METADATA_CHANGED);
|
super(SessionEventType.APPLICATION_METADATA_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ import EventData from "./EventData";
|
|||||||
|
|
||||||
import { SessionEventType } from "../enums";
|
import { SessionEventType } from "../enums";
|
||||||
|
|
||||||
|
|
||||||
export default class ApplicationStatusEventData extends EventData {
|
export default class ApplicationStatusEventData extends EventData {
|
||||||
constructor(
|
constructor(public status: string) {
|
||||||
public status: string) {
|
|
||||||
|
|
||||||
super(SessionEventType.APPLICATION_STATUS_CHANGED);
|
super(SessionEventType.APPLICATION_STATUS_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import logger from "../../../lib/logger";
|
|||||||
import CastOptions from "./CastOptions";
|
import CastOptions from "./CastOptions";
|
||||||
import CastSession from "./CastSession";
|
import CastSession from "./CastSession";
|
||||||
|
|
||||||
|
|
||||||
export default class CastContext extends EventTarget {
|
export default class CastContext extends EventTarget {
|
||||||
public endCurrentSession(_stopCasting: boolean): void {
|
public endCurrentSession(_stopCasting: boolean): void {
|
||||||
logger.info("STUB :: CastContext#endCurrentSession");
|
logger.info("STUB :: CastContext#endCurrentSession");
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
|
|
||||||
import * as cast from "../../cast";
|
import * as cast from "../../cast";
|
||||||
|
|
||||||
|
|
||||||
export default class CastOptions {
|
export default class CastOptions {
|
||||||
public autoJoinPolicy: string = cast.AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED;
|
public autoJoinPolicy: string = cast.AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED;
|
||||||
public language: (string | null) = null;
|
public language: string | null = null;
|
||||||
public receiverApplicationId: (string | null) = null;
|
public receiverApplicationId: string | null = null;
|
||||||
public resumeSavedSession = true;
|
public resumeSavedSession = true;
|
||||||
|
|
||||||
constructor(options: CastOptions = ({} as CastOptions)) {
|
constructor(options: CastOptions = {} as CastOptions) {
|
||||||
if (options.autoJoinPolicy) {
|
if (options.autoJoinPolicy) {
|
||||||
this.autoJoinPolicy = options.autoJoinPolicy;
|
this.autoJoinPolicy = options.autoJoinPolicy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ import * as cast from "../../cast";
|
|||||||
|
|
||||||
import ApplicationMetadata from "./ApplicationMetadata";
|
import ApplicationMetadata from "./ApplicationMetadata";
|
||||||
|
|
||||||
|
|
||||||
type MessageListener = (namespace: string, message: string) => void;
|
type MessageListener = (namespace: string, message: string) => void;
|
||||||
|
|
||||||
|
|
||||||
export default class CastSession extends EventTarget {
|
export default class CastSession extends EventTarget {
|
||||||
constructor(_sessionObj: cast.Session, _state: string) {
|
constructor(_sessionObj: cast.Session, _state: string) {
|
||||||
super();
|
super();
|
||||||
@@ -17,9 +15,9 @@ export default class CastSession extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addMessageListener(
|
public addMessageListener(
|
||||||
_namespace: string
|
_namespace: string,
|
||||||
, _listener: MessageListener): void {
|
_listener: MessageListener
|
||||||
|
): void {
|
||||||
logger.info("STUB :: CastSession#addMessageListener");
|
logger.info("STUB :: CastSession#addMessageListener");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,17 +81,17 @@ export default class CastSession extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public removeMessageListener(
|
public removeMessageListener(
|
||||||
_namespace: string
|
_namespace: string,
|
||||||
, _listener: MessageListener): void {
|
_listener: MessageListener
|
||||||
|
): void {
|
||||||
logger.info("STUB :: CastSession#removeMessageListener");
|
logger.info("STUB :: CastSession#removeMessageListener");
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendMessage(
|
public sendMessage(
|
||||||
_namespace: string
|
_namespace: string,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
, _data: any): Promise<string> {
|
_data: any
|
||||||
|
): Promise<string> {
|
||||||
logger.info("STUB :: CastSession#sendMessage");
|
logger.info("STUB :: CastSession#sendMessage");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ import EventData from "./EventData";
|
|||||||
|
|
||||||
import { CastContextEventType } from "../enums";
|
import { CastContextEventType } from "../enums";
|
||||||
|
|
||||||
|
|
||||||
export default class CastStateEventData extends EventData {
|
export default class CastStateEventData extends EventData {
|
||||||
constructor(
|
constructor(public castState: string) {
|
||||||
public castState: string) {
|
|
||||||
|
|
||||||
super(CastContextEventType.CAST_STATE_CHANGED);
|
super(CastContextEventType.CAST_STATE_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
export default class EventData {
|
export default class EventData {
|
||||||
constructor(
|
constructor(public type: string) {}
|
||||||
public type: string) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,8 @@ import EventData from "./EventData";
|
|||||||
|
|
||||||
import { SessionEventType } from "../enums";
|
import { SessionEventType } from "../enums";
|
||||||
|
|
||||||
|
|
||||||
export default class MediaSessionEventData extends EventData {
|
export default class MediaSessionEventData extends EventData {
|
||||||
constructor(
|
constructor(public mediaSession: cast.media.Media) {
|
||||||
public mediaSession: cast.media.Media) {
|
|
||||||
|
|
||||||
super(SessionEventType.MEDIA_SESSION);
|
super(SessionEventType.MEDIA_SESSION);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import * as cast from "../../cast";
|
|||||||
|
|
||||||
import RemotePlayerController from "./RemotePlayerController";
|
import RemotePlayerController from "./RemotePlayerController";
|
||||||
|
|
||||||
|
|
||||||
interface SavedPlayerState {
|
interface SavedPlayerState {
|
||||||
mediaInfo: string;
|
mediaInfo: string;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
@@ -15,19 +14,19 @@ export default class RemotePlayer {
|
|||||||
public canControlVolume = false;
|
public canControlVolume = false;
|
||||||
public canPause = false;
|
public canPause = false;
|
||||||
public canSeek = false;
|
public canSeek = false;
|
||||||
public controller: (RemotePlayerController | null) = null;
|
public controller: RemotePlayerController | null = null;
|
||||||
public currentTime = 0;
|
public currentTime = 0;
|
||||||
public displayName = "";
|
public displayName = "";
|
||||||
public displayStatus = "";
|
public displayStatus = "";
|
||||||
public duration = 0;
|
public duration = 0;
|
||||||
public imageUrl: (string | null) = null;
|
public imageUrl: string | null = null;
|
||||||
public isConnected = false;
|
public isConnected = false;
|
||||||
public isMediaLoaded = false;
|
public isMediaLoaded = false;
|
||||||
public isMuted = false;
|
public isMuted = false;
|
||||||
public isPaused = false;
|
public isPaused = false;
|
||||||
public mediaInfo: (cast.media.MediaInfo | null) = null;
|
public mediaInfo: cast.media.MediaInfo | null = null;
|
||||||
public playerState: (string | null) = null;
|
public playerState: string | null = null;
|
||||||
public savedPlayerState: (SavedPlayerState | null) = null;
|
public savedPlayerState: SavedPlayerState | null = null;
|
||||||
public statusText = "";
|
public statusText = "";
|
||||||
public title = "";
|
public title = "";
|
||||||
public volumeLevel = 1;
|
public volumeLevel = 1;
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
export default class RemotePlayerChangedEvent {
|
export default class RemotePlayerChangedEvent {
|
||||||
constructor(
|
constructor(public type: string, public field: string, public value: any) {}
|
||||||
public type: string
|
|
||||||
, public field: string
|
|
||||||
, public value: any) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import logger from "../../../lib/logger";
|
|||||||
|
|
||||||
import RemotePlayer from "./RemotePlayer";
|
import RemotePlayer from "./RemotePlayer";
|
||||||
|
|
||||||
|
|
||||||
export default class RemotePlayerController extends EventTarget {
|
export default class RemotePlayerController extends EventTarget {
|
||||||
constructor(_player: RemotePlayer) {
|
constructor(_player: RemotePlayer) {
|
||||||
super();
|
super();
|
||||||
@@ -16,7 +15,7 @@ export default class RemotePlayerController extends EventTarget {
|
|||||||
const minutes = Math.floor(timeInSec / 60) % 60;
|
const minutes = Math.floor(timeInSec / 60) % 60;
|
||||||
const seconds = timeInSec % 60;
|
const seconds = timeInSec % 60;
|
||||||
|
|
||||||
return [ hours, minutes, seconds ]
|
return [hours, minutes, seconds]
|
||||||
.map(c => c.toString().padStart(2, "0"))
|
.map(c => c.toString().padStart(2, "0"))
|
||||||
.join(":");
|
.join(":");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import EventData from "./EventData";
|
|||||||
|
|
||||||
import { SessionEventType } from "../enums";
|
import { SessionEventType } from "../enums";
|
||||||
|
|
||||||
|
|
||||||
export default class SessionStateEventData extends EventData {
|
export default class SessionStateEventData extends EventData {
|
||||||
constructor(
|
constructor(
|
||||||
public session: CastSession
|
public session: CastSession,
|
||||||
, public sessionState: string
|
public sessionState: string,
|
||||||
, public errorCode: (string | null) = null) {
|
public errorCode: string | null = null
|
||||||
|
) {
|
||||||
super(SessionEventType.APPLICATION_STATUS_CHANGED);
|
super(SessionEventType.APPLICATION_STATUS_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
|
|
||||||
import { SessionEventType } from "../enums";
|
import { SessionEventType } from "../enums";
|
||||||
|
|
||||||
|
|
||||||
export default class VolumeEventData {
|
export default class VolumeEventData {
|
||||||
public type = SessionEventType.VOLUME_CHANGED;
|
public type = SessionEventType.VOLUME_CHANGED;
|
||||||
|
|
||||||
constructor(
|
constructor(public volume: number, public isMute: boolean) {}
|
||||||
public volume: number
|
|
||||||
, public isMute: boolean) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,64 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
export enum ActiveInputState {
|
export enum ActiveInputState {
|
||||||
ACTIVE_INPUT_STATE_UNKNOWN = -1
|
ACTIVE_INPUT_STATE_UNKNOWN = -1,
|
||||||
, ACTIVE_INPUT_STATE_NO = 0
|
ACTIVE_INPUT_STATE_NO = 0,
|
||||||
, ACTIVE_INPUT_YES = 1
|
ACTIVE_INPUT_YES = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CastContextEventType {
|
export enum CastContextEventType {
|
||||||
CAST_STATE_CHANGED = "caststatechanged"
|
CAST_STATE_CHANGED = "caststatechanged",
|
||||||
, SESSION_STATE_CHANGED = "sessionstatechanged"
|
SESSION_STATE_CHANGED = "sessionstatechanged"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CastState {
|
export enum CastState {
|
||||||
NO_DEVICES_AVAILABLE = "NO_DEVICES_AVAILABLE"
|
NO_DEVICES_AVAILABLE = "NO_DEVICES_AVAILABLE",
|
||||||
, NOT_CONNECTED = "NOT_CONNECTED"
|
NOT_CONNECTED = "NOT_CONNECTED",
|
||||||
, CONNECTING = "CONNECTING"
|
CONNECTING = "CONNECTING",
|
||||||
, CONNECTED = "CONNECTED"
|
CONNECTED = "CONNECTED"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LoggerLevel {
|
export enum LoggerLevel {
|
||||||
DEBUG = 0
|
DEBUG = 0,
|
||||||
, INFO = 800
|
INFO = 800,
|
||||||
, WARNING = 900
|
WARNING = 900,
|
||||||
, ERROR = 1000
|
ERROR = 1000,
|
||||||
, NONE = 1500
|
NONE = 1500
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RemotePlayerEventType {
|
export enum RemotePlayerEventType {
|
||||||
ANY_CHANGE = "anyChanged"
|
ANY_CHANGE = "anyChanged",
|
||||||
, IS_CONNECTED_CHANGE = "isConnectedChanged"
|
IS_CONNECTED_CHANGE = "isConnectedChanged",
|
||||||
, IS_MEDIA_LOADED_CHANGED = "isMediaLoadedChanged"
|
IS_MEDIA_LOADED_CHANGED = "isMediaLoadedChanged",
|
||||||
, DURATION_CHANGED = "durationChanged"
|
DURATION_CHANGED = "durationChanged",
|
||||||
, CURRENT_TIME_CHANGED = "currentTimeChanged"
|
CURRENT_TIME_CHANGED = "currentTimeChanged",
|
||||||
, IS_PAUSED_CHANGED = "isPausedChanged"
|
IS_PAUSED_CHANGED = "isPausedChanged",
|
||||||
, VOLUME_LEVEL_CHANGED = "volumeLevelChanged"
|
VOLUME_LEVEL_CHANGED = "volumeLevelChanged",
|
||||||
, CAN_CONTROL_VOLUME_CHANGED = "canControlVolumeChanged"
|
CAN_CONTROL_VOLUME_CHANGED = "canControlVolumeChanged",
|
||||||
, IS_MUTED_CHANGED = "isMutedChanged"
|
IS_MUTED_CHANGED = "isMutedChanged",
|
||||||
, CAN_PAUSE_CHANGED = "canPauseChanged"
|
CAN_PAUSE_CHANGED = "canPauseChanged",
|
||||||
, CAN_SEEK_CHANGED = "canSeekChanged"
|
CAN_SEEK_CHANGED = "canSeekChanged",
|
||||||
, DISPLAY_NAME_CHANGED = "displayNameChanged"
|
DISPLAY_NAME_CHANGED = "displayNameChanged",
|
||||||
, STATUS_TEXT_CHANGED = "statusTextChanged"
|
STATUS_TEXT_CHANGED = "statusTextChanged",
|
||||||
, MEDIA_INFO_CHANGED = "mediaInfoChanged"
|
MEDIA_INFO_CHANGED = "mediaInfoChanged",
|
||||||
, IMAGE_URL_CHANGED = "imageUrlChanged"
|
IMAGE_URL_CHANGED = "imageUrlChanged",
|
||||||
, PLAYER_STATE_CHANGED = "playerStateChanged"
|
PLAYER_STATE_CHANGED = "playerStateChanged"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SessionEventType {
|
export enum SessionEventType {
|
||||||
APPLICATION_STATUS_CHANGED = "applicationstatuschanged"
|
APPLICATION_STATUS_CHANGED = "applicationstatuschanged",
|
||||||
, APPLICATION_METADATA_CHANGED = "applicationmetadatachanged"
|
APPLICATION_METADATA_CHANGED = "applicationmetadatachanged",
|
||||||
, ACTIVE_INPUT_STATE_CHANGED = "activeinputstatechanged"
|
ACTIVE_INPUT_STATE_CHANGED = "activeinputstatechanged",
|
||||||
, VOLUME_CHANGED = "volumechanged"
|
VOLUME_CHANGED = "volumechanged",
|
||||||
, MEDIA_SESSION = "mediasession"
|
MEDIA_SESSION = "mediasession"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SessionState {
|
export enum SessionState {
|
||||||
NO_SESSION = "NO_SESSION"
|
NO_SESSION = "NO_SESSION",
|
||||||
, SESSION_STARTING = "SESSION_STARTING"
|
SESSION_STARTING = "SESSION_STARTING",
|
||||||
, SESSION_STARTED = "SESSION_STARTED"
|
SESSION_STARTED = "SESSION_STARTED",
|
||||||
, SESSION_START_FAILED = "SESSION_START_FAILED"
|
SESSION_START_FAILED = "SESSION_START_FAILED",
|
||||||
, SESSION_ENDING = "SESSION_ENDING"
|
SESSION_ENDING = "SESSION_ENDING",
|
||||||
, SESSION_ENDED = "SESSION_ENDED"
|
SESSION_ENDED = "SESSION_ENDED",
|
||||||
, SESSION_RESUMED = "SESSION_RESUMED"
|
SESSION_RESUMED = "SESSION_RESUMED"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,49 +18,63 @@ import RemotePlayerController from "./classes/RemotePlayerController";
|
|||||||
import SessionStateEventData from "./classes/SessionStateEventData";
|
import SessionStateEventData from "./classes/SessionStateEventData";
|
||||||
import VolumeEventData from "./classes/VolumeEventData";
|
import VolumeEventData from "./classes/VolumeEventData";
|
||||||
|
|
||||||
import { ActiveInputState
|
import {
|
||||||
, CastContextEventType
|
ActiveInputState,
|
||||||
, CastState
|
CastContextEventType,
|
||||||
, LoggerLevel
|
CastState,
|
||||||
, RemotePlayerEventType
|
LoggerLevel,
|
||||||
, SessionEventType
|
RemotePlayerEventType,
|
||||||
, SessionState } from "./enums";
|
SessionEventType,
|
||||||
|
SessionState
|
||||||
|
} from "./enums";
|
||||||
|
|
||||||
import GoogleCastLauncher from "./GoogleCastLauncher";
|
import GoogleCastLauncher from "./GoogleCastLauncher";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// Enums
|
// Enums
|
||||||
ActiveInputState, CastContextEventType, CastState, LoggerLevel
|
ActiveInputState,
|
||||||
, RemotePlayerEventType, SessionEventType, SessionState
|
CastContextEventType,
|
||||||
|
CastState,
|
||||||
|
LoggerLevel,
|
||||||
|
RemotePlayerEventType,
|
||||||
|
SessionEventType,
|
||||||
|
SessionState,
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
, ActiveInputStateEventData, ApplicationMetadata
|
ActiveInputStateEventData,
|
||||||
, ApplicationMetadataEventData, ApplicationStatusEventData, CastOptions
|
ApplicationMetadata,
|
||||||
, CastSession, CastStateEventData, EventData, MediaSessionEventData
|
ApplicationMetadataEventData,
|
||||||
, RemotePlayer, RemotePlayerChangedEvent, RemotePlayerController
|
ApplicationStatusEventData,
|
||||||
, SessionStateEventData, VolumeEventData
|
CastOptions,
|
||||||
|
CastSession,
|
||||||
|
CastStateEventData,
|
||||||
|
EventData,
|
||||||
|
MediaSessionEventData,
|
||||||
|
RemotePlayer,
|
||||||
|
RemotePlayerChangedEvent,
|
||||||
|
RemotePlayerController,
|
||||||
|
SessionStateEventData,
|
||||||
|
VolumeEventData,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CastContext class with an extra getInstance method used to
|
* CastContext class with an extra getInstance method used to
|
||||||
* instantiate and fetch a singleton instance.
|
* instantiate and fetch a singleton instance.
|
||||||
*/
|
*/
|
||||||
, CastContext: {
|
CastContext: {
|
||||||
...CastContext
|
...CastContext,
|
||||||
|
|
||||||
, getInstance() {
|
getInstance() {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
, VERSION: "1.0.07"
|
VERSION: "1.0.07",
|
||||||
|
|
||||||
, setLoggerLevel(_level: number) {
|
setLoggerLevel(_level: number) {
|
||||||
logger.info("STUB :: cast.framework.setLoggerLevel");
|
logger.info("STUB :: cast.framework.setLoggerLevel");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Framework API defines a <google-cast-launcher> element
|
* The Framework API defines a <google-cast-launcher> element
|
||||||
* and a <button is="google-cast-button"> element extension,
|
* 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 { loadScript } from "../lib/utils";
|
||||||
import { onMessage } from "./eventMessageChannel";
|
import { onMessage } from "./eventMessageChannel";
|
||||||
|
|
||||||
|
const _window = window as any;
|
||||||
const _window = (window as any);
|
|
||||||
|
|
||||||
if (!_window.chrome) {
|
if (!_window.chrome) {
|
||||||
_window.chrome = {};
|
_window.chrome = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Create page-accessible API object
|
// Create page-accessible API object
|
||||||
_window.chrome.cast = cast;
|
_window.chrome.cast = cast;
|
||||||
|
|
||||||
|
|
||||||
let bridgeInfo: any;
|
let bridgeInfo: any;
|
||||||
let isFramework = false;
|
let isFramework = false;
|
||||||
|
|
||||||
@@ -29,14 +26,13 @@ function callPageReadyFunction() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If loaded within a page via a <script> element,
|
* If loaded within a page via a <script> element,
|
||||||
* document.currentScript should exist and we can check its
|
* document.currentScript should exist and we can check its
|
||||||
* [src] query string for the loadCastFramework param.
|
* [src] query string for the loadCastFramework param.
|
||||||
*/
|
*/
|
||||||
if (document.currentScript) {
|
if (document.currentScript) {
|
||||||
const currentScript = (document.currentScript as HTMLScriptElement);
|
const currentScript = document.currentScript as HTMLScriptElement;
|
||||||
const currentScriptUrl = new URL(currentScript.src);
|
const currentScriptUrl = new URL(currentScript.src);
|
||||||
const currentScriptParams = new URLSearchParams(currentScriptUrl.search);
|
const currentScriptParams = new URLSearchParams(currentScriptUrl.search);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { Error as Error_ } from "./cast/dataClasses";
|
import { Error as Error_ } from "./cast/dataClasses";
|
||||||
import { Media } from "./cast/media";
|
import { Media } from "./cast/media";
|
||||||
|
|
||||||
|
|
||||||
export type SuccessCallback = () => void;
|
export type SuccessCallback = () => void;
|
||||||
export type ErrorCallback = (err: Error_) => void;
|
export type ErrorCallback = (err: Error_) => void;
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
import { ReceiverStatus } from "./shim/cast/types";
|
import { ReceiverStatus } from "./shim/cast/types";
|
||||||
|
|
||||||
export interface ReceiverDevice {
|
export interface ReceiverDevice {
|
||||||
host: string
|
host: string;
|
||||||
friendlyName: string
|
friendlyName: string;
|
||||||
, id: string
|
id: string;
|
||||||
, port: number
|
port: number;
|
||||||
, status?: ReceiverStatus
|
status?: ReceiverStatus;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user