prettier: Re-format .ts files

This commit is contained in:
hensm
2021-08-31 08:28:23 +01:00
parent d6ca1325dc
commit 41d8edcab4
82 changed files with 2683 additions and 2532 deletions

View File

@@ -1,5 +1,6 @@
{ {
"arrowParens": "avoid", "arrowParens": "avoid",
"tabWidth": 4, "tabWidth": 4,
"trailingComma": "none" "trailingComma": "none",
"quoteProps": "consistent"
} }

View File

@@ -11,13 +11,12 @@ declare module "bplist-parser" {
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;
} }

View File

@@ -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;
@@ -27,41 +26,45 @@ declare module "castv2" {
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;
} }

View File

@@ -12,27 +12,25 @@ 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;
@@ -42,10 +40,7 @@ declare module "fast-srp-hap" {
} }
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;

View File

@@ -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,11 +108,9 @@ 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);

View File

@@ -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
}); });
} }
} }

View File

@@ -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,
receiverDevice.port,
sessionId => {
sessions.set(sessionId, session); 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 });

View File

@@ -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,7 +172,7 @@ interface VastAdsRequest {
} }
type Metadata = type Metadata =
GenericMediaMetadata | GenericMediaMetadata
| MovieMediaMetadata | MovieMediaMetadata
| MusicTrackMediaMetadata | MusicTrackMediaMetadata
| PhotoMediaMetadata | PhotoMediaMetadata
@@ -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" });

View File

@@ -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,7 +126,6 @@ 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.
@@ -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) {

View File

@@ -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) {
@@ -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", () =>
sendMessage({
subject: "mediaCast:mediaServerStopped" subject: "mediaCast:mediaServerStopped"
})); })
mediaServer.on("error", () => sendMessage({ );
mediaServer.on("error", () =>
sendMessage({
subject: "mediaCast:mediaServerError" subject: "mediaCast:mediaServerError"
})); })
);
mediaServer.listen(port); mediaServer.listen(port);
} }

View File

@@ -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,8 +33,10 @@ 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;
@@ -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
}); });
}); });

View File

@@ -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

View File

@@ -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();

View File

@@ -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

View File

@@ -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]>;

View File

@@ -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 {

View File

@@ -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,7 +32,6 @@ 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.
@@ -43,14 +39,10 @@ export function init(port: number) {
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())
.on("data", data => {
// Socket can be CLOSING here // Socket can be CLOSING here
if (socket.readyState === WebSocket.OPEN) { if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data)); socket.send(JSON.stringify(data));

3
app/src/global.d.ts vendored
View File

@@ -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;

View File

@@ -5,22 +5,21 @@ 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}`);
@@ -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,8 +45,7 @@ Options:
process.exit(1); process.exit(1);
} }
import("./daemon") import("./daemon").then(daemon => {
.then(daemon => {
daemon.init(port); daemon.init(port);
}); });
} else { } else {

View File

@@ -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,19 +12,18 @@ 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 {
@@ -35,7 +33,6 @@ export class ResponseTransform extends Transform {
} }
} }
/** /**
* 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,16 +49,13 @@ 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 // Append next chunk to buffer
this._messageBuffer = Buffer.concat([ this._messageBuffer = Buffer.concat([this._messageBuffer, chunk]);
this._messageBuffer
, chunk
]);
for (;;) { for (;;) {
if (this._messageLength === undefined) { if (this._messageLength === undefined) {
@@ -75,16 +69,19 @@ export class DecodeTransform extends Transform {
} }
} else { } else {
if (this._messageBuffer.length >= this._messageLength) { if (this._messageBuffer.length >= this._messageLength) {
const message = JSON.parse(this._messageBuffer const message = JSON.parse(
this._messageBuffer
.slice(0, this._messageLength) .slice(0, this._messageLength)
.toString()); .toString()
);
// Push message content // Push message content
this.push(message); this.push(message);
// Offset buffer to start of next message // Offset buffer to start of next message
this._messageBuffer = this._messageBuffer.slice( this._messageBuffer = this._messageBuffer.slice(
this._messageLength); this._messageLength
);
this._messageLength = undefined; this._messageLength = undefined;
// Next message // Next message
@@ -99,7 +96,6 @@ export class DecodeTransform extends Transform {
} }
} }
/** /**
* Takes input, encodes the message length and content and * Takes input, encodes the message length and content and
* outputs the encoded result. * outputs the encoded result.
@@ -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
]));
} }
} }

View File

@@ -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);
@@ -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 {
} }
} }
} }
}; })();

View File

@@ -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,7 +156,6 @@ 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
@@ -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
}); });
} }
}); });

View File

@@ -6,15 +6,15 @@ 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://*/*";
@@ -22,7 +22,6 @@ 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;
let menuIdCast: MenuId; let menuIdCast: MenuId;
@@ -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
}); });
} }
@@ -235,7 +235,8 @@ 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.match(/\./g) || []).length > 1
? url.hostname.substring(url.hostname.indexOf(".") + 1) ? url.hostname.substring(url.hostname.indexOf(".") + 1)
: url.hostname; : 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
.split("/")
.filter(segment => segment) .filter(segment => segment)
.reverse(); .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();

View File

@@ -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;
} }
@@ -118,7 +118,9 @@ export default new class extends TypedEventTarget<EventMap> {
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;
} }
@@ -136,16 +138,16 @@ export default new class extends TypedEventTarget<EventMap> {
} }
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
@@ -163,5 +165,5 @@ export default new class extends TypedEventTarget<EventMap> {
window.setTimeout(() => { window.setTimeout(() => {
this.refresh(); this.refresh();
}, 10000); }, 10000);
}
}; };
})();

View File

@@ -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(
new CustomEvent("selected", {
detail: message.data detail: message.data
})); })
);
break; break;
} }
case "receiverSelector:stop": { case "receiverSelector:stop": {
this.dispatchEvent(new CustomEvent("stop", { this.dispatchEvent(
new CustomEvent("stop", {
detail: message.data 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);

View File

@@ -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); logger.info("Selected receiver", ev.detail);
resolve({ resolve({
actionType: ReceiverSelectionActionType.Cast actionType: ReceiverSelectionActionType.Cast,
, receiver: ev.detail.receiver receiver: ev.detail.receiver,
, mediaType: ev.detail.mediaType mediaType: ev.detail.mediaType,
, filePath: ev.detail.filePath filePath: ev.detail.filePath
}); });
removeListeners(); removeListeners();
})); })
);
sharedSelector.addEventListener("cancelled"
, storeListener("cancelled", () => {
sharedSelector.addEventListener(
"cancelled",
storeListener("cancelled", () => {
logger.info("Cancelled receiver selection"); logger.info("Cancelled receiver selection");
resolve(null); resolve(null);
removeListeners(); removeListeners();
})); })
);
sharedSelector.addEventListener("error" sharedSelector.addEventListener(
, storeListener("error", () => { "error",
storeListener("error", () => {
reject(); reject();
removeListeners(); removeListeners();
})); })
);
sharedSelector.addEventListener("stop"
, storeListener("stop", async ev => {
sharedSelector.addEventListener(
"stop",
storeListener("stop", async ev => {
logger.info("Stopping receiver app...", ev.detail); logger.info("Stopping receiver app...", ev.detail);
receiverDevices.stopReceiverApp(ev.detail.receiver.id); receiverDevices.stopReceiverApp(ev.detail.receiver.id);
resolve({ resolve({
actionType: ReceiverSelectionActionType.Stop actionType: ReceiverSelectionActionType.Stop,
, receiver: ev.detail.receiver receiver: ev.detail.receiver
}); });
removeListeners(); 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
}; };

View File

@@ -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 {

View File

@@ -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,15 +53,16 @@ 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
@@ -67,22 +70,24 @@ export async function initWhitelist() {
* 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 =
host?.value === "www.youtube.com"
? chromeUserAgentHybrid ? chromeUserAgentHybrid
: chromeUserAgent; : 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,11 +115,13 @@ 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 =
host?.value === "www.youtube.com"
? chromeUserAgentHybrid ? chromeUserAgentHybrid
: chromeUserAgent; : 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);
} }

View File

@@ -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;

41
ext/src/global.d.ts vendored
View File

@@ -4,12 +4,10 @@ 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
wrappedJSObject: Object; wrappedJSObject: Object;
@@ -23,11 +21,14 @@ declare interface CanvasRenderingContext2D {
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 {
@@ -43,21 +44,19 @@ declare interface RTCPeerConnection {
} }
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;
@@ -68,10 +67,10 @@ 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 {
@@ -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;
} }

View File

@@ -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);
} }

View File

@@ -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
} }

View File

@@ -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);
} }

View File

@@ -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,7 +51,8 @@ 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 = () =>
new Promise<BridgeInfo>(async (resolve, reject) => {
const applicationName = await options.get("bridgeApplicationName"); const applicationName = await options.get("bridgeApplicationName");
if (!applicationName) { if (!applicationName) {
reject(logger.error("Bridge application name not found.")); reject(logger.error("Bridge application name not found."));
@@ -67,9 +69,9 @@ const getInfo = () => new Promise<BridgeInfo>(async (resolve, reject) => {
const { version } = browser.runtime.getManifest(); const { version } = browser.runtime.getManifest();
applicationVersion = await nativeMessaging.sendNativeMessage( applicationVersion = await nativeMessaging.sendNativeMessage(
applicationName applicationName,
, { subject: "bridge:/getInfo" { subject: "bridge:/getInfo", data: version }
, data: version }); );
} catch (err) { } catch (err) {
logger.error("Bridge connection failed."); logger.error("Bridge connection failed.");
reject(new BridgeConnectionError()); reject(new BridgeConnectionError());
@@ -91,9 +93,9 @@ const getInfo = () => new Promise<BridgeInfo>(async (resolve, reject) => {
* compatible. * compatible.
*/ */
const isVersionCompatible = const isVersionCompatible =
semver.eq(applicationVersion, extensionVersion) semver.eq(applicationVersion, extensionVersion) ||
|| (versionDiff !== "major" && extensionVersionMajor !== 0) (versionDiff !== "major" && extensionVersionMajor !== 0) ||
|| (versionDiff === "patch" && extensionVersionMajor === 0); (versionDiff === "patch" && extensionVersionMajor === 0);
const isVersionExact = semver.eq(applicationVersion, extensionVersion); const isVersionExact = semver.eq(applicationVersion, extensionVersion);
const isVersionOlder = semver.lt(applicationVersion, extensionVersion); const isVersionOlder = semver.lt(applicationVersion, extensionVersion);
@@ -101,26 +103,29 @@ const getInfo = () => new Promise<BridgeInfo>(async (resolve, reject) => {
// Print compatibility info to console // Print compatibility info to console
if (!isVersionCompatible) { if (!isVersionCompatible) {
logger.error(`Expecting ${applicationName} v${BRIDGE_VERSION}, found v${applicationVersion}. ${ logger.error(
`Expecting ${applicationName} v${BRIDGE_VERSION}, found v${applicationVersion}. ${
isVersionOlder isVersionOlder
? "Try updating the native app to the latest version." ? "Try updating the native app to the latest version."
: "Try updating the extension to the latest version"}`); : "Try updating the extension to the latest version"
}`
);
} }
resolve({ resolve({
name: applicationName name: applicationName,
, version: applicationVersion version: applicationVersion,
, expectedVersion: BRIDGE_VERSION expectedVersion: BRIDGE_VERSION,
// Version info // Version info
, isVersionExact isVersionExact,
, isVersionCompatible isVersionCompatible,
, isVersionOlder isVersionOlder,
, isVersionNewer isVersionNewer
}); });
}); });
export default { export default {
connect connect,
, getInfo getInfo
}; };

View File

@@ -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"
]; ];
/** /**

View File

@@ -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>;

View File

@@ -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;

View File

@@ -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
}; };

View File

@@ -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(
new CustomEvent("changed", {
detail: changedKeys as Array<keyof Options> 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
@@ -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);
} }
}; })();

View File

@@ -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}`;
} }

View File

@@ -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);

View File

@@ -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>();

View File

@@ -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);
}); });
@@ -118,14 +119,13 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
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}`;
@@ -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
}); });
} }

View File

@@ -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
}; };
} }

View File

@@ -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
);
} }
} }

View File

@@ -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(){

View File

@@ -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,24 +32,22 @@ 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) { switch (subject) {
@@ -63,10 +60,11 @@ async function onRequestSessionSuccess(newSession: cast.Session) {
break; 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);
}); });

View File

@@ -6,26 +6,28 @@ 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
@@ -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,9 +90,10 @@ 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?]
>();
/** /**
* *
@@ -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);
} }

View File

@@ -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
) {}
} }

View File

@@ -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"
} }

View File

@@ -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,14 +55,34 @@ 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,
export { ApiConfig, CredentialsData, DialRequest, Error_ as Error, Image Error_ as Error,
, Receiver, ReceiverDisplayStatus, SenderApplication, SessionRequest Image,
, Timeout, Volume, Session }; Receiver,
ReceiverDisplayStatus,
SenderApplication,
SessionRequest,
Timeout,
Volume,
Session
};
export const VERSION = [1, 2]; export const VERSION = [1, 2];
export let isAvailable = false; export let isAvailable = false;
@@ -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(
receiverDevices.size
? ReceiverAvailability.AVAILABLE ? ReceiverAvailability.AVAILABLE
: ReceiverAvailability.UNAVAILABLE); : 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;
@@ -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;
} }
} }
}); });

View File

@@ -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",
mediaSessionId: this.mediaSessionId
})
.then(successCallback) .then(successCallback)
.catch(errorCallback); .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(() => { })
.then(() => {
if (successCallback) { if (successCallback) {
successCallback(); successCallback();
} }
}).catch(errorCallback); })
.catch(errorCallback);
} }
supportsCommand(command: string): boolean { supportsCommand(command: string): boolean {

View File

@@ -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,9 +128,8 @@ export class LoadRequest {
} }
} }
export type Metadata = export type Metadata =
GenericMediaMetadata | GenericMediaMetadata
| MovieMediaMetadata | MovieMediaMetadata
| MusicTrackMediaMetadata | MusicTrackMediaMetadata
| PhotoMediaMetadata | PhotoMediaMetadata
@@ -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) {}
} }

View File

@@ -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"
} }

View File

@@ -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";

View File

@@ -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" });

View File

@@ -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);
} }

View File

@@ -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" });

View File

@@ -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
);
} }
}; };
} }

View File

@@ -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();

View File

@@ -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");
}); });

View File

@@ -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);
} }
} }

View File

@@ -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
);
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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");

View File

@@ -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;
} }

View File

@@ -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");
} }

View File

@@ -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);
} }
} }

View File

@@ -1,6 +1,5 @@
"use strict"; "use strict";
export default class EventData { export default class EventData {
constructor( constructor(public type: string) {}
public type: string) {}
} }

View File

@@ -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);
} }
} }

View File

@@ -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;

View File

@@ -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) {}
} }

View File

@@ -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();

View File

@@ -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);
} }
} }

View File

@@ -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) {}
} }

View File

@@ -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"
} }

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
} }