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",
"tabWidth": 4,
"trailingComma": "none"
"trailingComma": "none",
"quoteProps": "consistent"
}

View File

@@ -3,13 +3,13 @@
declare module "bplist-creator" {
import Buffer from "buffer";
function bplist (dicts: object): Buffer;
function bplist(dicts: object): Buffer;
export = bplist;
namespace bplist {
export class Real {
public value: number;
constructor (value: number);
constructor(value: number);
}
}
}

View File

@@ -7,17 +7,16 @@ declare module "bplist-parser" {
export var maxObjectCount: number;
export class UID {
constructor (id: number);
constructor(id: number);
UID: number;
}
type ParseFileCallback = (
err: string
, result?: Buffer) => void;
type ParseFileCallback = (err: string, result?: Buffer) => void;
export function parseFile (
fileNameOrBuffer: Buffer | string
, callback: ParseFileCallback): void;
export function parseFile(
fileNameOrBuffer: Buffer | string,
callback: ParseFileCallback
): void;
export function parseBuffer (buffer: Buffer): any;
export function parseBuffer(buffer: Buffer): any;
}

View File

@@ -10,7 +10,6 @@ declare module "castv2" {
type CallbackFunction = () => void;
export interface Channel extends EventEmitter {
bus: Client;
sourceId: string;
@@ -18,51 +17,55 @@ declare module "castv2" {
namespace: string;
encoding: string;
send (data: any): void;
close (): void;
send(data: any): void;
close(): void;
}
export interface DeviceAuthMessage {
parse (data: any): any;
serialize (data: any): any;
parse(data: any): any;
serialize(data: any): any;
}
export class Client extends EventEmitter {
public connect (
options: ClientConnectOptions | string
, callback?: CallbackFunction): void;
public connect(
options: ClientConnectOptions | string,
callback?: CallbackFunction
): void;
public close (): void;
public close(): void;
public send (
sourceId: string
, destinationId: string
, namespace: string
, data: Buffer | string): void;
public send(
sourceId: string,
destinationId: string,
namespace: string,
data: Buffer | string
): void;
public createChannel (
sourceId: string
, destinationId: string
, namespace: string
, encoding: string): Channel;
public createChannel(
sourceId: string,
destinationId: string,
namespace: string,
encoding: string
): Channel;
}
export class Server extends EventEmitter {
constructor (options: object);
constructor(options: object);
public listen (
port: number
, host: string
, callback?: CallbackFunction): void;
public listen(
port: number,
host: string,
callback?: CallbackFunction
): void;
public send (
clientId: string
, sourceId: string
, destinationId: string
, namespace: string
, data: Buffer | string): void;
public send(
clientId: string,
sourceId: string,
destinationId: string,
namespace: string,
data: Buffer | string
): void;
public close (): void;
public close(): void;
}
}

View File

@@ -12,44 +12,39 @@ declare module "fast-srp-hap" {
export const params: { [key: number]: Param };
type GenKeyCallback = (
err: string
, buf: Buffer) => void;
type GenKeyCallback = (err: string, buf: Buffer) => void;
export function genKey (
bytes: number
, callback: GenKeyCallback): void;
export function genKey(bytes: number, callback: GenKeyCallback): void;
export function computeVerifier (
params: object
, salt: Buffer
, I: Buffer
, P: Buffer): Buffer;
export function computeVerifier(
params: object,
salt: Buffer,
I: Buffer,
P: Buffer
): Buffer;
export class Client {
constructor (
params: object
, salt_buf: Buffer
, identity_buf: Buffer
, password_buf: Buffer
, secret1_buf: Buffer);
constructor(
params: object,
salt_buf: Buffer,
identity_buf: Buffer,
password_buf: Buffer,
secret1_buf: Buffer
);
computeA (): Buffer;
computeA(): Buffer;
setB(B_buf: Buffer): void;
computeM1 (): Buffer;
checkM2 (serverM2_buf: Buffer): void;
computeK (): Buffer;
computeM1(): Buffer;
checkM2(serverM2_buf: Buffer): void;
computeK(): Buffer;
}
export class Server {
constructor (
params: object
, verifier_buf: Buffer
, secret2_buf: Buffer);
constructor(params: object, verifier_buf: Buffer, secret2_buf: Buffer);
computeB (): Buffer;
setA (A_buf: Buffer): void;
checkM1 (clientM1_buf: Buffer): Buffer;
computeK (): Buffer;
computeB(): Buffer;
setA(A_buf: Buffer): void;
checkM1(clientM1_buf: Buffer): Buffer;
computeK(): Buffer;
}
}

View File

@@ -14,7 +14,6 @@ import fetch, { Headers } from "node-fetch";
import nacl from "tweetnacl";
import bplist from "./bplist";
const AIRPLAY_PORT = 7000;
const MIMETYPE_BPLIST = "application/x-apple-binary-plist";
@@ -27,10 +26,10 @@ export class AirPlayAuthCredentials {
public clientPk: Uint8Array;
constructor(
clientId?: string
, clientSk?: Uint8Array
, clientPk?: Uint8Array) {
clientId?: string,
clientSk?: Uint8Array,
clientPk?: Uint8Array
) {
if (clientId && clientSk && clientPk) {
this.clientId = clientId;
this.clientSk = clientSk;
@@ -74,8 +73,7 @@ export class AirPlayAuth {
*/
public async finishPairing(pin: string) {
// Stage 1 response
const { pk: serverPk
, salt: serverSalt } = await this.pairSetupPin1();
const { pk: serverPk, salt: serverSalt } = await this.pairSetupPin1();
// SRP params must 2048-bit SHA1
const srpParams = srp6a.params[2048];
@@ -83,19 +81,21 @@ export class AirPlayAuth {
// Create SRP client
const srpClient = new srp6a.Client(
srpParams // Params
, serverSalt // Receiver salt
, Buffer.from(this.credentials.clientId) // Username
, Buffer.from(pin) // Password (receiver pin)
, Buffer.from(this.credentials.clientSk)); // Client secret key
srpParams, // Params
serverSalt, // Receiver salt
Buffer.from(this.credentials.clientId), // Username
Buffer.from(pin), // Password (receiver pin)
Buffer.from(this.credentials.clientSk) // Client secret key
);
// Add receiver's public key
srpClient.setB(serverPk);
// Stage 2 response
await this.pairSetupPin2(
srpClient.computeA() // SRP public key
, srpClient.computeM1()); // SRP proof
srpClient.computeA(), // SRP public key
srpClient.computeM1() // SRP proof
);
// Stage 3 response
await this.pairSetupPin3(srpClient.computeK());
@@ -108,12 +108,10 @@ export class AirPlayAuth {
* its public key / salt.
*/
public async pairSetupPin1(): Promise<any> {
const [ response ] = await this.sendPostRequestBplist(
"/pair-setup-pin"
, {
method: "pin"
, user: this.credentials.clientId
});
const [response] = await this.sendPostRequestBplist("/pair-setup-pin", {
method: "pin",
user: this.credentials.clientId
});
return response;
}
@@ -125,13 +123,11 @@ export class AirPlayAuth {
* public keys, sending them to the receiver and receiving its
* proof.
*/
public async pairSetupPin2(
pk: Buffer
, proof: Buffer): Promise<any> {
const [ response ] = await this.sendPostRequestBplist(
"/pair-setup-pin"
, { pk, proof });
public async pairSetupPin2(pk: Buffer, proof: Buffer): Promise<any> {
const [response] = await this.sendPostRequestBplist("/pair-setup-pin", {
pk,
proof
});
return response;
}
@@ -144,17 +140,19 @@ export class AirPlayAuth {
* responds confirming the pairing is complete.
*/
public async pairSetupPin3(
sharedSecretHash: crypto.BinaryLike): Promise<any> {
sharedSecretHash: crypto.BinaryLike
): Promise<any> {
// Create AES key
const aesKey = crypto.createHash("sha512")
const aesKey = crypto
.createHash("sha512")
.update("Pair-Setup-AES-Key")
.update(sharedSecretHash)
.digest()
.slice(0, 16);
// Create AES IV
const aesIv = crypto.createHash("sha512")
const aesIv = crypto
.createHash("sha512")
.update("Pair-Setup-AES-IV")
.update(sharedSecretHash)
.digest()
@@ -162,7 +160,6 @@ export class AirPlayAuth {
aesIv[15]++;
const cipher = crypto.createCipheriv("aes-128-gcm", aesKey, aesIv);
// Encode client public key
@@ -170,23 +167,23 @@ export class AirPlayAuth {
cipher.final();
const authTag = cipher.getAuthTag();
const [ response ] = await this.sendPostRequestBplist(
"/pair-setup-pin"
, { epk, authTag });
const [response] = await this.sendPostRequestBplist("/pair-setup-pin", {
epk,
authTag
});
return response;
}
/**
* Sends a POST request to receiver and returns the
* response.
*/
public async sendPostRequest(
path: string
, contentType?: string
, data?: Buffer | string): Promise<any> {
path: string,
contentType?: string,
data?: Buffer | string
): Promise<any> {
// Create URL from base receiver URL and path
const requestUrl = new URL(path, this.baseUrl);
@@ -200,9 +197,9 @@ export class AirPlayAuth {
}
const response = await fetch(requestUrl.href, {
method: "POST"
, headers: requestHeaders
, body: data
method: "POST",
headers: requestHeaders,
body: data
});
if (!response.ok) {
@@ -217,16 +214,17 @@ export class AirPlayAuth {
* receiver, then decodes and returns the response.
*/
public async sendPostRequestBplist(
path: string
, data?: object): Promise<any> {
path: string,
data?: object
): Promise<any> {
// Convert data to compatible type
const requestBody = data
? bplist.create(data)
: undefined;
const requestBody = data ? bplist.create(data) : undefined;
const response = await this.sendPostRequest(
path, MIMETYPE_BPLIST, requestBody);
path,
MIMETYPE_BPLIST,
requestBody
);
// Convert response data to Buffer for bplist-parser
return bplist.parse.parseBuffer(response);

View File

@@ -7,14 +7,12 @@ import { sendMessage } from "../../lib/nativeMessaging";
import { ReceiverDevice } from "../../types";
import { ReceiverApplication, ReceiverMessage, SenderMessage } from "./types";
export const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection";
export const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
export const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
const HEARTBEAT_INTERVAL = 5000;
class CastClient {
protected client = new Client();
@@ -22,18 +20,26 @@ class CastClient {
protected heartbeatChannel?: Channel;
protected heartbeatIntervalId?: NodeJS.Timeout;
constructor(protected sourceId = "sender-0"
, protected destinationId = "receiver-0") {}
constructor(
protected sourceId = "sender-0",
protected destinationId = "receiver-0"
) {}
/**
* Create a channel on the client connection with a given
* namespace.
*/
createChannel(namespace: string
, sourceId = this.sourceId
, destinationId = this.destinationId) {
return this.client.createChannel(sourceId, destinationId, namespace, "JSON");
createChannel(
namespace: string,
sourceId = this.sourceId,
destinationId = this.destinationId
) {
return this.client.createChannel(
sourceId,
destinationId,
namespace,
"JSON"
);
}
connect(host: string, port: number, onHeartbeat?: () => void) {
@@ -49,10 +55,10 @@ class CastClient {
this.client.connect({ host, port }, () => {
this.connectionChannel = this.createChannel(NS_CONNECTION);
this.heartbeatChannel = this.createChannel(NS_HEARTBEAT);
this.connectionChannel.send({ type: "CONNECT" });
this.heartbeatChannel.send({ type: "PING" });
this.heartbeatIntervalId = setInterval(() => {
this.heartbeatChannel?.send({ type: "PING" });
if (onHeartbeat) {
@@ -66,7 +72,6 @@ class CastClient {
}
}
type OnSessionCreatedCallback = (sessionId: string) => void;
export default class Session extends CastClient {
@@ -81,7 +86,7 @@ export default class Session extends CastClient {
private transportId?: string;
private transportConnection?: Channel;
private transportHeartbeat?: Channel;
// Channels created by `sendCastSessionMessage` messages
private namespaceChannelMap = new Map<string, Channel>();
@@ -93,12 +98,17 @@ export default class Session extends CastClient {
private onSessionCreated?: OnSessionCreatedCallback;
private establishAppConnection(transportId: string) {
this.transportConnection = this.createChannel(
NS_CONNECTION, this.sourceId, transportId);
NS_CONNECTION,
this.sourceId,
transportId
);
this.transportHeartbeat = this.createChannel(
NS_HEARTBEAT, this.sourceId, transportId);
NS_HEARTBEAT,
this.sourceId,
transportId
);
this.transportConnection.send({ type: "CONNECT" });
}
@@ -111,7 +121,8 @@ export default class Session extends CastClient {
case "RECEIVER_STATUS": {
const { status } = message;
const application = status.applications?.find(
app => app.appId === this.appId);
app => app.appId === this.appId
);
/**
* If application isn't set, still waiting on the launch
@@ -133,20 +144,20 @@ export default class Session extends CastClient {
const { friendlyName } = this.receiverDevice;
sendMessage({
subject: "shim:castSessionCreated"
, data: {
sessionId: this.sessionId
, statusText: application.statusText
, namespaces: application.namespaces
, volume: status.volume
, appId: application.appId
, displayName: application.displayName
, receiverFriendlyName: friendlyName
, transportId: this.sessionId
subject: "shim:castSessionCreated",
data: {
sessionId: this.sessionId,
statusText: application.statusText,
namespaces: application.namespaces,
volume: status.volume,
appId: application.appId,
displayName: application.displayName,
receiverFriendlyName: friendlyName,
transportId: this.sessionId,
// TODO: Fix this
, senderApps: []
, appImages: []
// TODO: Fix this
senderApps: [],
appImages: []
}
});
}
@@ -161,15 +172,15 @@ export default class Session extends CastClient {
}
sendMessage({
subject: "shim:castSessionUpdated"
, data: {
sessionId: this.sessionId
, statusText: application.statusText
, namespaces: application.namespaces
, volume: message.status.volume
subject: "shim:castSessionUpdated",
data: {
sessionId: this.sessionId,
statusText: application.statusText,
namespaces: application.namespaces,
volume: message.status.volume
}
});
break;
}
@@ -179,13 +190,16 @@ export default class Session extends CastClient {
break;
}
}
}
};
sendMessage(namespace: string, message: unknown) {
let channel = this.namespaceChannelMap.get(namespace);
if (!channel) {
channel = this.createChannel(
namespace, this.sourceId, this.transportId);
namespace,
this.sourceId,
this.transportId
);
channel.on("message", messageData => {
if (!this.sessionId) {
@@ -195,11 +209,11 @@ export default class Session extends CastClient {
messageData = JSON.stringify(messageData);
sendMessage({
subject: "shim:receivedCastSessionMessage"
, data: {
sessionId: this.sessionId
, namespace
, messageData
subject: "shim:receivedCastSessionMessage",
data: {
sessionId: this.sessionId,
namespace,
messageData
}
});
});
@@ -222,25 +236,24 @@ export default class Session extends CastClient {
return requestId;
}
constructor(public appId: string
, public receiverDevice: ReceiverDevice) {
constructor(public appId: string, public receiverDevice: ReceiverDevice) {
super();
this.client.on("close", () => {
if (this.sessionId) {
sendMessage({
subject: "shim:castSessionStopped"
, data: { sessionId: this.sessionId }
subject: "shim:castSessionStopped",
data: { sessionId: this.sessionId }
});
}
});
}
async connect(host: string
, port: number
, onSessionCreated?: OnSessionCreatedCallback) {
async connect(
host: string,
port: number,
onSessionCreated?: OnSessionCreatedCallback
) {
if (onSessionCreated) {
this.onSessionCreated = onSessionCreated;
}
@@ -253,8 +266,8 @@ export default class Session extends CastClient {
});
this.launchRequestId = this.sendReceiverMessage({
type: "LAUNCH"
, appId: this.appId
type: "LAUNCH",
appId: this.appId
});
}
}

View File

@@ -5,9 +5,7 @@ import castv2 from "castv2";
import { sendMessage } from "../../lib/nativeMessaging";
import { Message } from "../../messaging";
import Session, { NS_CONNECTION
, NS_RECEIVER } from "./Session";
import Session, { NS_CONNECTION, NS_RECEIVER } from "./Session";
const sessions = new Map<string, Session>();
@@ -19,9 +17,12 @@ export function handleCastMessage(message: Message) {
// Connect and store with returned ID
const session = new Session(appId, receiverDevice);
session.connect(
receiverDevice.host, receiverDevice.port, sessionId => {
sessions.set(sessionId, session);
});
receiverDevice.host,
receiverDevice.port,
sessionId => {
sessions.set(sessionId, session);
}
);
break;
}
@@ -32,10 +33,11 @@ export function handleCastMessage(message: Message) {
const session = sessions.get(sessionId);
if (!session) {
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: {
error: "Session does not exist"
, sessionId, messageId
subject: "shim:impl_sendCastMessage",
data: {
error: "Session does not exist",
sessionId,
messageId
}
});
@@ -46,10 +48,11 @@ export function handleCastMessage(message: Message) {
session.sendReceiverMessage(messageData);
} catch (err) {
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: {
error: `Failed to send message (${err})`
, sessionId, messageId
subject: "shim:impl_sendCastMessage",
data: {
error: `Failed to send message (${err})`,
sessionId,
messageId
}
});
@@ -58,8 +61,8 @@ export function handleCastMessage(message: Message) {
// Success
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: { sessionId, messageId }
subject: "shim:impl_sendCastMessage",
data: { sessionId, messageId }
});
break;
@@ -71,10 +74,11 @@ export function handleCastMessage(message: Message) {
const session = sessions.get(sessionId);
if (!session) {
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: {
error: "Session does not exist"
, sessionId, messageId
subject: "shim:impl_sendCastMessage",
data: {
error: "Session does not exist",
sessionId,
messageId
}
});
@@ -91,10 +95,11 @@ export function handleCastMessage(message: Message) {
session.sendMessage(namespace, messageData);
} catch (err) {
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: {
error: `Failed to send message (${err})`
, sessionId, messageId
subject: "shim:impl_sendCastMessage",
data: {
error: `Failed to send message (${err})`,
sessionId,
messageId
}
});
@@ -103,8 +108,8 @@ export function handleCastMessage(message: Message) {
// Success
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: { sessionId, messageId }
subject: "shim:impl_sendCastMessage",
data: { sessionId, messageId }
});
break;
@@ -117,12 +122,20 @@ export function handleCastMessage(message: Message) {
client.connect({ host, port }, () => {
const sourceId = "sender-0";
const destinationId = "receiver-0";
const clientConnection = client.createChannel(
sourceId, destinationId, NS_CONNECTION, "JSON");
sourceId,
destinationId,
NS_CONNECTION,
"JSON"
);
const clientReceiver = client.createChannel(
sourceId, destinationId, NS_RECEIVER, "JSON");
sourceId,
destinationId,
NS_RECEIVER,
"JSON"
);
clientConnection.send({ type: "CONNECT" });
clientReceiver.send({ type: "STOP", requestId: 1 });
});

View File

@@ -7,18 +7,18 @@ export interface Image {
}
enum Capability {
VIDEO_OUT = "video_out"
, AUDIO_OUT = "audio_out"
, VIDEO_IN = "video_in"
, AUDIO_IN = "audio_in"
, MULTIZONE_GROUP = "multizone_group"
VIDEO_OUT = "video_out",
AUDIO_OUT = "audio_out",
VIDEO_IN = "video_in",
AUDIO_IN = "audio_in",
MULTIZONE_GROUP = "multizone_group"
}
enum ReceiverType {
CAST = "cast"
, DIAL = "dial"
, HANGOUT = "hangout"
, CUSTOM = "custom"
CAST = "cast",
DIAL = "dial",
HANGOUT = "hangout",
CUSTOM = "custom"
}
export interface SenderApplication {
@@ -28,9 +28,9 @@ export interface SenderApplication {
}
enum VolumeControlType {
ATTENUATION = "attenuation"
, FIXED = "fixed"
, MASTER = "master"
ATTENUATION = "attenuation",
FIXED = "fixed",
MASTER = "master"
}
export interface Volume {
@@ -43,75 +43,74 @@ export interface Volume {
// Media
enum IdleReason {
CANCELLED = "CANCELLED"
, INTERRUPTED = "INTERRUPTED"
, FINISHED = "FINISHED"
, ERROR = "ERROR"
CANCELLED = "CANCELLED",
INTERRUPTED = "INTERRUPTED",
FINISHED = "FINISHED",
ERROR = "ERROR"
}
enum HlsSegmentFormat {
AAC = "aac"
, AC3 = "ac3"
, MP3 = "mp3"
, TS = "ts"
, TS_AAC = "ts_aac"
, E_AC3 = "e_ac3"
, FMP4 = "fmp4"
AAC = "aac",
AC3 = "ac3",
MP3 = "mp3",
TS = "ts",
TS_AAC = "ts_aac",
E_AC3 = "e_ac3",
FMP4 = "fmp4"
}
export enum HlsVideoSegmentFormat {
MPEG2_TS = "mpeg2_ts"
, FMP4 = "fmp4"
MPEG2_TS = "mpeg2_ts",
FMP4 = "fmp4"
}
enum MetadataType {
GENERIC
, MOVIE
, TV_SHOW
, MUSIC_TRACK
, PHOTO
, AUDIOBOOK_CHAPTER
GENERIC,
MOVIE,
TV_SHOW,
MUSIC_TRACK,
PHOTO,
AUDIOBOOK_CHAPTER
}
enum PlayerState {
IDLE = "IDLE"
, PLAYING = "PLAYING"
, PAUSED = "PAUSED"
, BUFFERING = "BUFFERING"
IDLE = "IDLE",
PLAYING = "PLAYING",
PAUSED = "PAUSED",
BUFFERING = "BUFFERING"
}
enum RepeatMode {
OFF = "REPEAT_OFF"
, ALL = "REPEAT_ALL"
, SINGLE = "REPEAT_SINGLE"
, ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
OFF = "REPEAT_OFF",
ALL = "REPEAT_ALL",
SINGLE = "REPEAT_SINGLE",
ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
}
enum ResumeState {
PLAYBACK_START = "PLAYBACK_START"
, PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
PLAYBACK_START = "PLAYBACK_START",
PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
}
enum StreamType {
BUFFERED = "BUFFERED"
, LIVE = "LIVE"
, OTHER = "OTHER"
BUFFERED = "BUFFERED",
LIVE = "LIVE",
OTHER = "OTHER"
}
enum TrackType {
TEXT = "TEXT"
, AUDIO = "AUDIO"
, VIDEO = "VIDEO"
TEXT = "TEXT",
AUDIO = "AUDIO",
VIDEO = "VIDEO"
}
export enum UserAction {
LIKE = "LIKE"
, DISLIKE = "DISLIKE"
, FOLLOW = "FOLLOW"
, UNFOLLOW = "UNFOLLOW"
LIKE = "LIKE",
DISLIKE = "DISLIKE",
FOLLOW = "FOLLOW",
UNFOLLOW = "UNFOLLOW"
}
interface Break {
breakClipIds: string[];
duration?: number;
@@ -173,11 +172,11 @@ interface VastAdsRequest {
}
type Metadata =
GenericMediaMetadata
| MovieMediaMetadata
| MusicTrackMediaMetadata
| PhotoMediaMetadata
| TvShowMediaMetadata;
| GenericMediaMetadata
| MovieMediaMetadata
| MusicTrackMediaMetadata
| PhotoMediaMetadata
| TvShowMediaMetadata;
interface MediaInformation {
atvEntity?: string;
@@ -284,11 +283,11 @@ export interface MediaStatus {
playbackRate: number;
playerState: PlayerState;
idleReason?: IdleReason;
items?: QueueItem[]
items?: QueueItem[];
currentTime: number;
supportedMediaCommands: number;
repeatMode: RepeatMode;
volume: Volume
volume: Volume;
customData: unknown;
}
@@ -329,23 +328,21 @@ export interface ReceiverStatus {
volume: Volume;
}
interface ReqBase {
requestId: number;
}
// NS: urn:x-cast:com.google.cast.receiver
export type SenderMessage =
ReqBase & { type: "LAUNCH", appId: string }
| ReqBase & { type: "STOP", sessionId: string }
| ReqBase & { type: "GET_STATUS" }
| ReqBase & { type: "GET_APP_AVAILABILITY", appId: string[] }
| ReqBase & { type: "SET_VOLUME", volume: Volume };
| (ReqBase & { type: "LAUNCH"; appId: string })
| (ReqBase & { type: "STOP"; sessionId: string })
| (ReqBase & { type: "GET_STATUS" })
| (ReqBase & { type: "GET_APP_AVAILABILITY"; appId: string[] })
| (ReqBase & { type: "SET_VOLUME"; volume: Volume });
export type ReceiverMessage =
ReqBase & { type: "RECEIVER_STATUS", status: ReceiverStatus }
| ReqBase & { type: "LAUNCH_ERROR", reason: string }
| (ReqBase & { type: "RECEIVER_STATUS"; status: ReceiverStatus })
| (ReqBase & { type: "LAUNCH_ERROR"; reason: string });
interface MediaReqBase extends ReqBase {
mediaSessionId: number;
@@ -354,84 +351,84 @@ interface MediaReqBase extends ReqBase {
// NS: urn:x-cast:com.google.cast.media
export type SenderMediaMessage =
| MediaReqBase & { type: "PLAY" }
| MediaReqBase & { type: "PAUSE" }
| MediaReqBase & { type: "MEDIA_GET_STATUS" }
| MediaReqBase & { type: "STOP" }
| MediaReqBase & { type: "MEDIA_SET_VOLUME", volume: Volume }
| MediaReqBase & { type: "SET_PLAYBACK_RATE" , playbackRate: number }
| ReqBase & {
type: "LOAD"
, activeTrackIds: Nullable<number[]>
, atvCredentials?: string
, atvCredentialsType?: string
, autoplay: Nullable<boolean>
, currentTime: Nullable<number>
, customData?: unknown
, media: MediaInformation
, sessionId: Nullable<string>
}
| MediaReqBase & {
type: "SEEK"
, resumeState: Nullable<ResumeState>
, currentTime: Nullable<number>
}
| MediaReqBase & {
type: "EDIT_TRACKS_INFO"
, activeTrackIds: Nullable<number[]>
, textTrackStyle: Nullable<string>
}
// QueueLoadRequest
| MediaReqBase & {
type: "QUEUE_LOAD"
, items: QueueItem[]
, startIndex: number
, repeatMode: string
, sessionId: Nullable<string>
}
// QueueInsertItemsRequest
| MediaReqBase & {
type: "QUEUE_INSERT"
, items: QueueItem[]
, insertBefore: Nullable<number>
, sessionId: Nullable<string>
}
// QueueUpdateItemsRequest
| MediaReqBase & {
type: "QUEUE_UPDATE"
, items: QueueItem[]
, sessionId: Nullable<string>
}
// QueueJumpRequest
| MediaReqBase & {
type: "QUEUE_UPDATE"
, jump: Nullable<number>
, currentItemId: Nullable<number>
, sessionId: Nullable<string>
}
// QueueRemoveItemsRequest
| MediaReqBase & {
type: "QUEUE_REMOVE"
, itemIds: number[]
, sessionId: Nullable<string>
}
// QueueReorderItemsRequest
| MediaReqBase & {
type: "QUEUE_REORDER"
, itemIds: number[]
, insertBefore: Nullable<number>
, sessionId: Nullable<string>
}
// QueueSetPropertiesRequest
| MediaReqBase & {
type: "QUEUE_UPDATE"
, repeatMode: Nullable<string>
, sessionId: Nullable<string>
};
| (MediaReqBase & { type: "PLAY" })
| (MediaReqBase & { type: "PAUSE" })
| (MediaReqBase & { type: "MEDIA_GET_STATUS" })
| (MediaReqBase & { type: "STOP" })
| (MediaReqBase & { type: "MEDIA_SET_VOLUME"; volume: Volume })
| (MediaReqBase & { type: "SET_PLAYBACK_RATE"; playbackRate: number })
| (ReqBase & {
type: "LOAD";
activeTrackIds: Nullable<number[]>;
atvCredentials?: string;
atvCredentialsType?: string;
autoplay: Nullable<boolean>;
currentTime: Nullable<number>;
customData?: unknown;
media: MediaInformation;
sessionId: Nullable<string>;
})
| (MediaReqBase & {
type: "SEEK";
resumeState: Nullable<ResumeState>;
currentTime: Nullable<number>;
})
| (MediaReqBase & {
type: "EDIT_TRACKS_INFO";
activeTrackIds: Nullable<number[]>;
textTrackStyle: Nullable<string>;
})
// QueueLoadRequest
| (MediaReqBase & {
type: "QUEUE_LOAD";
items: QueueItem[];
startIndex: number;
repeatMode: string;
sessionId: Nullable<string>;
})
// QueueInsertItemsRequest
| (MediaReqBase & {
type: "QUEUE_INSERT";
items: QueueItem[];
insertBefore: Nullable<number>;
sessionId: Nullable<string>;
})
// QueueUpdateItemsRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
items: QueueItem[];
sessionId: Nullable<string>;
})
// QueueJumpRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
jump: Nullable<number>;
currentItemId: Nullable<number>;
sessionId: Nullable<string>;
})
// QueueRemoveItemsRequest
| (MediaReqBase & {
type: "QUEUE_REMOVE";
itemIds: number[];
sessionId: Nullable<string>;
})
// QueueReorderItemsRequest
| (MediaReqBase & {
type: "QUEUE_REORDER";
itemIds: number[];
insertBefore: Nullable<number>;
sessionId: Nullable<string>;
})
// QueueSetPropertiesRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
repeatMode: Nullable<string>;
sessionId: Nullable<string>;
});
export type ReceiverMediaMessage =
MediaReqBase & { type: "MEDIA_STATUS", status: MediaStatus[] }
| MediaReqBase & { type: "INVALID_PLAYER_STATE" }
| MediaReqBase & { type: "LOAD_FAILED" }
| MediaReqBase & { type: "LOAD_CANCELLED" }
| MediaReqBase & { type: "INVALID_REQUEST" };
| (MediaReqBase & { type: "MEDIA_STATUS"; status: MediaStatus[] })
| (MediaReqBase & { type: "INVALID_PLAYER_STATE" })
| (MediaReqBase & { type: "LOAD_FAILED" })
| (MediaReqBase & { type: "LOAD_CANCELLED" })
| (MediaReqBase & { type: "INVALID_REQUEST" });

View File

@@ -9,25 +9,31 @@ import mdns from "mdns";
import { sendMessage } from "../lib/nativeMessaging";
import { ReceiverStatus } from "./cast/types";
import { NS_CONNECTION, NS_HEARTBEAT, NS_RECEIVER }
from "./cast/Session";
import { NS_CONNECTION, NS_HEARTBEAT, NS_RECEIVER } from "./cast/Session";
interface CastTxtRecord {
id: string; cd: string; rm: string;
ve: string; md: string; ic: string;
fn: string; ca: string; st: string;
bs: string; nf: string; rs: string;
id: string;
cd: string;
rm: string;
ve: string;
md: string;
ic: string;
fn: string;
ca: string;
st: string;
bs: string;
nf: string;
rs: string;
}
const browser = mdns.createBrowser(mdns.tcp("googlecast"), {
resolverSequence: [
mdns.rst.DNSServiceResolve()
, "DNSServiceGetAddrInfo" in mdns.dns_sd
mdns.rst.DNSServiceResolve(),
"DNSServiceGetAddrInfo" in mdns.dns_sd
? mdns.rst.DNSServiceGetAddrInfo()
// Some issues on Linux with IPv6, so restrict to IPv4
: mdns.rst.getaddrinfo({ families: [ 4 ]})
, mdns.rst.makeAddressesUnique()
: // Some issues on Linux with IPv6, so restrict to IPv4
mdns.rst.getaddrinfo({ families: [4] }),
mdns.rst.makeAddressesUnique()
]
});
@@ -39,13 +45,13 @@ function onBrowserServiceUp(service: mdns.Service) {
const txtRecord = service.txtRecord as CastTxtRecord;
sendMessage({
subject: "main:receiverDeviceUp"
, data: {
subject: "main:receiverDeviceUp",
data: {
receiverDevice: {
host: service.addresses[0]
, port: service.port
, id: service.name
, friendlyName: txtRecord.fn
host: service.addresses[0],
port: service.port,
id: service.name,
friendlyName: txtRecord.fn
}
}
});
@@ -59,15 +65,14 @@ function onBrowserServiceDown(service: mdns.Service) {
const txtRecord = service.txtRecord as CastTxtRecord;
sendMessage({
subject: "main:receiverDeviceDown"
, data: { receiverDeviceId: service.name }
subject: "main:receiverDeviceDown",
data: { receiverDeviceId: service.name }
});
}
browser.on("serviceUp", onBrowserServiceUp);
browser.on("serviceDown", onBrowserServiceDown);
interface InitializeOptions {
shouldWatchStatus?: boolean;
}
@@ -88,8 +93,7 @@ export function startDiscovery(options: InitializeOptions) {
return;
}
const listener = new StatusListener(
service.addresses[0], service.port);
const listener = new StatusListener(service.addresses[0], service.port);
listener.on("receiverStatus", (status: ReceiverStatus) => {
if (!service.name) {
@@ -97,10 +101,10 @@ export function startDiscovery(options: InitializeOptions) {
}
sendMessage({
subject: "main:receiverDeviceUpdated"
, data: {
receiverDeviceId: service.name
, status
subject: "main:receiverDeviceUpdated",
data: {
receiverDeviceId: service.name,
status
}
});
});
@@ -122,12 +126,11 @@ export function stopDiscovery() {
browser.stop();
}
/**
* Creates a connection to a receiver device and forwards
* RECEIVER_STATUS updates to the extension.
*/
export default class StatusListener extends EventEmitter {
export default class StatusListener extends EventEmitter {
private client: Client;
private clientReceiver?: Channel;
private clientHeartbeatIntervalId?: NodeJS.Timeout;
@@ -160,17 +163,28 @@ export function stopDiscovery() {
this.client.close();
}
private onConnect(): void {
const sourceId = "sender-0";
const destinationId = "receiver-0";
const clientConnection = this.client.createChannel(
sourceId, destinationId, NS_CONNECTION, "JSON");
sourceId,
destinationId,
NS_CONNECTION,
"JSON"
);
const clientHeartbeat = this.client.createChannel(
sourceId, destinationId, NS_HEARTBEAT, "JSON");
sourceId,
destinationId,
NS_HEARTBEAT,
"JSON"
);
const clientReceiver = this.client.createChannel(
sourceId, destinationId, NS_RECEIVER, "JSON");
sourceId,
destinationId,
NS_RECEIVER,
"JSON"
);
clientReceiver.on("message", data => {
switch (data.type) {

View File

@@ -11,7 +11,6 @@ import mime from "mime-types";
import { sendMessage } from "../lib/nativeMessaging";
import { convertSrtToVtt } from "../lib/subtitles";
export let mediaServer: http.Server | undefined;
export async function startMediaServer(filePath: string, port: number) {
@@ -41,7 +40,7 @@ export async function startMediaServer(filePath: string, port: number) {
} catch (err) {
console.error("Error: Failed to find media path.");
sendMessage({
subject: "mediaCast:mediaServerError"
subject: "mediaCast:mediaServerError"
});
return;
@@ -63,15 +62,19 @@ export async function startMediaServer(filePath: string, port: number) {
*/
const subtitles = new Map<string, string>();
try {
const dirEntries = await fs.promises.readdir(
fileDir, { withFileTypes: true });
const dirEntries = await fs.promises.readdir(fileDir, {
withFileTypes: true
});
for (const dirEntry of dirEntries) {
if (dirEntry.isFile()
&& mime.lookup(dirEntry.name) === "application/x-subrip") {
subtitles.set(dirEntry.name, await convertSrtToVtt(
path.join(fileDir, dirEntry.name)));
if (
dirEntry.isFile() &&
mime.lookup(dirEntry.name) === "application/x-subrip"
) {
subtitles.set(
dirEntry.name,
await convertSrtToVtt(path.join(fileDir, dirEntry.name))
);
}
}
} catch (err) {
@@ -96,22 +99,20 @@ export async function startMediaServer(filePath: string, port: number) {
if (range) {
const bounds = range.substring(6).split("-");
const start = parseInt(bounds[0]);
const end = bounds[1]
? parseInt(bounds[1]) : fileSize - 1;
const end = bounds[1] ? parseInt(bounds[1]) : fileSize - 1;
res.writeHead(206, {
"Accept-Ranges": "bytes"
, "Content-Range": `bytes ${start}-${end}/${fileSize}`
, "Content-Length": (end - start) + 1
, "Content-Type": contentType
"Accept-Ranges": "bytes",
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
"Content-Length": end - start + 1,
"Content-Type": contentType
});
fs.createReadStream(
filePath, { start, end }).pipe(res);
fs.createReadStream(filePath, { start, end }).pipe(res);
} else {
res.writeHead(200, {
"Content-Length": fileSize
, "Content-Type": contentType
"Content-Length": fileSize,
"Content-Type": contentType
});
fs.createReadStream(filePath).pipe(res);
@@ -139,7 +140,8 @@ export async function startMediaServer(filePath: string, port: number) {
const ifaces = Object.values(os.networkInterfaces());
for (const iface of ifaces) {
const matchingIface = iface?.find(
details => details.family === "IPv4" && !details.internal);
details => details.family === "IPv4" && !details.internal
);
if (matchingIface) {
localAddress = matchingIface.address;
}
@@ -155,21 +157,25 @@ export async function startMediaServer(filePath: string, port: number) {
}
sendMessage({
subject: "mediaCast:mediaServerStarted"
, data: {
mediaPath: fileName
, subtitlePaths: Array.from(subtitles.keys())
, localAddress
subject: "mediaCast:mediaServerStarted",
data: {
mediaPath: fileName,
subtitlePaths: Array.from(subtitles.keys()),
localAddress
}
});
});
mediaServer.on("close", () => sendMessage({
subject: "mediaCast:mediaServerStopped"
}));
mediaServer.on("error", () => sendMessage({
subject: "mediaCast:mediaServerError"
}));
mediaServer.on("close", () =>
sendMessage({
subject: "mediaCast:mediaServerStopped"
})
);
mediaServer.on("error", () =>
sendMessage({
subject: "mediaCast:mediaServerError"
})
);
mediaServer.listen(port);
}

View File

@@ -5,13 +5,11 @@ import path from "path";
import { sendMessage } from "../lib/nativeMessaging";
function fatal(message: string) {
console.error(message);
process.exit(1);
}
let selectorApp: child_process.ChildProcess | undefined;
let selectorAppOpen = false;
@@ -35,37 +33,39 @@ export function startReceiverSelector(data: string) {
selectorAppOpen = false;
}
const selectorPath = path.join(process.cwd()
, "fx_cast_selector.app/Contents/MacOS/fx_cast_selector");
selectorApp = child_process.spawn(selectorPath, [ data ]);
const selectorPath = path.join(
process.cwd(),
"fx_cast_selector.app/Contents/MacOS/fx_cast_selector"
);
selectorApp = child_process.spawn(selectorPath, [data]);
selectorAppOpen = true;
if (selectorApp.stdout) {
selectorApp.stdout.setEncoding("utf-8");
selectorApp.stdout.setEncoding("utf-8");
selectorApp.stdout.on("data", data => {
const jsonData = JSON.parse(data);
if (!jsonData.mediaType) {
sendMessage({
subject: "main:receiverSelector/stopped"
, data: jsonData
subject: "main:receiverSelector/stopped",
data: jsonData
});
return;
}
sendMessage({
subject: "main:receiverSelector/selected"
, data: jsonData
subject: "main:receiverSelector/selected",
data: jsonData
});
});
}
selectorApp.on("error", err => {
sendMessage({
subject: "main:receiverSelector/error"
, data: err.message
subject: "main:receiverSelector/error",
data: err.message
});
});

View File

@@ -6,19 +6,19 @@ import { Message } from "./messaging";
import { handleCastMessage } from "./components/cast";
import { startDiscovery, stopDiscovery } from "./components/discovery";
import { startMediaServer, stopMediaServer } from "./components/mediaServer";
import { startReceiverSelector, stopReceiverSelector }
from "./components/receiverSelector";
import {
startReceiverSelector,
stopReceiverSelector
} from "./components/receiverSelector";
import { __applicationName, __applicationVersion } from "../../package.json";
process.on("SIGTERM", () => {
stopDiscovery();
stopMediaServer();
stopReceiverSelector();
});
/**
* Handle incoming messages from the extension and forward
* them to the appropriate handlers.
@@ -41,10 +41,12 @@ decodeTransform.on("data", (message: Message) => {
// Receiver selector
case "bridge:openReceiverSelector": {
startReceiverSelector(message.data); break;
startReceiverSelector(message.data);
break;
}
case "bridge:closeReceiverSelector": {
stopReceiverSelector(); break;
stopReceiverSelector();
break;
}
// Media server

View File

@@ -3,7 +3,6 @@
import { DecodeTransform, EncodeTransform } from "../../transforms";
import { Message } from "../messaging";
export const decodeTransform = new DecodeTransform();
export const encodeTransform = new EncodeTransform();

View File

@@ -2,13 +2,11 @@
import fs from "fs";
/**
* Reads a SubRip file and outputs text content as WebVTT.
*/
export async function convertSrtToVtt(srtFilePath: string) {
const fileStream = fs.createReadStream(
srtFilePath, { encoding: "utf-8" });
const fileStream = fs.createReadStream(srtFilePath, { encoding: "utf-8" });
let fileContents = "";
for await (let chunk of fileStream) {
@@ -21,7 +19,6 @@ export async function convertSrtToVtt(srtFilePath: string) {
fileContents += chunk.replace(/$\r\n/gm, "\n");
}
let vttText = "WEBVTT\n";
/**
@@ -30,7 +27,8 @@ export async function convertSrtToVtt(srtFilePath: string) {
* (followed by a new line), then any text content until a blank
* line.
*/
const REGEX_CAPTION = /(?:(\d+)\n(\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}))\n((?:.+)\n?)*/g;
const REGEX_CAPTION =
/(?:(\d+)\n(\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}))\n((?:.+)\n?)*/g;
/**
* WebVTT is very similar to SubRip, the main differences being

View File

@@ -1,121 +1,110 @@
"use strict";
import { Image
, ReceiverStatus
, SenderApplication
, SenderMessage
, Volume } from "./components/cast/types";
import { ReceiverDevice
, ReceiverSelectionCast
, ReceiverSelectionStop } from "./types";
import {
Image,
ReceiverStatus,
SenderApplication,
SenderMessage,
Volume
} from "./components/cast/types";
import {
ReceiverDevice,
ReceiverSelectionCast,
ReceiverSelectionStop
} from "./types";
interface CastSessionUpdated {
sessionId: string
, statusText: string
, namespaces: Array<{ name: string }>
, volume: Volume
sessionId: string;
statusText: string;
namespaces: Array<{ name: string }>;
volume: Volume;
}
interface CastSessionCreated extends CastSessionUpdated {
appId: string
, appImages: Image[]
, displayName: string
, receiverFriendlyName: string
, senderApps: SenderApplication[]
, transportId: string
appId: string;
appImages: Image[];
displayName: string;
receiverFriendlyName: string;
senderApps: SenderApplication[];
transportId: string;
}
type MessageDefinitions = {
"shim:castSessionCreated": CastSessionCreated
, "shim:castSessionUpdated": CastSessionUpdated
, "shim:castSessionStopped": {
sessionId: string
}
, "shim:receivedCastSessionMessage": {
sessionId: string
, namespace: string
, messageData: string
}
, "shim:impl_sendCastMessage": {
sessionId: string
, messageId: string
, error?: string
}
, "bridge:createCastSession": {
appId: string
, receiverDevice: ReceiverDevice
}
, "bridge:sendCastReceiverMessage": {
sessionId: string
, messageData: SenderMessage
, messageId: string
}
, "bridge:sendCastSessionMessage": {
sessionId: string
, namespace: string
, messageData: object | string
, messageId: string
}
, "bridge:stopCastApp": { receiverDevice: ReceiverDevice }
"shim:castSessionCreated": CastSessionCreated;
"shim:castSessionUpdated": CastSessionUpdated;
"shim:castSessionStopped": {
sessionId: string;
};
"shim:receivedCastSessionMessage": {
sessionId: string;
namespace: string;
messageData: string;
};
"shim:impl_sendCastMessage": {
sessionId: string;
messageId: string;
error?: string;
};
"bridge:createCastSession": {
appId: string;
receiverDevice: ReceiverDevice;
};
"bridge:sendCastReceiverMessage": {
sessionId: string;
messageData: SenderMessage;
messageId: string;
};
"bridge:sendCastSessionMessage": {
sessionId: string;
namespace: string;
messageData: object | string;
messageId: string;
};
"bridge:stopCastApp": { receiverDevice: ReceiverDevice };
// Bridge messages
, "main:receiverSelector/selected": ReceiverSelectionCast
, "main:receiverSelector/stopped": ReceiverSelectionStop
, "main:receiverSelector/cancelled": {}
, "main:receiverSelector/error": string
"main:receiverSelector/selected": ReceiverSelectionCast;
"main:receiverSelector/stopped": ReceiverSelectionStop;
"main:receiverSelector/cancelled": {};
"main:receiverSelector/error": string;
/**
* getInfo uses the old :/ form for compat with old bridge
* versions.
*/
, "bridge:getInfo": string
, "bridge:/getInfo": string
, "bridge:startDiscovery": {
shouldWatchStatus: boolean
}
, "bridge:openReceiverSelector": string
, "bridge:closeReceiverSelector": {}
, "bridge:startMediaServer": {
filePath: string
, port: number
}
, "bridge:stopMediaServer": {}
, "mediaCast:mediaServerStarted": {
mediaPath: string
, subtitlePaths: string[]
, localAddress: string
}
, "mediaCast:mediaServerStopped": {}
, "mediaCast:mediaServerError": {}
, "main:serviceUp": ReceiverDevice
, "main:serviceDown": { id: string }
, "main:updateReceiverStatus": {
id: string
, status: ReceiverStatus
}
, "main:receiverDeviceUp": { receiverDevice: ReceiverDevice }
, "main:receiverDeviceDown": { receiverDeviceId: string }
, "main:receiverDeviceUpdated": {
receiverDeviceId: string
, status: ReceiverStatus
}
}
"bridge:getInfo": string;
"bridge:/getInfo": string;
"bridge:startDiscovery": {
shouldWatchStatus: boolean;
};
"bridge:openReceiverSelector": string;
"bridge:closeReceiverSelector": {};
"bridge:startMediaServer": {
filePath: string;
port: number;
};
"bridge:stopMediaServer": {};
"mediaCast:mediaServerStarted": {
mediaPath: string;
subtitlePaths: string[];
localAddress: string;
};
"mediaCast:mediaServerStopped": {};
"mediaCast:mediaServerError": {};
"main:serviceUp": ReceiverDevice;
"main:serviceDown": { id: string };
"main:updateReceiverStatus": {
id: string;
status: ReceiverStatus;
};
"main:receiverDeviceUp": { receiverDevice: ReceiverDevice };
"main:receiverDeviceDown": { receiverDeviceId: string };
"main:receiverDeviceUpdated": {
receiverDeviceId: string;
status: ReceiverStatus;
};
};
interface MessageBase<K extends keyof MessageDefinitions> {
subject: K;
@@ -124,7 +113,7 @@ interface MessageBase<K extends keyof MessageDefinitions> {
type Messages = {
[K in keyof MessageDefinitions]: MessageBase<K>;
}
};
/**
* For better call semantics, make message data key optional if
@@ -137,5 +126,4 @@ type NarrowedMessage<L extends MessageBase<keyof MessageDefinitions>> =
: L
: never;
export type Message = NarrowedMessage<Messages[keyof Messages]>;

View File

@@ -2,17 +2,16 @@
import { ReceiverStatus } from "./components/cast/types";
export enum ReceiverSelectorMediaType {
App = 1
, Tab = 2
, Screen = 4
, File = 8
App = 1,
Tab = 2,
Screen = 4,
File = 8
}
export enum ReceiverSelectionActionType {
Cast = 1
, Stop = 2
Cast = 1,
Stop = 2
}
export interface ReceiverSelectionCast {

View File

@@ -6,9 +6,7 @@ import { Readable } from "stream";
import minimist from "minimist";
import WebSocket from "ws";
import { DecodeTransform
, EncodeTransform } from "./transforms";
import { DecodeTransform, EncodeTransform } from "./transforms";
export function init(port: number) {
process.stdout.write("Starting WebSocket server... ");
@@ -18,13 +16,12 @@ export function init(port: number) {
console.log("Done!");
});
wss.on("error", (err) => {
wss.on("error", err => {
// eslint-disable-next-line no-console
console.log("Failed!");
console.error(err);
});
wss.on("connection", socket => {
// Stream for incoming WebSocket messages
const messageStream = new Readable({ objectMode: true });
@@ -35,27 +32,22 @@ export function init(port: number) {
messageStream.push(JSON.parse(message));
});
/**
* Daemon and bridge are the same binary, so spawn a new
* version of self in bridge mode.
*/
const bridge = spawn(process.execPath, [ process.argv[1] ]);
const bridge = spawn(process.execPath, [process.argv[1]]);
// socket -> bridge.stdin
messageStream
.pipe(new EncodeTransform())
.pipe(bridge.stdin);
messageStream.pipe(new EncodeTransform()).pipe(bridge.stdin);
// bridge.stdout -> socket
bridge.stdout
.pipe(new DecodeTransform())
.on("data", data => {
// Socket can be CLOSING here
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data));
}
});
bridge.stdout.pipe(new DecodeTransform()).on("data", data => {
// Socket can be CLOSING here
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data));
}
});
// Handle termination
socket.on("close", () => bridge.kill());

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

@@ -1,6 +1,5 @@
declare type Nullable<T> = T | null;
declare type DistributiveOmit<T, K extends keyof any> =
T extends any
? Omit<T, K>
: never;
declare type DistributiveOmit<T, K extends keyof any> = T extends any
? Omit<T, K>
: never;

View File

@@ -5,29 +5,28 @@ import minimist from "minimist";
import { __applicationVersion } from "../package.json";
const argv = minimist(process.argv.slice(2), {
boolean: [ "daemon", "help", "version" ]
, string: [ "__name", "port" ]
, alias: {
d: "daemon"
, h: "help"
, v: "version"
, p: "port"
}
, default: {
__name: path.basename(process.argv[0])
, daemon: false
, port: "9556"
boolean: ["daemon", "help", "version"],
string: ["__name", "port"],
alias: {
d: "daemon",
h: "help",
v: "version",
p: "port"
},
default: {
__name: path.basename(process.argv[0]),
daemon: false,
port: "9556"
}
});
if (argv.version) {
// eslint-disable-next-line no-console
console.log(`v${__applicationVersion}`);
} else if (argv.help) {
// eslint-disable-next-line no-console
console.log(
`Usage: ${argv.__name} [options]
`Usage: ${argv.__name} [options]
Options:
-h, --help Print usage info
@@ -37,8 +36,8 @@ Options:
options.
-p, --port Set port number for WebSocket server. This must match the
port set in the extension options.
`);
`
);
} else if (argv.daemon) {
const port = parseInt(argv.port);
if (!port || port < 1025 || port > 65535) {
@@ -46,10 +45,9 @@ Options:
process.exit(1);
}
import("./daemon")
.then(daemon => {
daemon.init(port);
});
import("./daemon").then(daemon => {
daemon.init(port);
});
} else {
import("./bridge");
}

View File

@@ -3,7 +3,6 @@
import { Transform } from "stream";
import { Message } from "./bridge/messaging";
type ResponseHandlerFunction = (message: Message) => Promise<any>;
/**
@@ -13,29 +12,27 @@ type ResponseHandlerFunction = (message: Message) => Promise<any>;
export class ResponseTransform extends Transform {
constructor(private _handler: ResponseHandlerFunction) {
super({
readableObjectMode: true
, writableObjectMode: true
readableObjectMode: true,
writableObjectMode: true
});
}
public _transform(
chunk: Message
, _encoding: string
// tslint:disable-next-line:ban-types
, callback: Function) {
Promise.resolve(this._handler(chunk))
.then(res => {
if (res) {
callback(null, res);
} else {
callback(null);
}
});
chunk: Message,
_encoding: string,
// tslint:disable-next-line:ban-types
callback: Function
) {
Promise.resolve(this._handler(chunk)).then(res => {
if (res) {
callback(null, res);
} else {
callback(null);
}
});
}
}
/**
* Takes input, decodes the message string, parses as JSON
* and outputs the parsed result.
@@ -52,53 +49,52 @@ export class DecodeTransform extends Transform {
}
public _transform(
chunk: any
, _encoding: string
// tslint:disable-next-line:ban-types
, callback: Function) {
chunk: any,
_encoding: string,
// tslint:disable-next-line:ban-types
callback: Function
) {
// Append next chunk to buffer
this._messageBuffer = Buffer.concat([this._messageBuffer, chunk]);
// Append next chunk to buffer
this._messageBuffer = Buffer.concat([
this._messageBuffer
, chunk
]);
for (;;) {
if (this._messageLength === undefined) {
if (this._messageBuffer.length >= 4) {
// Read message length and offset buffer
this._messageLength = this._messageBuffer.readUInt32LE(0);
this._messageBuffer = this._messageBuffer.slice(4);
for (;;) {
if (this._messageLength === undefined) {
if (this._messageBuffer.length >= 4) {
// Read message length and offset buffer
this._messageLength = this._messageBuffer.readUInt32LE(0);
this._messageBuffer = this._messageBuffer.slice(4);
// Next message chunk
continue;
}
} else {
if (this._messageBuffer.length >= this._messageLength) {
const message = JSON.parse(
this._messageBuffer
.slice(0, this._messageLength)
.toString()
);
// Next message chunk
continue;
}
} else {
if (this._messageBuffer.length >= this._messageLength) {
const message = JSON.parse(this._messageBuffer
.slice(0, this._messageLength)
.toString());
// Push message content
this.push(message);
// Push message content
this.push(message);
// Offset buffer to start of next message
this._messageBuffer = this._messageBuffer.slice(
this._messageLength
);
this._messageLength = undefined;
// Offset buffer to start of next message
this._messageBuffer = this._messageBuffer.slice(
this._messageLength);
this._messageLength = undefined;
// Next message
continue;
}
}
// Next message
continue;
}
}
// No complete messages left
callback();
break;
}
// No complete messages left
callback();
break;
}
}
}
}
/**
* Takes input, encodes the message length and content and
@@ -112,11 +108,11 @@ export class EncodeTransform extends Transform {
}
public _transform(
chunk: any
, _encoding: string
// tslint:disable-next-line:ban-types
, callback: Function) {
chunk: any,
_encoding: string,
// tslint:disable-next-line:ban-types
callback: Function
) {
const messageLength = Buffer.alloc(4);
const message = Buffer.from(JSON.stringify(chunk));
@@ -124,9 +120,6 @@ export class EncodeTransform extends Transform {
messageLength.writeUInt32LE(message.length, 0);
// Output joined length and content
callback(null, Buffer.concat([
messageLength
, message
]));
callback(null, Buffer.concat([messageLength, message]));
}
}
}

View File

@@ -6,15 +6,15 @@ import logger from "../lib/logger";
import messaging, { Message, Port } from "../messaging";
import options from "../lib/options";
import { ReceiverSelectionActionType
, ReceiverSelectorMediaType } from "./receiverSelector";
import {
ReceiverSelectionActionType,
ReceiverSelectorMediaType
} from "./receiverSelector";
import ReceiverSelectorManager
from "./receiverSelector/ReceiverSelectorManager";
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
import receiverDevices from "./receiverDevices";
type AnyPort = Port | MessagePort;
export interface Shim {
@@ -25,8 +25,7 @@ export interface Shim {
appId?: string;
}
export default new class ShimManager {
export default new (class ShimManager {
private activeShims = new Set<Shim>();
public async init() {
@@ -40,8 +39,8 @@ export default new class ShimManager {
receiverDevices.addEventListener("receiverDeviceUp", ev => {
for (const shim of this.activeShims) {
shim.contentPort.postMessage({
subject: "shim:serviceUp"
, data: { receiverDevice: ev.detail.receiverDevice }
subject: "shim:serviceUp",
data: { receiverDevice: ev.detail.receiverDevice }
});
}
});
@@ -49,8 +48,8 @@ export default new class ShimManager {
receiverDevices.addEventListener("receiverDeviceDown", ev => {
for (const shim of this.activeShims) {
shim.contentPort.postMessage({
subject: "shim:serviceDown"
, data: { receiverDeviceId: ev.detail.receiverDeviceId }
subject: "shim:serviceDown",
data: { receiverDeviceId: ev.detail.receiverDeviceId }
});
}
});
@@ -74,19 +73,19 @@ export default new class ShimManager {
: this.createShimFromContent(port));
shim.contentPort.postMessage({
subject: "shim:initialized"
, data: await bridge.getInfo()
subject: "shim:initialized",
data: await bridge.getInfo()
});
this.activeShims.add(shim);
}
private async createShimFromBackground(
contentPort: MessagePort): Promise<Shim> {
contentPort: MessagePort
): Promise<Shim> {
const shim: Shim = {
bridgePort: await bridge.connect()
, contentPort
bridgePort: await bridge.connect(),
contentPort
};
shim.bridgePort.onDisconnect.addListener(() => {
@@ -105,12 +104,14 @@ export default new class ShimManager {
return shim;
}
private async createShimFromContent(
contentPort: Port): Promise<Shim> {
if (contentPort.sender?.tab?.id === undefined
|| contentPort.sender?.frameId === undefined) {
throw logger.error("Content shim created with an invalid port context.");
private async createShimFromContent(contentPort: Port): Promise<Shim> {
if (
contentPort.sender?.tab?.id === undefined ||
contentPort.sender?.frameId === undefined
) {
throw logger.error(
"Content shim created with an invalid port context."
);
}
/**
@@ -118,20 +119,21 @@ export default new class ShimManager {
* tab/frame ID, disconnect it.
*/
for (const activeShim of this.activeShims) {
if (activeShim.contentTabId === contentPort.sender.tab.id
&& activeShim.contentFrameId === contentPort.sender.frameId) {
if (
activeShim.contentTabId === contentPort.sender.tab.id &&
activeShim.contentFrameId === contentPort.sender.frameId
) {
activeShim.bridgePort.disconnect();
}
}
const shim: Shim = {
bridgePort: await bridge.connect()
, contentPort
, contentTabId: contentPort.sender.tab.id
, contentFrameId: contentPort.sender.frameId
bridgePort: await bridge.connect(),
contentPort,
contentTabId: contentPort.sender.tab.id,
contentFrameId: contentPort.sender.frameId
};
const onContentPortMessage = (message: Message) => {
this.handleContentMessage(shim, message);
};
@@ -150,7 +152,6 @@ export default new class ShimManager {
this.activeShims.delete(shim);
};
shim.bridgePort.onDisconnect.addListener(onDisconnect);
shim.bridgePort.onMessage.addListener(onBridgePortMessage);
@@ -161,7 +162,7 @@ export default new class ShimManager {
}
private async handleContentMessage(shim: Shim, message: Message) {
const [ destination ] = message.subject.split(":");
const [destination] = message.subject.split(":");
if (destination === "bridge") {
shim.bridgePort.postMessage(message);
}
@@ -172,8 +173,8 @@ export default new class ShimManager {
for (const receiverDevice of receiverDevices.getDevices()) {
shim.contentPort.postMessage({
subject: "shim:serviceUp"
, data: { receiverDevice }
subject: "shim:serviceUp",
data: { receiverDevice }
});
}
@@ -181,15 +182,21 @@ export default new class ShimManager {
}
case "main:selectReceiver": {
if (shim.contentTabId === undefined
|| shim.contentFrameId === undefined) {
throw logger.error("Shim associated with content sender missing tab/frame ID");
if (
shim.contentTabId === undefined ||
shim.contentFrameId === undefined
) {
throw logger.error(
"Shim associated with content sender missing tab/frame ID"
);
}
try {
const selection =
await ReceiverSelectorManager.getSelection(
shim.contentTabId, shim.contentFrameId);
await ReceiverSelectorManager.getSelection(
shim.contentTabId,
shim.contentFrameId
);
// Handle cancellation
if (!selection) {
@@ -207,25 +214,26 @@ export default new class ShimManager {
* been changed, we need to cancel the current
* sender and switch it out for the right one.
*/
if (selection.mediaType !==
ReceiverSelectorMediaType.App) {
if (
selection.mediaType !==
ReceiverSelectorMediaType.App
) {
shim.contentPort.postMessage({
subject: "shim:selectReceiver/cancelled"
});
loadSender({
tabId: shim.contentTabId
, frameId: shim.contentFrameId
, selection
tabId: shim.contentTabId,
frameId: shim.contentFrameId,
selection
});
break;
}
shim.contentPort.postMessage({
subject: "shim:selectReceiver/selected"
, data: selection
subject: "shim:selectReceiver/selected",
data: selection
});
break;
@@ -233,8 +241,8 @@ export default new class ShimManager {
case ReceiverSelectionActionType.Stop: {
shim.contentPort.postMessage({
subject: "shim:selectReceiver/stopped"
, data: selection
subject: "shim:selectReceiver/stopped",
data: selection
});
break;
@@ -257,7 +265,8 @@ export default new class ShimManager {
case "main:sessionCreated": {
const selector = await ReceiverSelectorManager.getSelector();
const shouldClose = await options.get(
"receiverSelectorWaitForConnection");
"receiverSelectorWaitForConnection"
);
if (selector.isOpen && shouldClose) {
selector.close();
@@ -267,4 +276,4 @@ export default new class ShimManager {
}
}
}
};
})();

View File

@@ -7,8 +7,7 @@ import messaging from "../messaging";
import options from "../lib/options";
import bridge, { BridgeInfo } from "../lib/bridge";
import ReceiverSelectorManager
from "./receiverSelector/ReceiverSelectorManager";
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
import ShimManager from "./ShimManager";
@@ -17,10 +16,8 @@ import receiverDevices from "./receiverDevices";
import { initMenus } from "./menus";
import { initWhitelist } from "./whitelist";
const _ = browser.i18n.getMessage;
/**
* On install, set the default options before initializing the
* extension. On update, handle any unset values and set to
@@ -36,7 +33,7 @@ browser.runtime.onInstalled.addListener(async details => {
init();
break;
}
case "update": {
// Set new defaults
await options.update(defaultOptions);
@@ -45,7 +42,6 @@ browser.runtime.onInstalled.addListener(async details => {
}
});
/**
* Sets up media overlay content script and handles toggling
* on options change.
@@ -62,10 +58,10 @@ async function initMediaOverlay() {
try {
contentScript = await browser.contentScripts.register({
allFrames: true
, js: [{ file: "senders/media/overlay/overlayContentLoader.js" }]
, matches: [ "<all_urls>" ]
, runAt: "document_start"
allFrames: true,
js: [{ file: "senders/media/overlay/overlayContentLoader.js" }],
matches: ["<all_urls>"],
runAt: "document_start"
});
} catch (err) {
logger.error("Failed to register media overlay");
@@ -76,7 +72,6 @@ async function initMediaOverlay() {
await contentScript?.unregister();
}
registerMediaOverlayContentScript();
// Update if toggled
@@ -90,7 +85,6 @@ async function initMediaOverlay() {
});
}
/**
* Checks whether the bridge can be reached and is compatible
* with the current version of the extension. If not, triggers
@@ -113,10 +107,11 @@ async function notifyBridgeCompat() {
logger.info("... bridge incompatible!");
const updateNotificationId = await browser.notifications.create({
type: "basic"
, title: `${
_("extensionName")}${_("optionsBridgeIssueStatusTitle")}`
, message: info.isVersionOlder
type: "basic",
title: `${_("extensionName")}${_(
"optionsBridgeIssueStatusTitle"
)}`,
message: info.isVersionOlder
? _("optionsBridgeOlderAction")
: _("optionsBridgeNewerAction")
});
@@ -127,14 +122,12 @@ async function notifyBridgeCompat() {
}
browser.tabs.create({
url: `https://github.com/hensm/fx_cast/releases/tag/v${
info.expectedVersion}`
url: `https://github.com/hensm/fx_cast/releases/tag/v${info.expectedVersion}`
});
});
}
}
let isInitialized = false;
async function init() {
@@ -163,14 +156,13 @@ async function init() {
await initWhitelist();
await initMediaOverlay();
/**
* When the browser action is clicked, open a receiver
* selector and load a sender for the response. The
* mirroring sender is loaded into the current tab at the
* top-level frame.
*/
browser.browserAction.onClicked.addListener(async tab => {
browser.browserAction.onClicked.addListener(async tab => {
if (tab.id === undefined) {
throw logger.error("Tab ID not found in browser action handler.");
}
@@ -178,9 +170,9 @@ async function init() {
const selection = await ReceiverSelectorManager.getSelection(tab.id);
if (selection) {
loadSender({
tabId: tab.id
, frameId: 0
, selection
tabId: tab.id,
frameId: 0,
selection
});
}
});

View File

@@ -6,22 +6,21 @@ import options from "../lib/options";
import { stringify } from "../lib/utils";
import { ReceiverSelectionActionType
, ReceiverSelectorMediaType } from "./receiverSelector";
import {
ReceiverSelectionActionType,
ReceiverSelectorMediaType
} from "./receiverSelector";
import ReceiverSelectorManager
from "./receiverSelector/ReceiverSelectorManager";
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
const _ = browser.i18n.getMessage;
const URL_PATTERN_HTTP = "http://*/*";
const URL_PATTERN_HTTPS = "https://*/*";
const URL_PATTERN_FILE = "file://*/*";
const URL_PATTERNS_REMOTE = [ URL_PATTERN_HTTP, URL_PATTERN_HTTPS ];
const URL_PATTERNS_ALL = [ ...URL_PATTERNS_REMOTE, URL_PATTERN_FILE ];
const URL_PATTERNS_REMOTE = [URL_PATTERN_HTTP, URL_PATTERN_HTTPS];
const URL_PATTERNS_ALL = [...URL_PATTERNS_REMOTE, URL_PATTERN_FILE];
type MenuId = string | number;
@@ -39,44 +38,44 @@ export async function initMenus() {
// Global "Cast..." menu item
menuIdCast = browser.menus.create({
contexts: [ "browser_action", "page", "tools_menu" ]
, title: _("contextCast")
contexts: ["browser_action", "page", "tools_menu"],
title: _("contextCast")
});
// <video>/<audio> "Cast..." context menu item
menuIdMediaCast = browser.menus.create({
contexts: [ "audio", "video", "image" ]
, title: _("contextCast")
, visible: opts.mediaEnabled
, targetUrlPatterns: opts.localMediaEnabled
contexts: ["audio", "video", "image"],
title: _("contextCast"),
visible: opts.mediaEnabled,
targetUrlPatterns: opts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
menuIdWhitelist = browser.menus.create({
contexts: [ "browser_action" ]
, title: _("contextAddToWhitelist")
, enabled: false
contexts: ["browser_action"],
title: _("contextAddToWhitelist"),
enabled: false
});
menuIdWhitelistRecommended = browser.menus.create({
title: _("contextAddToWhitelistRecommended")
, parentId: menuIdWhitelist
title: _("contextAddToWhitelistRecommended"),
parentId: menuIdWhitelist
});
browser.menus.create({
type: "separator"
, parentId: menuIdWhitelist
type: "separator",
parentId: menuIdWhitelist
});
}
browser.menus.onClicked.addListener(async (info, tab) => {
if (info.parentMenuItemId === menuIdWhitelist) {
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
if (!pattern) {
throw logger.error(`Whitelist pattern not found for menu item ID ${info.menuItemId}.`);
throw logger.error(
`Whitelist pattern not found for menu item ID ${info.menuItemId}.`
);
}
const whitelist = await options.get("userAgentWhitelist");
@@ -99,7 +98,9 @@ browser.menus.onClicked.addListener(async (info, tab) => {
switch (info.menuItemId) {
case menuIdCast: {
const selection = await ReceiverSelectorManager.getSelection(
tab.id, info.frameId);
tab.id,
info.frameId
);
// Selection cancelled
if (!selection) {
@@ -107,9 +108,9 @@ browser.menus.onClicked.addListener(async (info, tab) => {
}
loadSender({
tabId: tab.id
, frameId: info.frameId
, selection
tabId: tab.id,
frameId: info.frameId,
selection
});
break;
@@ -117,7 +118,10 @@ browser.menus.onClicked.addListener(async (info, tab) => {
case menuIdMediaCast: {
const selection = await ReceiverSelectorManager.getSelection(
tab.id, info.frameId, true);
tab.id,
info.frameId,
true
);
// Selection cancelled
if (!selection) {
@@ -130,30 +134,26 @@ browser.menus.onClicked.addListener(async (info, tab) => {
* If the selected media type is App, that refers to the
* media sender in this context, so load media sender.
*/
if (selection.mediaType ===
ReceiverSelectorMediaType.App) {
if (selection.mediaType === ReceiverSelectorMediaType.App) {
await browser.tabs.executeScript(tab.id, {
code: stringify`
window.receiver = ${selection.receiver};
window.mediaUrl = ${info.srcUrl};
window.targetElementId = ${
info.targetElementId};
`
, frameId: info.frameId
window.targetElementId = ${info.targetElementId};
`,
frameId: info.frameId
});
await browser.tabs.executeScript(tab.id, {
file: "senders/media/index.js"
, frameId: info.frameId
file: "senders/media/index.js",
frameId: info.frameId
});
} else {
// Handle other responses
loadSender({
tabId: tab.id
, frameId: info.frameId
, selection
tabId: tab.id,
frameId: info.frameId,
selection
});
}
@@ -225,7 +225,7 @@ browser.menus.onShown.addListener(async info => {
enabled: true
});
for (const [ menuId ] of whitelistChildMenuPatterns) {
for (const [menuId] of whitelistChildMenuPatterns) {
// Clear all page-specific temporary menus
if (menuId !== menuIdWhitelistRecommended) {
browser.menus.remove(menuId);
@@ -235,9 +235,10 @@ browser.menus.onShown.addListener(async info => {
}
// If there is more than one subdomain, get the base domain
const baseDomain = (url.hostname.match(/\./g) || []).length > 1
? url.hostname.substring(url.hostname.indexOf(".") + 1)
: url.hostname;
const baseDomain =
(url.hostname.match(/\./g) || []).length > 1
? url.hostname.substring(url.hostname.indexOf(".") + 1)
: url.hostname;
const portlessOrigin = `${url.protocol}//${url.hostname}`;
@@ -253,17 +254,17 @@ browser.menus.onShown.addListener(async info => {
});
whitelistChildMenuPatterns.set(
menuIdWhitelistRecommended, patternRecommended);
menuIdWhitelistRecommended,
patternRecommended
);
if (url.search) {
const whitelistSearchMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", patternSearch)
, parentId: menuIdWhitelist
title: _("contextAddToWhitelistAdvancedAdd", patternSearch),
parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
whitelistSearchMenuId, patternSearch);
whitelistChildMenuPatterns.set(whitelistSearchMenuId, patternSearch);
}
/**
@@ -275,65 +276,63 @@ browser.menus.onShown.addListener(async info => {
? url.pathname.substring(0, url.pathname.length - 1)
: url.pathname;
const pathSegments = pathTrimmed.split("/")
.filter(segment => segment)
.reverse();
const pathSegments = pathTrimmed
.split("/")
.filter(segment => segment)
.reverse();
if (pathSegments.length) {
for (let i = 0; i < pathSegments.length; i++) {
const partialPath = pathSegments
.slice(i)
.reverse()
.join("/");
const partialPath = pathSegments.slice(i).reverse().join("/");
const pattern = `${portlessOrigin}/${partialPath}/*`;
const partialPathMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", pattern)
, parentId: menuIdWhitelist
title: _("contextAddToWhitelistAdvancedAdd", pattern),
parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
partialPathMenuId, pattern);
whitelistChildMenuPatterns.set(partialPathMenuId, pattern);
}
}
}
const wildcardProtocolMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardProtocol)
, parentId: menuIdWhitelist
title: _("contextAddToWhitelistAdvancedAdd", patternWildcardProtocol),
parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardProtocolMenuId, patternWildcardProtocol);
wildcardProtocolMenuId,
patternWildcardProtocol
);
const wildcardSubdomainMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardSubdomain)
, parentId: menuIdWhitelist
title: _("contextAddToWhitelistAdvancedAdd", patternWildcardSubdomain),
parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardSubdomainMenuId, patternWildcardSubdomain);
wildcardSubdomainMenuId,
patternWildcardSubdomain
);
const wildcardProtocolAndSubdomainMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardProtocolAndSubdomain)
, parentId: menuIdWhitelist
title: _(
"contextAddToWhitelistAdvancedAdd",
patternWildcardProtocolAndSubdomain
),
parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardProtocolAndSubdomainMenuId
, patternWildcardProtocolAndSubdomain);
wildcardProtocolAndSubdomainMenuId,
patternWildcardProtocolAndSubdomain
);
await browser.menus.refresh();
});
options.addEventListener("changed", async ev => {
const alteredOpts = ev.detail;
const newOpts = await options.getAll();

View File

@@ -8,17 +8,16 @@ import { Message, Port } from "../messaging";
import { ReceiverDevice } from "../types";
import { ReceiverStatus } from "../shim/cast/types";
interface EventMap {
"receiverDeviceUp": { receiverDevice: ReceiverDevice }
, "receiverDeviceDown": { receiverDeviceId: string }
, "receiverDeviceUpdated": {
receiverDeviceId: string
, status: ReceiverStatus
}
receiverDeviceUp: { receiverDevice: ReceiverDevice };
receiverDeviceDown: { receiverDeviceId: string };
receiverDeviceUpdated: {
receiverDeviceId: string;
status: ReceiverStatus;
};
}
export default new class extends TypedEventTarget<EventMap> {
export default new (class extends TypedEventTarget<EventMap> {
/**
* Map of receiver device IDs to devices. Updated as
* receiverDevice messages are received from the bridge.
@@ -27,7 +26,6 @@ export default new class extends TypedEventTarget<EventMap> {
private bridgePort?: Port;
async init() {
if (!this.bridgePort) {
await this.refresh();
@@ -47,8 +45,8 @@ export default new class extends TypedEventTarget<EventMap> {
port.onDisconnect.addListener(this.onBridgeDisconnect);
port.postMessage({
subject: "bridge:startDiscovery"
, data: {
subject: "bridge:startDiscovery",
data: {
// Also send back status messages
shouldWatchStatus: true
}
@@ -69,15 +67,17 @@ export default new class extends TypedEventTarget<EventMap> {
*/
stopReceiverApp(receiverDeviceId: string) {
if (!this.bridgePort) {
logger.error("Failed to stop receiver device, no bridge connection");
logger.error(
"Failed to stop receiver device, no bridge connection"
);
return;
}
const receiverDevice = this.receiverDevices.get(receiverDeviceId);
if (receiverDevice) {
this.bridgePort.postMessage({
subject: "bridge:stopCastApp"
, data: { receiverDevice }
subject: "bridge:stopCastApp",
data: { receiverDevice }
});
}
}
@@ -89,10 +89,10 @@ export default new class extends TypedEventTarget<EventMap> {
this.receiverDevices.set(receiverDevice.id, receiverDevice);
this.dispatchEvent(
new CustomEvent("receiverDeviceUp"
, {
detail: { receiverDevice }
}));
new CustomEvent("receiverDeviceUp", {
detail: { receiverDevice }
})
);
break;
}
@@ -104,10 +104,10 @@ export default new class extends TypedEventTarget<EventMap> {
this.receiverDevices.delete(receiverDeviceId);
}
this.dispatchEvent(
new CustomEvent("receiverDeviceDown"
, {
detail: { receiverDeviceId }
}));
new CustomEvent("receiverDeviceDown", {
detail: { receiverDeviceId }
})
);
break;
}
@@ -115,10 +115,12 @@ export default new class extends TypedEventTarget<EventMap> {
case "main:receiverDeviceUpdated": {
const { receiverDeviceId, status } = message.data;
const receiverDevice =
this.receiverDevices.get(receiverDeviceId);
this.receiverDevices.get(receiverDeviceId);
if (!receiverDevice) {
logger.error(`Receiver ID \`${receiverDeviceId}\` not found!`);
logger.error(
`Receiver ID \`${receiverDeviceId}\` not found!`
);
break;
}
@@ -129,27 +131,27 @@ export default new class extends TypedEventTarget<EventMap> {
if (status.applications) {
receiverDevice.status.applications =
status.applications;
status.applications;
}
} else {
receiverDevice.status = status;
}
this.dispatchEvent(
new CustomEvent("receiverDeviceUpdated"
, {
detail: {
receiverDeviceId
, status: receiverDevice.status
}
}));
new CustomEvent("receiverDeviceUpdated", {
detail: {
receiverDeviceId,
status: receiverDevice.status
}
})
);
}
}
}
};
private onBridgeDisconnect = () => {
// Notify listeners of device availablility
for (const [ , receiverDevice ] of this.receiverDevices) {
for (const [, receiverDevice] of this.receiverDevices) {
const event = new CustomEvent("receiverDeviceDown", {
detail: { receiverDeviceId: receiverDevice.id }
});
@@ -163,5 +165,5 @@ export default new class extends TypedEventTarget<EventMap> {
window.setTimeout(() => {
this.refresh();
}, 10000);
}
};
};
})();

View File

@@ -8,25 +8,22 @@ import { TypedEventTarget } from "../../lib/TypedEventTarget";
import { getWindowCenteredProps, WindowCenteredProps } from "../../lib/utils";
import { ReceiverDevice } from "../../types";
import { ReceiverSelectionCast
, ReceiverSelectionStop
, ReceiverSelectorMediaType } from "./index";
import {
ReceiverSelectionCast,
ReceiverSelectionStop,
ReceiverSelectorMediaType
} from "./index";
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
interface ReceiverSelectorEvents {
"selected": ReceiverSelectionCast;
"error": string;
"cancelled": void;
"stop": ReceiverSelectionStop;
selected: ReceiverSelectionCast;
error: string;
cancelled: void;
stop: ReceiverSelectionStop;
}
export default class ReceiverSelector
extends TypedEventTarget<ReceiverSelectorEvents> {
export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorEvents> {
private windowId?: number;
private messagePort?: Port;
@@ -65,11 +62,11 @@ export default class ReceiverSelector
}
public async open(
receivers: ReceiverDevice[]
, defaultMediaType: ReceiverSelectorMediaType
, availableMediaTypes: ReceiverSelectorMediaType
, appId?: string): Promise<void> {
receivers: ReceiverDevice[],
defaultMediaType: ReceiverSelectorMediaType,
availableMediaTypes: ReceiverSelectorMediaType,
appId?: string
): Promise<void> {
this.appId = appId;
// If popup already exists, close it
@@ -81,27 +78,28 @@ export default class ReceiverSelector
this.defaultMediaType = defaultMediaType;
this.availableMediaTypes = availableMediaTypes;
let centeredProps: WindowCenteredProps = {
left: 100
, top: 100
, width: 350
, height: 200
left: 100,
top: 100,
width: 350,
height: 200
};
try {
// Calculate centered size/position based on current window
centeredProps = getWindowCenteredProps(
await browser.windows.getCurrent()
, centeredProps.width, centeredProps.height);
await browser.windows.getCurrent(),
centeredProps.width,
centeredProps.height
);
} catch {
// Shouldn't ever hit this, but defaults are provided in case
}
const popup = await browser.windows.create({
url: POPUP_URL
, type: "popup"
, ...centeredProps
url: POPUP_URL,
type: "popup",
...centeredProps
});
if (popup?.id === undefined) {
@@ -116,22 +114,23 @@ export default class ReceiverSelector
...centeredProps
});
const closeIfFocusLost = await options.get(
"receiverSelectorCloseIfFocusLost");
"receiverSelectorCloseIfFocusLost"
);
if (closeIfFocusLost) {
// Add focus listener
browser.windows.onFocusChanged.addListener(
this.onWindowsFocusChanged);
this.onWindowsFocusChanged
);
}
}
public update(receivers: ReceiverDevice[]) {
this.receivers = receivers;
this.messagePort?.postMessage({
subject: "popup:update"
, data: {
subject: "popup:update",
data: {
receivers: this.receivers
}
});
@@ -167,23 +166,25 @@ export default class ReceiverSelector
this.messagePortDisconnected = true;
});
if (!this.receivers
|| !this.defaultMediaType
|| !this.availableMediaTypes) {
if (
!this.receivers ||
!this.defaultMediaType ||
!this.availableMediaTypes
) {
throw logger.error("Popup receiver data not found.");
}
this.messagePort.postMessage({
subject: "popup:init"
, data: { appId: this.appId }
subject: "popup:init",
data: { appId: this.appId }
});
this.messagePort.postMessage({
subject: "popup:update"
, data: {
receivers: this.receivers
, defaultMediaType: this.defaultMediaType
, availableMediaTypes: this.availableMediaTypes
subject: "popup:update",
data: {
receivers: this.receivers,
defaultMediaType: this.defaultMediaType,
availableMediaTypes: this.availableMediaTypes
}
});
@@ -197,17 +198,21 @@ export default class ReceiverSelector
switch (message.subject) {
case "receiverSelector:selected": {
this.wasReceiverSelected = true;
this.dispatchEvent(new CustomEvent("selected", {
detail: message.data
}));
this.dispatchEvent(
new CustomEvent("selected", {
detail: message.data
})
);
break;
}
case "receiverSelector:stop": {
this.dispatchEvent(new CustomEvent("stop", {
detail: message.data
}));
this.dispatchEvent(
new CustomEvent("stop", {
detail: message.data
})
);
break;
}
@@ -226,7 +231,8 @@ export default class ReceiverSelector
browser.windows.onRemoved.removeListener(this.onWindowsRemoved);
browser.windows.onFocusChanged.removeListener(
this.onWindowsFocusChanged);
this.onWindowsFocusChanged
);
if (!this.wasReceiverSelected) {
this.dispatchEvent(new CustomEvent("cancelled"));
@@ -247,12 +253,14 @@ export default class ReceiverSelector
* `WINDOW_ID_NONE` or if the popup window is re-focused.
*/
private onWindowsFocusChanged(windowId: number) {
if (windowId !== browser.windows.WINDOW_ID_NONE
&& windowId !== this.windowId) {
if (
windowId !== browser.windows.WINDOW_ID_NONE &&
windowId !== this.windowId
) {
// Only run once
browser.windows.onFocusChanged.removeListener(
this.onWindowsFocusChanged);
this.onWindowsFocusChanged
);
if (this.windowId) {
browser.windows.remove(this.windowId);

View File

@@ -8,18 +8,18 @@ import receiverDevices from "../receiverDevices";
import { getMediaTypesForPageUrl } from "../../lib/utils";
import { ReceiverSelection
, ReceiverSelectionActionType
, ReceiverSelectorMediaType } from "./index";
import {
ReceiverSelection,
ReceiverSelectionActionType,
ReceiverSelectorMediaType
} from "./index";
import ReceiverSelector from "./ReceiverSelector";
async function createSelector() {
return new ReceiverSelector();
}
let sharedSelector: ReceiverSelector;
async function getSelector() {
@@ -34,7 +34,6 @@ async function getSelector() {
return sharedSelector;
}
/**
* Opens a receiver selector with the specified
* default/available media types.
@@ -46,21 +45,18 @@ async function getSelector() {
* - Rejects if the selection fails.
*/
async function getSelection(
contextTabId: number
, contextFrameId = 0
, withMediaSender = false)
: Promise<ReceiverSelection | null> {
contextTabId: number,
contextFrameId = 0,
withMediaSender = false
): Promise<ReceiverSelection | null> {
return new Promise(async (resolve, reject) => {
let currentShim = ShimManager.getShim(
contextTabId, contextFrameId);
let currentShim = ShimManager.getShim(contextTabId, contextFrameId);
/**
* If the current context is running the mirroring app, pretend
* it doesn't exist because it shouldn't be launched like this.
*/
if (currentShim?.appId ===
await options.get("mirroringAppId")) {
if (currentShim?.appId === (await options.get("mirroringAppId"))) {
currentShim = undefined;
}
@@ -69,13 +65,15 @@ async function getSelection(
try {
const { url } = await browser.webNavigation.getFrame({
tabId: contextTabId
, frameId: contextFrameId
tabId: contextTabId,
frameId: contextFrameId
});
availableMediaTypes = getMediaTypesForPageUrl(url);
} catch {
logger.error("Failed to locate frame, falling back to default available media types.");
logger.error(
"Failed to locate frame, falling back to default available media types."
);
availableMediaTypes = ReceiverSelectorMediaType.File;
}
@@ -90,8 +88,8 @@ async function getSelection(
// Remove mirroring media types if mirroring is not enabled
if (!opts.mirroringEnabled) {
availableMediaTypes &= ~(
ReceiverSelectorMediaType.Tab
| ReceiverSelectorMediaType.Screen);
ReceiverSelectorMediaType.Tab | ReceiverSelectorMediaType.Screen
);
}
// Remove file media type if local media is not enabled
@@ -107,26 +105,28 @@ async function getSelection(
// Get a new selector for each selection
sharedSelector = await createSelector();
function onReceiverChange() {
sharedSelector.update(receiverDevices.getDevices());
}
receiverDevices.addEventListener("receiverDeviceUp", onReceiverChange);
receiverDevices.addEventListener(
"receiverDeviceUp", onReceiverChange);
"receiverDeviceDown",
onReceiverChange
);
receiverDevices.addEventListener(
"receiverDeviceDown", onReceiverChange);
receiverDevices.addEventListener(
"receiverDeviceUpdated", onReceiverChange);
"receiverDeviceUpdated",
onReceiverChange
);
let onSelected: any;
let onCancelled: any;
let onError: any;
let onStop: any;
type EvParamsType =
Parameters<typeof sharedSelector.addEventListener>[0];
type EvParamsType = Parameters<
typeof sharedSelector.addEventListener
>[0];
function storeListener<T>(type: EvParamsType, fn: T) {
if (type === "selected") {
@@ -149,67 +149,78 @@ async function getSelection(
sharedSelector.removeEventListener("stop", onStop);
receiverDevices.removeEventListener(
"receiverDeviceUp", onReceiverChange);
"receiverDeviceUp",
onReceiverChange
);
receiverDevices.removeEventListener(
"receiverDeviceDown", onReceiverChange);
"receiverDeviceDown",
onReceiverChange
);
receiverDevices.removeEventListener(
"receiverDeviceUpdated", onReceiverChange);
"receiverDeviceUpdated",
onReceiverChange
);
}
sharedSelector.addEventListener("selected"
, storeListener("selected", ev => {
sharedSelector.addEventListener(
"selected",
storeListener("selected", ev => {
logger.info("Selected receiver", ev.detail);
resolve({
actionType: ReceiverSelectionActionType.Cast,
receiver: ev.detail.receiver,
mediaType: ev.detail.mediaType,
filePath: ev.detail.filePath
});
removeListeners();
})
);
logger.info("Selected receiver", ev.detail);
resolve({
actionType: ReceiverSelectionActionType.Cast
, receiver: ev.detail.receiver
, mediaType: ev.detail.mediaType
, filePath: ev.detail.filePath
});
removeListeners();
}));
sharedSelector.addEventListener(
"cancelled",
storeListener("cancelled", () => {
logger.info("Cancelled receiver selection");
resolve(null);
removeListeners();
})
);
sharedSelector.addEventListener("cancelled"
, storeListener("cancelled", () => {
sharedSelector.addEventListener(
"error",
storeListener("error", () => {
reject();
removeListeners();
})
);
logger.info("Cancelled receiver selection");
resolve(null);
removeListeners();
}));
sharedSelector.addEventListener(
"stop",
storeListener("stop", async ev => {
logger.info("Stopping receiver app...", ev.detail);
sharedSelector.addEventListener("error"
, storeListener("error", () => {
reject();
removeListeners();
}));
sharedSelector.addEventListener("stop"
, storeListener("stop", async ev => {
logger.info("Stopping receiver app...", ev.detail);
receiverDevices.stopReceiverApp(ev.detail.receiver.id);
resolve({
actionType: ReceiverSelectionActionType.Stop
, receiver: ev.detail.receiver
});
removeListeners();
}));
receiverDevices.stopReceiverApp(ev.detail.receiver.id);
resolve({
actionType: ReceiverSelectionActionType.Stop,
receiver: ev.detail.receiver
});
removeListeners();
})
);
// Ensure status manager is initialized
await receiverDevices.init();
sharedSelector.open(
receiverDevices.getDevices()
, defaultMediaType
, availableMediaTypes
, currentShim?.appId);
receiverDevices.getDevices(),
defaultMediaType,
availableMediaTypes,
currentShim?.appId
);
});
}
export default {
getSelection
, getSelector
getSelection,
getSelector
};

View File

@@ -1,20 +1,20 @@
"use strict";
export enum ReceiverSelectorType {
Popup
, Native
Popup,
Native
}
export enum ReceiverSelectorMediaType {
App = 1
, Tab = 2
, Screen = 4
, File = 8
App = 1,
Tab = 2,
Screen = 4,
File = 8
}
export enum ReceiverSelectionActionType {
Cast = 1
, Stop = 2
Cast = 1,
Stop = 2
}
export interface ReceiverSelectionCast {

View File

@@ -5,21 +5,23 @@ import options from "../lib/options";
import { getChromeUserAgent } from "../lib/userAgents";
import { CAST_FRAMEWORK_LOADER_SCRIPT_URL
, CAST_LOADER_SCRIPT_URL } from "../lib/endpoints";
import {
CAST_FRAMEWORK_LOADER_SCRIPT_URL,
CAST_LOADER_SCRIPT_URL
} from "../lib/endpoints";
// Missing on @types/firefox-webext-browser
type OnBeforeSendHeadersDetails = Parameters<Parameters<
typeof browser.webRequest.onBeforeSendHeaders.addListener>[0]>[0] & {
frameAncestors?: Array<{ url: string, frameId: number }>
type OnBeforeSendHeadersDetails = Parameters<
Parameters<typeof browser.webRequest.onBeforeSendHeaders.addListener>[0]
>[0] & {
frameAncestors?: Array<{ url: string; frameId: number }>;
};
type OnBeforeRequestDetails = Parameters<Parameters<
typeof browser.webRequest.onBeforeRequest.addListener>[0]>[0] & {
frameAncestors?: Array<{ url: string, frameId: number }>
type OnBeforeRequestDetails = Parameters<
Parameters<typeof browser.webRequest.onBeforeRequest.addListener>[0]
>[0] & {
frameAncestors?: Array<{ url: string; frameId: number }>;
};
const originUrlCache: string[] = [];
let platform: string;
@@ -51,40 +53,43 @@ export async function initWhitelist() {
options.addEventListener("changed", ev => {
const alteredOpts = ev.detail;
if (alteredOpts.includes("userAgentWhitelist")
|| alteredOpts.includes("userAgentWhitelistEnabled")) {
if (
alteredOpts.includes("userAgentWhitelist") ||
alteredOpts.includes("userAgentWhitelistEnabled")
) {
unregisterUserAgentWhitelist();
registerUserAgentWhitelist();
}
});
}
/**
* Web apps usually only load the sender library and
* provide cast functionality if the browser is detected
* as Chrome, so we should rewrite the User-Agent header
* to reflect this on whitelisted sites.
*/
async function onWhitelistedBeforeSendHeaders(
details: OnBeforeSendHeadersDetails) {
async function onWhitelistedBeforeSendHeaders(
details: OnBeforeSendHeadersDetails
) {
if (!details.requestHeaders) {
throw logger.error("OnBeforeSendHeaders handler details missing requestHeaders.");
throw logger.error(
"OnBeforeSendHeaders handler details missing requestHeaders."
);
}
if (details.originUrl && !originUrlCache.includes(details.originUrl)) {
originUrlCache.push(details.originUrl);
}
const host = details.requestHeaders.find(
header => header.name === "Host");
const host = details.requestHeaders.find(header => header.name === "Host");
for (const header of details.requestHeaders) {
if (header.name === "User-Agent") {
header.value = host?.value === "www.youtube.com"
? chromeUserAgentHybrid
: chromeUserAgent;
header.value =
host?.value === "www.youtube.com"
? chromeUserAgentHybrid
: chromeUserAgent;
break;
}
}
@@ -101,8 +106,8 @@ export async function initWhitelist() {
* main site is whitelisted.
*/
function onWhitelistedChildBeforeSendHeaders(
details: OnBeforeSendHeadersDetails) {
details: OnBeforeSendHeadersDetails
) {
if (!details.requestHeaders || !details.frameAncestors) {
return;
}
@@ -110,13 +115,15 @@ function onWhitelistedChildBeforeSendHeaders(
for (const ancestor of details.frameAncestors) {
if (originUrlCache.includes(ancestor.url)) {
const host = details.requestHeaders.find(
header => header.name === "Host");
header => header.name === "Host"
);
for (const header of details.requestHeaders) {
if (header.name === "User-Agent") {
header.value = host?.value === "www.youtube.com"
? chromeUserAgentHybrid
: chromeUserAgent;
header.value =
host?.value === "www.youtube.com"
? chromeUserAgentHybrid
: chromeUserAgent;
break;
}
}
@@ -128,7 +135,6 @@ function onWhitelistedChildBeforeSendHeaders(
}
}
/**
* Sender applications load a cast_sender.js script that
* functions as a loader for the internal chrome-extension:
@@ -165,16 +171,17 @@ async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) {
await browser.tabs.executeScript(details.tabId, {
code: `
window.isFramework = ${
details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL};
`
, frameId: details.frameId
, runAt: "document_start"
details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL
};
`,
frameId: details.frameId,
runAt: "document_start"
});
await browser.tabs.executeScript(details.tabId, {
file: "shim/contentBridge.js"
, frameId: details.frameId
, runAt: "document_start"
file: "shim/contentBridge.js",
frameId: details.frameId,
runAt: "document_start"
});
return {
@@ -182,40 +189,41 @@ async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) {
};
}
async function registerUserAgentWhitelist() {
const { userAgentWhitelist
, userAgentWhitelistEnabled } = await options.getAll();
const { userAgentWhitelist, userAgentWhitelistEnabled } =
await options.getAll();
browser.webRequest.onBeforeRequest.addListener(
onBeforeCastSDKRequest
, { urls: [
CAST_LOADER_SCRIPT_URL
, CAST_FRAMEWORK_LOADER_SCRIPT_URL ]}
, [ "blocking" ]);
onBeforeCastSDKRequest,
{ urls: [CAST_LOADER_SCRIPT_URL, CAST_FRAMEWORK_LOADER_SCRIPT_URL] },
["blocking"]
);
if (!userAgentWhitelistEnabled || !userAgentWhitelist.length) {
return;
}
browser.webRequest.onBeforeSendHeaders.addListener(
onWhitelistedBeforeSendHeaders
, { urls: userAgentWhitelist }
, [ "blocking", "requestHeaders" ]);
onWhitelistedBeforeSendHeaders,
{ urls: userAgentWhitelist },
["blocking", "requestHeaders"]
);
browser.webRequest.onBeforeSendHeaders.addListener(
onWhitelistedChildBeforeSendHeaders
, { urls: [ "<all_urls>" ]}
, [ "blocking", "requestHeaders" ]);
onWhitelistedChildBeforeSendHeaders,
{ urls: ["<all_urls>"] },
["blocking", "requestHeaders"]
);
}
function unregisterUserAgentWhitelist() {
originUrlCache.length = 0;
browser.webRequest.onBeforeSendHeaders
.removeListener(onWhitelistedBeforeSendHeaders);
browser.webRequest.onBeforeSendHeaders
.removeListener(onWhitelistedChildBeforeSendHeaders);
browser.webRequest.onBeforeRequest
.removeListener(onBeforeCastSDKRequest);
browser.webRequest.onBeforeSendHeaders.removeListener(
onWhitelistedBeforeSendHeaders
);
browser.webRequest.onBeforeSendHeaders.removeListener(
onWhitelistedChildBeforeSendHeaders
);
browser.webRequest.onBeforeRequest.removeListener(onBeforeCastSDKRequest);
}

View File

@@ -3,25 +3,22 @@
import { ReceiverSelectorType } from "./background/receiverSelector";
import { Options } from "./lib/options";
export default {
bridgeApplicationName: BRIDGE_NAME
, bridgeBackupEnabled: false
, bridgeBackupHost: "localhost"
, bridgeBackupPort: 9556
, mediaEnabled: true
, mediaOverlayEnabled: false
, mediaSyncElement: false
, mediaStopOnUnload: false
, localMediaEnabled: true
, localMediaServerPort: 9555
, mirroringEnabled: false
, mirroringAppId: MIRRORING_APP_ID
, receiverSelectorCloseIfFocusLost: true
, receiverSelectorWaitForConnection: true
, userAgentWhitelistEnabled: true
, userAgentWhitelistRestrictedEnabled: true
, userAgentWhitelist: [
"https://www.netflix.com/*"
]
bridgeApplicationName: BRIDGE_NAME,
bridgeBackupEnabled: false,
bridgeBackupHost: "localhost",
bridgeBackupPort: 9556,
mediaEnabled: true,
mediaOverlayEnabled: false,
mediaSyncElement: false,
mediaStopOnUnload: false,
localMediaEnabled: true,
localMediaServerPort: 9555,
mirroringEnabled: false,
mirroringAppId: MIRRORING_APP_ID,
receiverSelectorCloseIfFocusLost: true,
receiverSelectorWaitForConnection: true,
userAgentWhitelistEnabled: true,
userAgentWhitelistRestrictedEnabled: true,
userAgentWhitelist: ["https://www.netflix.com/*"]
} as Options;

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

@@ -4,11 +4,9 @@ declare const MIRRORING_APP_ID: string;
declare type Nullable<T> = T | null;
declare type DistributiveOmit<T, K extends keyof any> =
T extends any
? Omit<T, K>
: never;
declare type DistributiveOmit<T, K extends keyof any> = T extends any
? Omit<T, K>
: never;
declare interface Object {
// eslint-disable-next-line @typescript-eslint/ban-types
@@ -22,16 +20,19 @@ declare interface CanvasRenderingContext2D {
DRAWWINDOW_USE_WIDGET_LAYERS: 0x08;
DRAWWINDOW_ASYNC_DECODE_IMAGES: 0x10;
drawWindow (
window: Window
, x: number, y: number
, w: number, h: number
, bgColor: string
, flags: number): void;
drawWindow(
window: Window,
x: number,
y: number,
w: number,
h: number,
bgColor: string,
flags: number
): void;
}
declare interface HTMLCanvasElement {
captureStream (frameRate?: number): MediaStream;
captureStream(frameRate?: number): MediaStream;
}
declare interface MediaTrackConstraints {
@@ -39,25 +40,23 @@ declare interface MediaTrackConstraints {
}
declare interface RTCPeerConnection {
addStream (mediaStream: MediaStream): void;
addStream(mediaStream: MediaStream): void;
}
declare interface MediaDevices {
getDisplayMedia (constraints: MediaStreamConstraints)
: Promise<MediaStream>;
getDisplayMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
}
interface CloneIntoOptions {
cloneFunctions?: boolean;
wrapReflectors?: boolean;
}
declare function cloneInto<T> (
obj: T
, targetScope: Window
, options?: CloneIntoOptions): T;
declare function cloneInto<T>(
obj: T,
targetScope: Window,
options?: CloneIntoOptions
): T;
interface ExportFunctionOptions {
defineAs: string;
@@ -67,11 +66,11 @@ interface ExportFunctionOptions {
type ExportFunctionFunc = (...args: any[]) => any;
declare function exportFunction (
func: ExportFunctionFunc
, targetScope: any
, options?: ExportFunctionOptions): ExportFunctionFunc;
declare function exportFunction(
func: ExportFunctionFunc,
targetScope: any,
options?: ExportFunctionOptions
): ExportFunctionFunc;
// Fix issues with @types/firefox-webext-browser
declare namespace browser.events {
@@ -80,8 +79,8 @@ declare namespace browser.events {
* event types.
*/
interface Event {
addListener (...args: any[]): void | Promise<void>;
removeListener (...args: any[]): void | Promise<void>;
addListener(...args: any[]): void | Promise<void>;
removeListener(...args: any[]): void | Promise<void>;
}
}
@@ -97,7 +96,7 @@ declare namespace browser.runtime {
}
function connect(connectInfo: {
name?: string
, includeTlsChannelId?: boolean
}): browser.runtime.Port;
name?: string;
includeTlsChannelId?: boolean;
}): browser.runtime.Port;
}

View File

@@ -10,14 +10,18 @@ interface TypedEvents {
export class TypedEventTarget<T extends TypedEvents> extends EventTarget {
// @ts-ignore
public addEventListener<K extends keyof T>(
type: K, listener: (ev: CustomEvent<T[K]>) => void): void {
type: K,
listener: (ev: CustomEvent<T[K]>) => void
): void {
// @ts-ignore
super.addEventListener(type as string, listener);
}
// @ts-ignore
public removeEventListener<K extends keyof T>(
type: K, listener: (ev: CustomEvent<T[K]>) => void): void {
type: K,
listener: (ev: CustomEvent<T[K]>) => void
): void {
// @ts-ignore
super.removeEventListener(type as string, listener);
}

View File

@@ -4,24 +4,21 @@
* Provides a typed interface to runtime.Port objects.
*/
export interface TypedPort<T>
extends Omit<browser.runtime.Port
, "onDisconnect"
| "onMessage"
| "postMessage"> {
extends Omit<
browser.runtime.Port,
"onDisconnect" | "onMessage" | "postMessage"
> {
onDisconnect: {
addListener (cb: (port: TypedPort<T>) => void): void | Promise<void>
, removeListener (cb: (port: TypedPort<T>) => void): void | Promise<void>
, hasListener (cb: (port: TypedPort<T>) => void): boolean
, hasListeners (): boolean
}
, onMessage: {
addListener (cb: (message: T) => void): void | Promise<void>
, removeListener (cb: (message: T) => void): void | Promise<void>
, hasListener (cb: (message: T) => void): boolean
, hasListeners (): boolean
}
, postMessage (message: T): void
addListener(cb: (port: TypedPort<T>) => void): void | Promise<void>;
removeListener(cb: (port: TypedPort<T>) => void): void | Promise<void>;
hasListener(cb: (port: TypedPort<T>) => void): boolean;
hasListeners(): boolean;
};
onMessage: {
addListener(cb: (message: T) => void): void | Promise<void>;
removeListener(cb: (message: T) => void): void | Promise<void>;
hasListener(cb: (message: T) => void): boolean;
hasListeners(): boolean;
};
postMessage(message: T): void;
}

View File

@@ -13,21 +13,18 @@ export class TypedStorageArea<Schema extends { [key: string]: any }> {
this.storageArea = storageArea;
}
public async get<SchemaKey extends keyof Schema
, SchemaPartial extends Partial<Schema>>(
keys?: SchemaKey
| SchemaKey[]
| SchemaPartial
| null | undefined)
: Promise<Pick<Schema, Extract<
keyof SchemaPartial, SchemaKey>>> {
public async get<
SchemaKey extends keyof Schema,
SchemaPartial extends Partial<Schema>
>(
keys?: SchemaKey | SchemaKey[] | SchemaPartial | null | undefined
): Promise<Pick<Schema, Extract<keyof SchemaPartial, SchemaKey>>> {
return await this.storageArea.get(keys);
}
public async getBytesInUse<SchemaKey extends keyof Schema>(
keys?: Schema | SchemaKey[]): Promise<number> {
keys?: Schema | SchemaKey[]
): Promise<number> {
return await this.storageArea.getBytesInUse(keys);
}
@@ -36,8 +33,8 @@ export class TypedStorageArea<Schema extends { [key: string]: any }> {
}
public async remove<SchemaKey extends keyof Schema>(
keys: SchemaKey | SchemaKey[]): Promise<void> {
keys: SchemaKey | SchemaKey[]
): Promise<void> {
await this.storageArea.remove(keys);
}

View File

@@ -7,7 +7,6 @@ import { Port } from "../messaging";
import nativeMessaging from "./nativeMessaging";
import options from "./options";
export const BRIDGE_TIMEOUT = 5000;
/**
@@ -15,13 +14,16 @@ export const BRIDGE_TIMEOUT = 5000;
*/
async function connect(): Promise<Port> {
const applicationName = await options.get("bridgeApplicationName");
const bridgePort = nativeMessaging.connectNative(applicationName) as
unknown as Port;
const bridgePort = nativeMessaging.connectNative(
applicationName
) as unknown as Port;
bridgePort.onDisconnect.addListener(() => {
if (bridgePort.error) {
console.error(`${applicationName} disconnected:`
, bridgePort.error.message);
console.error(
`${applicationName} disconnected:`,
bridgePort.error.message
);
} else {
console.info(`${applicationName} disconnected`);
}
@@ -30,7 +32,6 @@ async function connect(): Promise<Port> {
return bridgePort;
}
export interface BridgeInfo {
name: string;
version: string;
@@ -50,77 +51,81 @@ export class BridgeTimedOutError extends Error {}
* rules to determine compatiblity, then returns a
* BridgeInfo object.
*/
const getInfo = () => new Promise<BridgeInfo>(async (resolve, reject) => {
const applicationName = await options.get("bridgeApplicationName");
if (!applicationName) {
reject(logger.error("Bridge application name not found."));
return;
}
const getInfo = () =>
new Promise<BridgeInfo>(async (resolve, reject) => {
const applicationName = await options.get("bridgeApplicationName");
if (!applicationName) {
reject(logger.error("Bridge application name not found."));
return;
}
const bridgeTimeoutId = setTimeout(() => {
logger.error("Bridge timed out.");
reject(new BridgeTimedOutError());
}, BRIDGE_TIMEOUT);
const bridgeTimeoutId = setTimeout(() => {
logger.error("Bridge timed out.");
reject(new BridgeTimedOutError());
}, BRIDGE_TIMEOUT);
let applicationVersion: string;
try {
const { version } = browser.runtime.getManifest();
let applicationVersion: string;
try {
const { version } = browser.runtime.getManifest();
applicationVersion = await nativeMessaging.sendNativeMessage(
applicationName,
{ subject: "bridge:/getInfo", data: version }
);
} catch (err) {
logger.error("Bridge connection failed.");
reject(new BridgeConnectionError());
clearTimeout(bridgeTimeoutId);
return;
}
applicationVersion = await nativeMessaging.sendNativeMessage(
applicationName
, { subject: "bridge:/getInfo"
, data: version });
} catch (err) {
logger.error("Bridge connection failed.");
reject(new BridgeConnectionError());
clearTimeout(bridgeTimeoutId);
return;
}
const extensionVersion = browser.runtime.getManifest().version;
const extensionVersionMajor = semver.major(extensionVersion);
clearTimeout(bridgeTimeoutId);
const versionDiff = semver.diff(applicationVersion, extensionVersion);
const extensionVersion = browser.runtime.getManifest().version;
const extensionVersionMajor = semver.major(extensionVersion);
/**
* If the target version is above 0.x.x range, API is stable
* and versions with minor or patch level changes should be
* compatible.
*/
const isVersionCompatible =
semver.eq(applicationVersion, extensionVersion) ||
(versionDiff !== "major" && extensionVersionMajor !== 0) ||
(versionDiff === "patch" && extensionVersionMajor === 0);
const versionDiff = semver.diff(applicationVersion, extensionVersion);
const isVersionExact = semver.eq(applicationVersion, extensionVersion);
const isVersionOlder = semver.lt(applicationVersion, extensionVersion);
const isVersionNewer = semver.gt(applicationVersion, extensionVersion);
/**
* If the target version is above 0.x.x range, API is stable
* and versions with minor or patch level changes should be
* compatible.
*/
const isVersionCompatible =
semver.eq(applicationVersion, extensionVersion)
|| (versionDiff !== "major" && extensionVersionMajor !== 0)
|| (versionDiff === "patch" && extensionVersionMajor === 0);
// Print compatibility info to console
if (!isVersionCompatible) {
logger.error(
`Expecting ${applicationName} v${BRIDGE_VERSION}, found v${applicationVersion}. ${
isVersionOlder
? "Try updating the native app to the latest version."
: "Try updating the extension to the latest version"
}`
);
}
const isVersionExact = semver.eq(applicationVersion, extensionVersion);
const isVersionOlder = semver.lt(applicationVersion, extensionVersion);
const isVersionNewer = semver.gt(applicationVersion, extensionVersion);
resolve({
name: applicationName,
version: applicationVersion,
expectedVersion: BRIDGE_VERSION,
// Print compatibility info to console
if (!isVersionCompatible) {
logger.error(`Expecting ${applicationName} v${BRIDGE_VERSION}, found v${applicationVersion}. ${
isVersionOlder
? "Try updating the native app to the latest version."
: "Try updating the extension to the latest version"}`);
}
resolve({
name: applicationName
, version: applicationVersion
, expectedVersion: BRIDGE_VERSION
// Version info
, isVersionExact
, isVersionCompatible
, isVersionOlder
, isVersionNewer
// Version info
isVersionExact,
isVersionCompatible,
isVersionOlder,
isVersionNewer
});
});
});
export default {
connect
, getInfo
connect,
getInfo
};

View File

@@ -8,7 +8,7 @@
* UA string checking.
*/
export const CAST_LOADER_SCRIPT_URL =
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js";
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js";
/**
* Cast Chrome Sender Framework API loader script.
@@ -18,8 +18,7 @@ export const CAST_LOADER_SCRIPT_URL =
* the framework API script is conditionally loaded in
* addition to the regular SDK script.
*/
export const CAST_FRAMEWORK_LOADER_SCRIPT_URL =
`${CAST_LOADER_SCRIPT_URL}?loadCastFramework=1`;
export const CAST_FRAMEWORK_LOADER_SCRIPT_URL = `${CAST_LOADER_SCRIPT_URL}?loadCastFramework=1`;
/**
* Cast extension URLs.
@@ -29,8 +28,8 @@ export const CAST_FRAMEWORK_LOADER_SCRIPT_URL =
* chrome-extension: URLs for compatibility reasons (?).
*/
export const CAST_SCRIPT_URLS = [
"chrome-extension://pkedcjkdefgpdelpbcmbmeomcjbeemfm/cast_sender.js"
, "chrome-extension://enhhojjnijigcajfphajepfemndkmdlo/cast_sender.js"
"chrome-extension://pkedcjkdefgpdelpbcmbmeomcjbeemfm/cast_sender.js",
"chrome-extension://enhhojjnijigcajfphajepfemndkmdlo/cast_sender.js"
];
/**
@@ -41,4 +40,4 @@ export const CAST_SCRIPT_URLS = [
* opposed to within the cast extension.
*/
export const CAST_FRAMEWORK_SCRIPT_URL =
"https://www.gstatic.com/cast/sdk/libs/sender/1.0/cast_framework.js";
"https://www.gstatic.com/cast/sdk/libs/sender/1.0/cast_framework.js";

View File

@@ -15,21 +15,30 @@ interface KnownApp {
*/
export default {
// Web-supported
"CA5E8412": { name: "Netflix" , matches: "https://www.netflix.com/*" }
, "233637DE": { name: "YouTube" , matches: "https://www.youtube.com/*" }
, "CC32E753": { name: "Spotify" , matches: "https://open.spotify.com/*" }
, "5E81F6DB": { name: "BBC iPlayer" , matches: "https://www.bbc.co.uk/iplayer/*" }
, "03977A48": { name: "BBC Sounds" , matches: "https://www.bbc.co.uk/sounds/*" }
, "AA666EDD": { name: "Crunchyroll" , matches: "https://crunchyroll.com/*" }
, "10AAD887": { name: "All 4" , matches: "https://www.channel4.com/*" }
, "B3DCF968": { name: "Twitch" , matches: "https://www.twitch.tv/*" }
, "B88B034A": { name: "Dailymotion" , matches: "https://www.dailymotion.com/*" }
, "C3DE6BC2": { name: "Disney+" , matches: "https://www.disneyplus.com/*" }
"CA5E8412": { name: "Netflix", matches: "https://www.netflix.com/*" },
"233637DE": { name: "YouTube", matches: "https://www.youtube.com/*" },
"CC32E753": { name: "Spotify", matches: "https://open.spotify.com/*" },
"5E81F6DB": {
name: "BBC iPlayer",
matches: "https://www.bbc.co.uk/iplayer/*"
},
"03977A48": {
name: "BBC Sounds",
matches: "https://www.bbc.co.uk/sounds/*"
},
"AA666EDD": { name: "Crunchyroll", matches: "https://crunchyroll.com/*" },
"10AAD887": { name: "All 4", matches: "https://www.channel4.com/*" },
"B3DCF968": { name: "Twitch", matches: "https://www.twitch.tv/*" },
"B88B034A": {
name: "Dailymotion",
matches: "https://www.dailymotion.com/*"
},
"C3DE6BC2": { name: "Disney+", matches: "https://www.disneyplus.com/*" },
// Misc
, "17608BC8": { name: "Prime Video" }
, "9AC194DC": { name: "Plex" }
, "CD7B9F59": { name: "Global Player Live" }
"17608BC8": { name: "Prime Video" },
"9AC194DC": { name: "Plex" },
"CD7B9F59": { name: "Global Player Live" },
, "CC1AD845": { name: _("popupMediaTypeAppMedia") }
"CC1AD845": { name: _("popupMediaTypeAppMedia") }
} as Record<string, KnownApp>;

View File

@@ -3,13 +3,14 @@
import logger from "./logger";
import { stringify } from "./utils";
import { ReceiverSelection
, ReceiverSelectionActionType
, ReceiverSelectorMediaType } from "../background/receiverSelector";
import {
ReceiverSelection,
ReceiverSelectionActionType,
ReceiverSelectorMediaType
} from "../background/receiverSelector";
import ShimManager from "../background/ShimManager";
interface LoadSenderOptions {
tabId: number;
frameId?: number;
@@ -34,13 +35,14 @@ export default async function loadSender(opts: LoadSenderOptions) {
case ReceiverSelectorMediaType.App: {
const shim = ShimManager.getShim(opts.tabId, opts.frameId);
if (!shim) {
throw logger.error(`Shim not found at tabId ${
opts.tabId} / frameId ${opts.frameId}`);
throw logger.error(
`Shim not found at tabId ${opts.tabId} / frameId ${opts.frameId}`
);
}
shim.contentPort.postMessage({
subject: "shim:launchApp"
, data: { receiver: opts.selection.receiver }
subject: "shim:launchApp",
data: { receiver: opts.selection.receiver }
});
break;
@@ -52,13 +54,13 @@ export default async function loadSender(opts: LoadSenderOptions) {
code: stringify`
window.selectedMedia = ${opts.selection.mediaType};
window.selectedReceiver = ${opts.selection.receiver};
`
, frameId: opts.frameId
`,
frameId: opts.frameId
});
await browser.tabs.executeScript(opts.tabId, {
file: "senders/mirroring.js"
, frameId: opts.frameId
file: "senders/mirroring.js",
frameId: opts.frameId
});
break;
@@ -69,8 +71,8 @@ export default async function loadSender(opts: LoadSenderOptions) {
const { init } = await import("../senders/media");
init({
mediaUrl: fileUrl.href
, receiver: opts.selection.receiver
mediaUrl: fileUrl.href,
receiver: opts.selection.receiver
});
break;

View File

@@ -5,7 +5,6 @@ import options from "./options";
import { Message, Port } from "../messaging";
type DisconnectListener = (port: Port) => void;
type MessageListener = (message: Message) => void;
@@ -26,7 +25,6 @@ function connectNative(application: string): Port {
const port = browser.runtime.connectNative(application);
let socket: WebSocket;
const onDisconnectListeners = new Set<DisconnectListener>();
@@ -34,47 +32,47 @@ function connectNative(application: string): Port {
// Port proxy API
const portObject: Port = {
error: null as any
, name: ""
error: null as any,
name: "",
, onDisconnect: {
onDisconnect: {
addListener(cb: DisconnectListener) {
onDisconnectListeners.add(cb);
}
, removeListener(cb: DisconnectListener) {
},
removeListener(cb: DisconnectListener) {
onDisconnectListeners.delete(cb);
}
, hasListener(cb: DisconnectListener) {
},
hasListener(cb: DisconnectListener) {
return onDisconnectListeners.has(cb);
},
hasListeners() {
return onDisconnectListeners.size > 0;
}
, hasListeners() {
return onDisconnectListeners.size > 0;
}
}
, onMessage: {
},
onMessage: {
addListener(cb: MessageListener) {
onMessageListeners.add(cb);
}
, removeListener(cb: MessageListener) {
},
removeListener(cb: MessageListener) {
onMessageListeners.delete(cb);
}
, hasListener(cb: MessageListener) {
},
hasListener(cb: MessageListener) {
return onMessageListeners.has(cb);
},
hasListeners() {
return onMessageListeners.size > 0;
}
, hasListeners() {
return onMessageListeners.size > 0;
}
}
},
, disconnect() {
disconnect() {
if (socket) {
socket.close();
} else {
port.disconnect();
}
}
},
, postMessage(message) {
postMessage(message) {
if (socket) {
switch (socket.readyState) {
case WebSocket.CONNECTING: {
@@ -99,11 +97,9 @@ function connectNative(application: string): Port {
}
};
port.onDisconnect.addListener(async () => {
const { bridgeBackupEnabled
, bridgeBackupHost
, bridgeBackupPort } = await options.getAll();
const { bridgeBackupEnabled, bridgeBackupHost, bridgeBackupPort } =
await options.getAll();
if (!bridgeBackupEnabled) {
portObject.error = {
@@ -114,14 +110,17 @@ function connectNative(application: string): Port {
listener(portObject);
}
throw logger.error("Bridge connection failed and backup not enabled.");
throw logger.error(
"Bridge connection failed and backup not enabled."
);
}
if (port.error && !isNativeHostStatusKnown) {
isNativeHostStatusKnown = true;
socket = new WebSocket(
`ws://${bridgeBackupHost}:${bridgeBackupPort}`);
`ws://${bridgeBackupHost}:${bridgeBackupPort}`
);
socket.addEventListener("open", () => {
// Send all messages in queue
@@ -163,30 +162,28 @@ function connectNative(application: string): Port {
}
});
return portObject;
}
async function sendNativeMessage(
application: string
, message: Message) {
async function sendNativeMessage(application: string, message: Message) {
try {
return await browser.runtime.sendNativeMessage(application, message);
} catch {
const { bridgeBackupEnabled
, bridgeBackupHost
, bridgeBackupPort } = await options.getAll();
const { bridgeBackupEnabled, bridgeBackupHost, bridgeBackupPort } =
await options.getAll();
if (!bridgeBackupEnabled) {
throw logger.error("Bridge connection failed and backup not enabled.");
throw logger.error(
"Bridge connection failed and backup not enabled."
);
}
const port = await options.get("bridgeBackupPort");
return await new Promise((resolve, reject) => {
const ws = new WebSocket(
`ws://${bridgeBackupHost}:${bridgeBackupPort}`);
`ws://${bridgeBackupHost}:${bridgeBackupPort}`
);
ws.addEventListener("open", () => {
ws.send(JSON.stringify(message));
@@ -205,8 +202,7 @@ async function sendNativeMessage(
}
}
export default {
connectNative
, sendNativeMessage
connectNative,
sendNativeMessage
};

View File

@@ -8,9 +8,8 @@ import { ReceiverSelectorType } from "../background/receiverSelector";
import { TypedEventTarget } from "./TypedEventTarget";
import { TypedStorageArea } from "./TypedStorageArea";
const storageArea = new TypedStorageArea<{
options: Options
options: Options;
}>(browser.storage.sync);
export interface Options {
@@ -35,12 +34,11 @@ export interface Options {
[key: string]: Options[keyof Options];
}
interface EventMap {
"changed": Array<keyof Options>;
changed: Array<keyof Options>;
}
export default new class extends TypedEventTarget<EventMap> {
export default new (class extends TypedEventTarget<EventMap> {
constructor() {
super();
this.onStorageChanged = this.onStorageChanged.bind(this);
@@ -53,9 +51,9 @@ export default new class extends TypedEventTarget<EventMap> {
}
private onStorageChanged(
changes: { [key: string]: browser.storage.StorageChange }
, areaName: string) {
changes: { [key: string]: browser.storage.StorageChange },
areaName: string
) {
if (areaName !== "sync") {
return;
}
@@ -80,11 +78,16 @@ export default new class extends TypedEventTarget<EventMap> {
}
// Array comparison
if (oldKeyValue instanceof Array
&& newKeyValue instanceof Array) {
if (oldKeyValue.length === newKeyValue.length
&& oldKeyValue.every((value, index) =>
value === newKeyValue[index])) {
if (
oldKeyValue instanceof Array &&
newKeyValue instanceof Array
) {
if (
oldKeyValue.length === newKeyValue.length &&
oldKeyValue.every(
(value, index) => value === newKeyValue[index]
)
) {
continue;
}
}
@@ -93,9 +96,11 @@ export default new class extends TypedEventTarget<EventMap> {
changedKeys.push(key);
}
this.dispatchEvent(new CustomEvent("changed", {
detail: changedKeys as Array<keyof Options>
}));
this.dispatchEvent(
new CustomEvent("changed", {
detail: changedKeys as Array<keyof Options>
})
);
}
}
@@ -135,15 +140,14 @@ export default new class extends TypedEventTarget<EventMap> {
* promise.
*/
public async set<T extends keyof Options>(
name: T
, value: Options[T]): Promise<void> {
name: T,
value: Options[T]
): Promise<void> {
const options = await this.getAll();
options[name] = value;
return this.setAll(options);
}
/**
* Gets existing options from storage and compares it
* against defaults. Any options in defaults and not in
@@ -153,7 +157,7 @@ export default new class extends TypedEventTarget<EventMap> {
const newOpts = await this.getAll();
// Find options not already in storage
for (const [ optName, optVal ] of Object.entries(defaults)) {
for (const [optName, optVal] of Object.entries(defaults)) {
if (!newOpts.hasOwnProperty(optName)) {
newOpts[optName] = optVal;
}
@@ -162,4 +166,4 @@ export default new class extends TypedEventTarget<EventMap> {
// Update storage with default values of new options
return this.setAll(newOpts);
}
};
})();

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_LINUX = "X11; Linux x86_64";
const UA_CHROME = "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36";
const UA_CHROME =
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36";
const UA_HYBRID = "Chrome/80.0.3987.87 Gecko/20100101 Firefox/72.0";
export function getChromeUserAgent(platform: string, hybrid = false) {
let platformComponent: string;
if (platform === "mac") {
platformComponent = hybrid
? PLATFORM_MAC_HYBRID
: PLATFORM_MAC;
platformComponent = hybrid ? PLATFORM_MAC_HYBRID : PLATFORM_MAC;
} else if (platform === "win") {
platformComponent = PLATFORM_WIN;
} else if (platform === "linux") {
@@ -22,9 +21,7 @@ export function getChromeUserAgent(platform: string, hybrid = false) {
return;
}
const browserComponent = hybrid
? UA_HYBRID
: UA_CHROME;
const browserComponent = hybrid ? UA_HYBRID : UA_CHROME;
return `Mozilla/5.0 (${platformComponent}) ${browserComponent}`;
}

View File

@@ -4,7 +4,6 @@ import logger from "./logger";
import { ReceiverSelectorMediaType } from "../background/receiverSelector";
export function getNextEllipsis(ellipsis: string): string {
if (ellipsis === "") return ".";
if (ellipsis === ".") return "..";
@@ -18,9 +17,9 @@ export function getNextEllipsis(ellipsis: string): string {
* Template literal tag function, JSON-encodes substitutions.
*/
export function stringify(
templateStrings: TemplateStringsArray
, ...substitutions: any[]) {
templateStrings: TemplateStringsArray,
...substitutions: any[]
) {
let formattedString = "";
for (const templateString of templateStrings) {
@@ -35,8 +34,8 @@ export function stringify(
}
export function getMediaTypesForPageUrl(
pageUrl: string): ReceiverSelectorMediaType {
pageUrl: string
): ReceiverSelectorMediaType {
const url = new URL(pageUrl);
let availableMediaTypes = ReceiverSelectorMediaType.File;
@@ -45,18 +44,18 @@ export function getMediaTypesForPageUrl(
* Mozilla domains.
*/
const blockedHosts = [
"accounts-static.cdn.mozilla.net"
, "accounts.firefox.com"
, "addons.cdn.mozilla.net"
, "addons.mozilla.org"
, "api.accounts.firefox.com"
, "content.cdn.mozilla.net"
, "discovery.addons.mozilla.org"
, "install.mozilla.org"
, "oauth.accounts.firefox.com"
, "profile.accounts.firefox.com"
, "support.mozilla.org"
, "sync.services.mozilla.com"
"accounts-static.cdn.mozilla.net",
"accounts.firefox.com",
"addons.cdn.mozilla.net",
"addons.mozilla.org",
"api.accounts.firefox.com",
"content.cdn.mozilla.net",
"discovery.addons.mozilla.org",
"install.mozilla.org",
"oauth.accounts.firefox.com",
"profile.accounts.firefox.com",
"support.mozilla.org",
"sync.services.mozilla.com"
];
if (blockedHosts.includes(url.host)) {
@@ -80,7 +79,6 @@ export function getMediaTypesForPageUrl(
return availableMediaTypes;
}
export interface WindowCenteredProps {
width: number;
height: number;
@@ -89,33 +87,37 @@ export interface WindowCenteredProps {
}
export function getWindowCenteredProps(
refWin: browser.windows.Window
, width: number
, height: number): WindowCenteredProps {
if (refWin.left === undefined || refWin.width === undefined
|| refWin.top === undefined || refWin.height === undefined) {
refWin: browser.windows.Window,
width: number,
height: number
): WindowCenteredProps {
if (
refWin.left === undefined ||
refWin.width === undefined ||
refWin.top === undefined ||
refWin.height === undefined
) {
throw logger.error("refWin missing positional attributes.");
}
const centerX = refWin.left + (refWin.width / 2);
const centerY = refWin.top + (refWin.height / 3);
const centerX = refWin.left + refWin.width / 2;
const centerY = refWin.top + refWin.height / 3;
return {
width, height
, left: Math.floor(centerX - width / 2)
, top: Math.floor(centerY - height / 2)
width,
height,
left: Math.floor(centerX - width / 2),
top: Math.floor(centerY - height / 2)
};
}
export const REMOTE_MATCH_PATTERN_REGEX = /^(?:(?:(\*|https?|ftp):\/\/(\*|(?:\*\.(?:[^\/\*:]\.?)+(?:[^\.])|[^\/\*:]*))?)(\/.*)|<all_urls>)$/;
export const REMOTE_MATCH_PATTERN_REGEX =
/^(?:(?:(\*|https?|ftp):\/\/(\*|(?:\*\.(?:[^\/\*:]\.?)+(?:[^\.])|[^\/\*:]*))?)(\/.*)|<all_urls>)$/;
export function loadScript(
scriptUrl: string
, doc: Document = document): HTMLScriptElement {
scriptUrl: string,
doc: Document = document
): HTMLScriptElement {
const scriptElement = doc.createElement("script");
scriptElement.src = scriptUrl;
(doc.head || doc.documentElement).append(scriptElement);

View File

@@ -4,63 +4,59 @@ import { TypedPort } from "./lib/TypedPort";
import { BridgeInfo } from "./lib/bridge";
import { ReceiverSelectorMediaType } from "./background/receiverSelector";
import { ReceiverSelection
, ReceiverSelectionCast
, ReceiverSelectionStop } from "./background/receiverSelector";
import {
ReceiverSelection,
ReceiverSelectionCast,
ReceiverSelectionStop
} from "./background/receiverSelector";
import { CastSessionCreated
, CastSessionUpdated
, ReceiverStatus
, SenderMessage } from "./shim/cast/types";
import {
CastSessionCreated,
CastSessionUpdated,
ReceiverStatus,
SenderMessage
} from "./shim/cast/types";
import { ReceiverDevice } from "./types";
/**
* Messages are JSON objects with a `subject` string key and a
* generic `data` key:
* { subject: "...", data: ... }
*
*
* Message subjects may include an optional destination and
* response name formatted like this:
* ^(destination:)?messageName(\/responseName)?$
*
*
* Message formats are specified with subject as a key and data
* as the value in the message tables.
*/
/**
* Messages exclusively used internally between extension
* components.
*/
type ExtMessageDefinitions = {
"popup:init": { appId?: string }
, "popup:update": {
receivers: ReceiverDevice[]
, defaultMediaType?: ReceiverSelectorMediaType
, availableMediaTypes?: ReceiverSelectorMediaType
}
, "popup:close": {}
, "receiverSelector:selected": ReceiverSelection
, "receiverSelector:stop": ReceiverSelection
, "main:shimReady": { appId: string }
, "main:selectReceiver": {}
, "shim:selectReceiver/selected": ReceiverSelectionCast
, "shim:selectReceiver/stopped": ReceiverSelectionStop
, "shim:selectReceiver/cancelled": {}
, "main:sessionCreated": {}
, "shim:initialized": BridgeInfo
, "shim:serviceUp": { receiverDevice: ReceiverDevice }
, "shim:serviceDown": { receiverDeviceId: ReceiverDevice["id"] }
, "shim:launchApp": { receiver: ReceiverDevice }
}
"popup:init": { appId?: string };
"popup:update": {
receivers: ReceiverDevice[];
defaultMediaType?: ReceiverSelectorMediaType;
availableMediaTypes?: ReceiverSelectorMediaType;
};
"popup:close": {};
"receiverSelector:selected": ReceiverSelection;
"receiverSelector:stop": ReceiverSelection;
"main:shimReady": { appId: string };
"main:selectReceiver": {};
"shim:selectReceiver/selected": ReceiverSelectionCast;
"shim:selectReceiver/stopped": ReceiverSelectionStop;
"shim:selectReceiver/cancelled": {};
"main:sessionCreated": {};
"shim:initialized": BridgeInfo;
"shim:serviceUp": { receiverDevice: ReceiverDevice };
"shim:serviceDown": { receiverDeviceId: ReceiverDevice["id"] };
"shim:launchApp": { receiver: ReceiverDevice };
};
/**
* Messages that cross the native messaging channel. MUST keep
@@ -68,89 +64,76 @@ type ExtMessageDefinitions = {
* app/bridge/messaging.ts > MessagesBase
*/
type AppMessageDefinitions = {
"shim:castSessionCreated": CastSessionCreated
, "shim:castSessionUpdated": CastSessionUpdated
, "shim:castSessionStopped": {
sessionId: string
}
, "shim:receivedCastSessionMessage": {
sessionId: string
, namespace: string
, messageData: string
}
, "shim:impl_sendCastMessage": {
sessionId: string
, messageId: string
, error?: string
}
, "bridge:createCastSession": {
appId: string
, receiverDevice: ReceiverDevice
}
, "bridge:sendCastReceiverMessage": {
sessionId: string
, messageData: SenderMessage
, messageId: string
}
, "bridge:sendCastSessionMessage": {
sessionId: string
, namespace: string
, messageData: object | string
, messageId: string
}
, "bridge:stopCastApp": { receiverDevice: ReceiverDevice }
"shim:castSessionCreated": CastSessionCreated;
"shim:castSessionUpdated": CastSessionUpdated;
"shim:castSessionStopped": {
sessionId: string;
};
"shim:receivedCastSessionMessage": {
sessionId: string;
namespace: string;
messageData: string;
};
"shim:impl_sendCastMessage": {
sessionId: string;
messageId: string;
error?: string;
};
"bridge:createCastSession": {
appId: string;
receiverDevice: ReceiverDevice;
};
"bridge:sendCastReceiverMessage": {
sessionId: string;
messageData: SenderMessage;
messageId: string;
};
"bridge:sendCastSessionMessage": {
sessionId: string;
namespace: string;
messageData: object | string;
messageId: string;
};
"bridge:stopCastApp": { receiverDevice: ReceiverDevice };
// Bridge messages
, "main:receiverSelector/selected": ReceiverSelectionCast
, "main:receiverSelector/stopped": ReceiverSelectionStop
, "main:receiverSelector/cancelled": {}
, "main:receiverSelector/error": string
"main:receiverSelector/selected": ReceiverSelectionCast;
"main:receiverSelector/stopped": ReceiverSelectionStop;
"main:receiverSelector/cancelled": {};
"main:receiverSelector/error": string;
/**
* getInfo uses the old :/ form for compat with old bridge
* versions.
*/
, "bridge:getInfo": string
, "bridge:/getInfo": string
, "bridge:startDiscovery": {
shouldWatchStatus: boolean
}
, "bridge:openReceiverSelector": string
, "bridge:closeReceiverSelector": {}
, "bridge:startMediaServer": {
filePath: string
, port: number
}
, "bridge:stopMediaServer": {}
, "mediaCast:mediaServerStarted": {
mediaPath: string
, subtitlePaths: string[]
, localAddress: string
}
, "mediaCast:mediaServerStopped": {}
, "mediaCast:mediaServerError": {}
, "main:receiverDeviceUp": { receiverDevice: ReceiverDevice }
, "main:receiverDeviceDown": { receiverDeviceId: string }
, "main:receiverDeviceUpdated": {
receiverDeviceId: string
, status: ReceiverStatus
}
}
type MessageDefinitions =
ExtMessageDefinitions
& AppMessageDefinitions;
"bridge:getInfo": string;
"bridge:/getInfo": string;
"bridge:startDiscovery": {
shouldWatchStatus: boolean;
};
"bridge:openReceiverSelector": string;
"bridge:closeReceiverSelector": {};
"bridge:startMediaServer": {
filePath: string;
port: number;
};
"bridge:stopMediaServer": {};
"mediaCast:mediaServerStarted": {
mediaPath: string;
subtitlePaths: string[];
localAddress: string;
};
"mediaCast:mediaServerStopped": {};
"mediaCast:mediaServerError": {};
"main:receiverDeviceUp": { receiverDevice: ReceiverDevice };
"main:receiverDeviceDown": { receiverDeviceId: string };
"main:receiverDeviceUpdated": {
receiverDeviceId: string;
status: ReceiverStatus;
};
};
type MessageDefinitions = ExtMessageDefinitions & AppMessageDefinitions;
interface MessageBase<K extends keyof MessageDefinitions> {
subject: K;
@@ -159,7 +142,7 @@ interface MessageBase<K extends keyof MessageDefinitions> {
type Messages = {
[K in keyof MessageDefinitions]: MessageBase<K>;
}
};
/**
* For better call semantics, make message data key optional if
@@ -172,39 +155,35 @@ type NarrowedMessage<L extends MessageBase<keyof MessageDefinitions>> =
: L
: never;
export type Port = TypedPort<Message>;
export type Message = NarrowedMessage<Messages[keyof Messages]>;
/**
* Typed WebExtension-style messaging utility class.
*/
class Messenger<T> {
connect(connectInfo: { name: string; }) {
return browser.runtime.connect(connectInfo) as
unknown as TypedPort<T>;
connect(connectInfo: { name: string }) {
return browser.runtime.connect(connectInfo) as unknown as TypedPort<T>;
}
connectTab(tabId: number
, connectInfo: { name: string
, frameId: number }) {
return browser.tabs.connect(tabId, connectInfo) as
unknown as TypedPort<T>;
connectTab(tabId: number, connectInfo: { name: string; frameId: number }) {
return browser.tabs.connect(
tabId,
connectInfo
) as unknown as TypedPort<T>;
}
onConnect = {
addListener(cb: (port: TypedPort<T>) => void) {
browser.runtime.onConnect.addListener(cb as any);
}
, removeListener(cb: (port: TypedPort<T>) => void) {
},
removeListener(cb: (port: TypedPort<T>) => void) {
browser.runtime.onConnect.removeListener(cb as any);
}
, hasListener(cb: (port: TypedPort<T>) => void) {
},
hasListener(cb: (port: TypedPort<T>) => void) {
return browser.runtime.onConnect.hasListener(cb as any);
}
}
};
}
export default new Messenger<Message>();

View File

@@ -7,18 +7,20 @@ import cast, { ensureInit } from "../../shim/export";
import { Message } from "../../messaging";
import { ReceiverDevice } from "../../types";
function startMediaServer(filePath: string, port: number)
: Promise<{ mediaPath: string
, subtitlePaths: string[]
, localAddress: string }> {
function startMediaServer(
filePath: string,
port: number
): Promise<{
mediaPath: string;
subtitlePaths: string[];
localAddress: string;
}> {
return new Promise((resolve, reject) => {
backgroundPort.postMessage({
subject: "bridge:startMediaServer"
, data: {
filePath: decodeURI(filePath)
, port
subject: "bridge:startMediaServer",
data: {
filePath: decodeURI(filePath),
port
}
} as Message);
@@ -45,7 +47,6 @@ function startMediaServer(filePath: string, port: number)
});
}
let backgroundPort: MessagePort;
let currentSession: cast.Session;
@@ -53,7 +54,6 @@ let currentMedia: cast.media.Media;
let targetElement: HTMLElement;
function getSession(opts: InitOptions): Promise<cast.Session> {
return new Promise(async (resolve, reject) => {
/**
@@ -64,10 +64,11 @@ function getSession(opts: InitOptions): Promise<cast.Session> {
function receiverListener(availability: string) {
if (availability === cast.ReceiverAvailability.AVAILABLE) {
cast.requestSession(
onRequestSessionSuccess
, onRequestSessionError
, undefined
, opts.receiver);
onRequestSessionSuccess,
onRequestSessionError,
undefined,
opts.receiver
);
}
}
@@ -82,15 +83,15 @@ function getSession(opts: InitOptions): Promise<cast.Session> {
reject(err.description);
}
const sessionRequest = new cast.SessionRequest(
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID
);
const apiConfig = new cast.ApiConfig(
sessionRequest
, sessionListener // sessionListener
, receiverListener); // receiverListener
sessionRequest,
sessionListener, // sessionListener
receiverListener
); // receiverListener
cast.initialize(apiConfig);
});
@@ -113,19 +114,18 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
try {
// Wait until media server is listening
const { localAddress, mediaPath, subtitlePaths } =
await startMediaServer(mediaTitle, port);
await startMediaServer(mediaTitle, port);
const baseUrl = new URL(`http://${localAddress}:${port}/`);
mediaUrl = new URL(mediaPath, baseUrl);
subtitleUrls = subtitlePaths.map(
path => new URL(path, baseUrl));
path => new URL(path, baseUrl)
);
} catch (err) {
throw logger.error("Failed to start media server");
}
}
const activeTrackIds: number[] = [];
const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, "");
@@ -137,7 +137,9 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
let trackIndex = 0;
for (const subtitleUrl of subtitleUrls) {
const castTrack = new cast.media.Track(
trackIndex, cast.media.TrackType.TEXT);
trackIndex,
cast.media.TrackType.TEXT
);
castTrack.name = subtitleUrl.pathname;
castTrack.trackContentId = subtitleUrl.href;
@@ -168,7 +170,9 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
* and type as TrackType.TEXT.
*/
const castTrack = new cast.media.Track(
trackIndex, cast.media.TrackType.TEXT);
trackIndex,
cast.media.TrackType.TEXT
);
// Copy TextTrack properties
castTrack.name = track.label || `track-${trackIndex}`;
@@ -179,29 +183,29 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
switch (track.kind) {
case "subtitles":
castTrack.subtype =
cast.media.TextTrackType.SUBTITLES;
cast.media.TextTrackType.SUBTITLES;
break;
case "captions":
castTrack.subtype =
cast.media.TextTrackType.CAPTIONS;
cast.media.TextTrackType.CAPTIONS;
break;
case "descriptions":
castTrack.subtype =
cast.media.TextTrackType.DESCRIPTIONS;
cast.media.TextTrackType.DESCRIPTIONS;
break;
case "chapters":
castTrack.subtype =
cast.media.TextTrackType.CHAPTERS;
cast.media.TextTrackType.CHAPTERS;
break;
case "metadata":
castTrack.subtype =
cast.media.TextTrackType.METADATA;
cast.media.TextTrackType.METADATA;
break;
// Default to subtitles
default:
castTrack.subtype =
cast.media.TextTrackType.SUBTITLES;
cast.media.TextTrackType.SUBTITLES;
}
// Add track to mediaInfo
@@ -225,7 +229,6 @@ function getMedia(opts: InitOptions): Promise<cast.media.Media> {
});
}
let ignoreMediaEvents = false;
async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
@@ -244,7 +247,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
mediaElement.addEventListener("ratechange", checkIgnore, true);
mediaElement.addEventListener("volumechange", checkIgnore, true);
mediaElement.addEventListener("play", () => {
currentMedia.play();
});
@@ -270,15 +272,15 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
mediaElement.addEventListener("volumechange", () => {
const newVolume = new cast.Volume(
currentMedia.volume.level
, currentMedia.volume.muted);
currentMedia.volume.level,
currentMedia.volume.muted
);
const volumeRequest = new cast.media.VolumeRequest(newVolume);
currentMedia.setVolume(volumeRequest);
});
currentMedia.addUpdateListener(isAlive => {
if (!isAlive) {
return;
@@ -303,7 +305,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
}
}
const localRepeatMode = mediaElement.loop
? cast.media.RepeatMode.SINGLE
: cast.media.RepeatMode.OFF;
@@ -323,7 +324,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
}
}
if (currentMedia.currentTime !== mediaElement.currentTime) {
ignoreMediaEvents = true;
mediaElement.currentTime = currentMedia.currentTime;
@@ -332,7 +332,6 @@ async function registerMediaElementListeners(mediaElement: HTMLMediaElement) {
}
}
interface InitOptions {
mediaUrl: string;
receiver?: ReceiverDevice;
@@ -355,7 +354,8 @@ export async function init(opts: InitOptions) {
}
targetElement = browser.menus.getTargetElement(
opts.targetElementId) as HTMLMediaElement;
opts.targetElementId
) as HTMLMediaElement;
currentSession = await getSession(opts);
currentMedia = await getMedia(opts);
@@ -384,11 +384,11 @@ export async function init(opts: InitOptions) {
* provided on the window object.
*/
if (window.location.protocol !== "moz-extension:") {
const _window = (window as any);
const _window = window as any;
init({
mediaUrl: _window.mediaUrl
, receiver: _window.receiver
, targetElementId: _window.targetElementId
mediaUrl: _window.mediaUrl,
receiver: _window.receiver,
targetElementId: _window.targetElementId
});
}

View File

@@ -5,15 +5,13 @@
* descriptor is found, otherwise return undefined.
*/
export function getPropertyDescriptor(
target: any, prop: string | number | symbol)
: PropertyDescriptor | undefined {
target: any,
prop: string | number | symbol
): PropertyDescriptor | undefined {
let desc: PropertyDescriptor | undefined;
while (!desc && target !== null) {
desc = Object.getOwnPropertyDescriptor(target, prop);
if (!desc) {
target = Object.getPrototypeOf(target);
}
if (!desc) target = Object.getPrototypeOf(target);
}
return desc;
@@ -24,14 +22,14 @@ export function getPropertyDescriptor(
* to a target object.
*/
export function bindPropertyDescriptor(
desc: PropertyDescriptor, target: any)
: PropertyDescriptor {
desc: PropertyDescriptor,
target: any
): PropertyDescriptor {
if (typeof desc.value === "function") {
desc.value = desc.value.bind(target);
} else {
if (desc.get) { desc.get = desc.get.bind(target); }
if (desc.set) { desc.set = desc.set.bind(target); }
if (desc.get) desc.get = desc.get.bind(target);
if (desc.set) desc.set = desc.set.bind(target);
}
return desc;
@@ -43,9 +41,9 @@ export function bindPropertyDescriptor(
* element and collect them into a property descriptor map.
*/
export function clonePropsDescriptor<T>(
target: T, props: any[])
: PropertyDescriptorMap {
target: T,
props: any[]
): PropertyDescriptorMap {
return props.reduce<PropertyDescriptorMap>((descriptorMap, prop) => {
const desc = getPropertyDescriptor(target, prop);
if (desc) {
@@ -59,17 +57,19 @@ export function clonePropsDescriptor<T>(
export function makeGetterDescriptor(val: any): PropertyDescriptor {
return {
enumerable: true
, configurable: true
, get() { return val; }
enumerable: true,
configurable: true,
get() {
return val;
}
};
}
export function makeValueDescriptor(val: any): PropertyDescriptor {
return {
enumerable: true
, configurable: true
, writable: true
, value: val
enumerable: true,
configurable: true,
writable: true,
value: val
};
}

View File

@@ -2,18 +2,19 @@
import logger from "../../../lib/logger";
import { bindPropertyDescriptor
, clonePropsDescriptor
, getPropertyDescriptor
, makeGetterDescriptor
, makeValueDescriptor } from "./descriptorUtils";
import {
bindPropertyDescriptor,
clonePropsDescriptor,
getPropertyDescriptor,
makeGetterDescriptor,
makeValueDescriptor
} from "./descriptorUtils";
// Injected by content loader
declare const iconAirPlayAudio: string;
declare const iconAirPlayVideo: string;
declare const mediaOverlayTitle: string;
/**
* Intercept and store references to shadow root nodes created by
* calls to `attachShadow`. Used to reference shadow roots, even when
@@ -27,7 +28,6 @@ Element.prototype.attachShadow = function (init) {
return shadowRoot;
};
function getShadowRootFromNode(node: Node): ShadowRoot | undefined {
// Don't touch our custom element
if (node instanceof PlayerElement) {
@@ -37,7 +37,6 @@ function getShadowRootFromNode(node: Node): ShadowRoot | undefined {
return internalShadowRoots.get(node as Element);
}
const DQS_XPATH_EXPRESSION = `//*[contains(name(), "-")]`;
/**
@@ -46,12 +45,15 @@ const DQS_XPATH_EXPRESSION = `//*[contains(name(), "-")]`;
*/
function deepQuerySelector(selector: string): Element | null {
const result = document.evaluate(
DQS_XPATH_EXPRESSION, document, null
, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
DQS_XPATH_EXPRESSION,
document,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE
);
let node: Node | null;
// eslint-disable-next-line no-cond-assign
while (node = result.iterateNext()) {
while ((node = result.iterateNext())) {
const shadowRoot = getShadowRootFromNode(node);
if (!shadowRoot) {
continue;
@@ -72,14 +74,17 @@ function deepQuerySelector(selector: string): Element | null {
*/
function deepQuerySelectorAll(selector: string): Node[] {
const result = document.evaluate(
DQS_XPATH_EXPRESSION, document, null
, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
DQS_XPATH_EXPRESSION,
document,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE
);
const nodes: Node[] = [];
let node: Node | null;
// eslint-disable-next-line no-cond-assign
while (node = result.iterateNext()) {
while ((node = result.iterateNext())) {
const shadowRoot = getShadowRootFromNode(node);
if (shadowRoot) {
nodes.push(...shadowRoot.querySelectorAll(selector));
@@ -89,26 +94,45 @@ function deepQuerySelectorAll(selector: string): Node[] {
return nodes;
}
const mediaElementTypes = [
HTMLMediaElement
, HTMLVideoElement
, HTMLAudioElement
HTMLMediaElement,
HTMLVideoElement,
HTMLAudioElement
];
const mediaElementEvents = [
"abort", "canplay", "canplaythrough", "durationchange", "emptied"
, "encrypted", "ended", "error", "interruptbegin", "interruptend"
, "loadeddata", "loadedmetadata", "loadstart", "mozaudioavailable", "pause"
, "play", "playing", "progress", "ratechange", "seeked", "seeking", "stalled"
, "suspend", "timeupdate", "volumechange", "waiting"
"abort",
"canplay",
"canplaythrough",
"durationchange",
"emptied",
"encrypted",
"ended",
"error",
"interruptbegin",
"interruptend",
"loadeddata",
"loadedmetadata",
"loadstart",
"mozaudioavailable",
"pause",
"play",
"playing",
"progress",
"ratechange",
"seeked",
"seeking",
"stalled",
"suspend",
"timeupdate",
"volumechange",
"waiting"
];
const mediaElementAttributes = mediaElementTypes
.flatMap(type => Object.getOwnPropertyNames(type.prototype))
.concat(mediaElementEvents.map(ev => `on${ev}`));
/**
* Opaque wrapper around the media element to provide an overlay without
* author interference. Relevant properties, attributes, events and
@@ -125,8 +149,14 @@ class PlayerElement extends HTMLElement {
switch (this.constructor) {
// URL variables injected ahead of current script
case AudioPlayerElement: { iconUrl = iconAirPlayAudio; break; }
case VideoPlayerElement: { iconUrl = iconAirPlayVideo; break; }
case AudioPlayerElement: {
iconUrl = iconAirPlayAudio;
break;
}
case VideoPlayerElement: {
iconUrl = iconAirPlayVideo;
break;
}
}
shadowRoot.innerHTML = `
@@ -192,10 +222,19 @@ class PlayerElement extends HTMLElement {
* listeners, etc... on the media element, but since it's hidden
* within the shadow DOM, these properties must be proxied.
*/
Object.defineProperties(host, clonePropsDescriptor(videoElement, [
"attributes", "setAttribute", "removeAttribute", "setAttribute"
, "addEventListener", "removeEventListener", "hasEventListener"
, ...mediaElementAttributes as any ]));
Object.defineProperties(
host,
clonePropsDescriptor(videoElement, [
"attributes",
"setAttribute",
"removeAttribute",
"setAttribute",
"addEventListener",
"removeEventListener",
"hasEventListener",
...(mediaElementAttributes as any)
])
);
shadowRoot.prepend(videoElement);
}
@@ -217,13 +256,14 @@ try {
customElements.define("audio-player-element", AudioPlayerElement);
customElements.define("video-player-element", VideoPlayerElement);
} catch (err) {
if (err instanceof DOMException
&& err.code === DOMException.NOT_SUPPORTED_ERR) {
if (
err instanceof DOMException &&
err.code === DOMException.NOT_SUPPORTED_ERR
) {
// Script already injected
}
}
// Original functions
const _createElement = document.createElement;
const _createElementNS = document.createElementNS;
@@ -233,23 +273,22 @@ const _createElementNS = document.createElementNS;
* custom element version that imitates the original. Otherwise, returns
* the result of the original.
*/
function createElement(
tagName: string
, options?: ElementCreationOptions) {
function createElement(tagName: string, options?: ElementCreationOptions) {
// Normalize formatting
const lowerTagName = tagName.toLowerCase();
const upperTagName = tagName.toUpperCase();
if (lowerTagName === "audio" || lowerTagName === "video") {
const fakeElement = _createElement.call(document
, `${lowerTagName}-player-element`) as HTMLMediaElement;
const fakeElement = _createElement.call(
document,
`${lowerTagName}-player-element`
) as HTMLMediaElement;
// Ensure all references to the element name match tagName
Object.defineProperties(fakeElement, {
tagName: makeGetterDescriptor(upperTagName)
, nodeName: makeGetterDescriptor(upperTagName)
, localName: makeGetterDescriptor(lowerTagName)
tagName: makeGetterDescriptor(upperTagName),
nodeName: makeGetterDescriptor(upperTagName),
localName: makeGetterDescriptor(lowerTagName)
});
return fakeElement;
@@ -264,34 +303,41 @@ function createElement(
* original.
*/
function createElementNS(
namespaceURI: string
, qualifiedName: string
, options?: ElementCreationOptions) {
namespaceURI: string,
qualifiedName: string,
options?: ElementCreationOptions
) {
if (namespaceURI === document.namespaceURI) {
return createElement(qualifiedName, options);
}
return _createElementNS.call(document
, namespaceURI, qualifiedName, options);
return _createElementNS.call(
document,
namespaceURI,
qualifiedName,
options
);
}
/**
* Attempt to hide function source from page scripts by returning the
* toString/toSource values of the native function.
*/
Object.defineProperties(createElement, clonePropsDescriptor(
_createElement, [ "toString", "toSource" ]));
Object.defineProperties(createElementNS, clonePropsDescriptor(
_createElementNS, [ "toString", "toSource" ]));
Object.defineProperties(
createElement,
clonePropsDescriptor(_createElement, ["toString", "toSource"])
);
Object.defineProperties(
createElementNS,
clonePropsDescriptor(_createElementNS, ["toString", "toSource"])
);
// Re-define element creation functions
Object.defineProperties(document, {
createElement: makeValueDescriptor(createElement)
, createElementNS: makeValueDescriptor(createElementNS)
createElement: makeValueDescriptor(createElement),
createElementNS: makeValueDescriptor(createElementNS)
});
/**
* Takes a media element, creates a `PlayerElement` via the patched
* `createElement` function, fetches the shadow root and copies any
@@ -321,7 +367,10 @@ function wrapMediaElement(mediaElement: HTMLMediaElement) {
* internal media element instead.
*/
HTMLElement.prototype.setAttribute.call(
wrappedMedia, attr.name, attr.value);
wrappedMedia,
attr.name,
attr.value
);
}
}
@@ -362,7 +411,7 @@ document.addEventListener("DOMContentLoaded", () => {
const mediaElements = document.querySelectorAll(mediaSelector);
const deepMediaElements = deepQuerySelectorAll(mediaSelector);
for (const mediaElement of [ ...mediaElements, ...deepMediaElements ]) {
for (const mediaElement of [...mediaElements, ...deepMediaElements]) {
wrapMediaElement(mediaElement as HTMLMediaElement);
}
});

View File

@@ -7,17 +7,22 @@ const _ = browser.i18n.getMessage;
* scripts from loading before its execution.
*/
const req = new XMLHttpRequest();
req.open("GET", browser.runtime.getURL(
"senders/media/overlay/overlayContent.js"), false);
req.open(
"GET",
browser.runtime.getURL("senders/media/overlay/overlayContent.js"),
false
);
req.send();
if (req.status === 200) {
// TODO: Replace with cast icons until AirPlay support is ready
const iconAirPlayAudio = browser.runtime.getURL(
"senders/media/overlay/AirPlay_Audio.svg");
"senders/media/overlay/AirPlay_Audio.svg"
);
const iconAirPlayVideo = browser.runtime.getURL(
"senders/media/overlay/AirPlay_Audio.svg");
"senders/media/overlay/AirPlay_Audio.svg"
);
const scriptElement = document.createElement("script");
scriptElement.textContent = `(function(){

View File

@@ -6,23 +6,22 @@ import cast, { ensureInit } from "../shim/export";
import { ReceiverSelectorMediaType } from "../background/receiverSelector";
import { ReceiverDevice } from "../types";
// Variables passed from background
const { selectedMedia
, selectedReceiver }
: { selectedMedia: ReceiverSelectorMediaType
, selectedReceiver: ReceiverDevice } = (window as any);
const {
selectedMedia,
selectedReceiver
}: {
selectedMedia: ReceiverSelectorMediaType;
selectedReceiver: ReceiverDevice;
} = window as any;
const FX_CAST_RECEIVER_APP_NAMESPACE = "urn:x-cast:fx_cast";
let session: cast.Session;
let wasSessionRequested = false;
let peerConnection: RTCPeerConnection;
/**
* Sends a message to the fx_cast app running on the
* receiver device.
@@ -33,40 +32,39 @@ function sendAppMessage(subject: string, data: any) {
}
session.sendMessage(FX_CAST_RECEIVER_APP_NAMESPACE, {
subject
, data
subject,
data
});
}
window.addEventListener("beforeunload", () => {
sendAppMessage("close", null);
});
async function onRequestSessionSuccess(newSession: cast.Session) {
cast.logMessage("onRequestSessionSuccess");
session = newSession;
session.addMessageListener(FX_CAST_RECEIVER_APP_NAMESPACE
, async (_namespace, message) => {
session.addMessageListener(
FX_CAST_RECEIVER_APP_NAMESPACE,
async (_namespace, message) => {
const { subject, data } = JSON.parse(message);
const { subject, data } = JSON.parse(message);
switch (subject) {
case "peerConnectionAnswer": {
peerConnection.setRemoteDescription(data);
break;
}
case "iceCandidate": {
peerConnection.addIceCandidate(data);
break;
switch (subject) {
case "peerConnectionAnswer": {
peerConnection.setRemoteDescription(data);
break;
}
case "iceCandidate": {
peerConnection.addIceCandidate(data);
break;
}
}
}
});
);
peerConnection = new RTCPeerConnection();
peerConnection.addEventListener("icecandidate", (ev) => {
peerConnection.addEventListener("icecandidate", ev => {
sendAppMessage("iceCandidate", ev.candidate);
});
@@ -95,27 +93,29 @@ async function onRequestSessionSuccess(newSession: cast.Session) {
// TODO: Test performance
const drawFlags =
ctx.DRAWWINDOW_DRAW_CARET
| ctx.DRAWWINDOW_DRAW_VIEW
| ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES
| ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
ctx.DRAWWINDOW_DRAW_CARET |
ctx.DRAWWINDOW_DRAW_VIEW |
ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
let lastFrame: DOMHighResTimeStamp;
window.requestAnimationFrame(
function draw(now: DOMHighResTimeStamp) {
window.requestAnimationFrame(function draw(
now: DOMHighResTimeStamp
) {
if (!lastFrame) {
lastFrame = now;
}
if ((now - lastFrame) > (1000 / 30)) {
if (now - lastFrame > 1000 / 30) {
ctx.drawWindow(
window // window
, 0, 0 // x, y
, canvas.width // w
, canvas.height // h
, "white" // bgColor
, drawFlags); // flags
window, // window
0,
0, // x, y
canvas.width, // w
canvas.height, // h
"white", // bgColor
drawFlags
); // flags
lastFrame = now;
}
@@ -134,8 +134,8 @@ async function onRequestSessionSuccess(newSession: cast.Session) {
case ReceiverSelectorMediaType.Screen: {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: "motion" }
, audio: false
video: { cursor: "motion" },
audio: false
});
peerConnection.addStream(stream);
@@ -152,7 +152,6 @@ async function onRequestSessionSuccess(newSession: cast.Session) {
sendAppMessage("peerConnectionOffer", offer);
}
function receiverListener(availability: string) {
cast.logMessage("receiverListener");
@@ -162,14 +161,15 @@ function receiverListener(availability: string) {
if (availability === cast.ReceiverAvailability.AVAILABLE) {
wasSessionRequested = true;
cast.requestSession(onRequestSessionSuccess
, onRequestSessionError
, undefined
, selectedReceiver);
cast.requestSession(
onRequestSessionSuccess,
onRequestSessionError,
undefined,
selectedReceiver
);
}
}
function onRequestSessionError() {
cast.logMessage("onRequestSessionError");
}
@@ -183,18 +183,17 @@ function onInitializeError() {
cast.logMessage("onInitializeError");
}
ensureInit().then(async () => {
const mirroringAppId = await options.get("mirroringAppId");
const sessionRequest = new cast.SessionRequest(mirroringAppId);
const apiConfig = new cast.ApiConfig(
sessionRequest
, sessionListener
, receiverListener
, undefined, undefined);
sessionRequest,
sessionListener,
receiverListener,
undefined,
undefined
);
cast.initialize(apiConfig
, onInitializeSuccess
, onInitializeError);
cast.initialize(apiConfig, onInitializeSuccess, onInitializeError);
});

View File

@@ -6,32 +6,34 @@ import logger from "../../lib/logger";
import { sendMessageResponse } from "../eventMessageChannel";
import { ErrorCallback
, LoadSuccessCallback
, MediaListener
, MessageListener
, SuccessCallback
, UpdateListener } from "../types";
import {
ErrorCallback,
LoadSuccessCallback,
MediaListener,
MessageListener,
SuccessCallback,
UpdateListener
} from "../types";
import { MediaStatus
, ReceiverMediaMessage
, SenderMediaMessage
, SenderMessage } from "./types";
import {
MediaStatus,
ReceiverMediaMessage,
SenderMediaMessage,
SenderMessage
} from "./types";
import { Image, Receiver, SenderApplication } from "./dataClasses";
import { SessionStatus } from "./enums";
import { Media, LoadRequest, QueueLoadRequest, QueueItem } from "./media";
const NS_MEDIA = "urn:x-cast:com.google.cast.media";
/**
* Takes a media object and a media status object and merges
* the status with the existing media object, updating it with
* new properties.
*/
function updateMedia(media: Media, status: MediaStatus) {
function updateMedia(media: Media, status: MediaStatus) {
if (status.currentTime) {
media._lastUpdateTime = Date.now();
}
@@ -51,7 +53,8 @@ const NS_MEDIA = "urn:x-cast:com.google.cast.media";
if (!newItem.media) {
// Existing queue item with the same ID
const existingItem = media.items?.find(
item => item.itemId === newItem.itemId);
item => item.itemId === newItem.itemId
);
/**
* Use existing queue item's media info if available
@@ -60,8 +63,10 @@ const NS_MEDIA = "urn:x-cast:com.google.cast.media";
*/
if (existingItem?.media) {
newItem.media = existingItem.media;
} else if (media.media
&& newItem.itemId === media.currentItemId) {
} else if (
media.media &&
newItem.itemId === media.currentItemId
) {
newItem.media = media.media;
}
}
@@ -73,7 +78,6 @@ const NS_MEDIA = "urn:x-cast:com.google.cast.media";
}
}
export default class Session {
#id = uuid();
@@ -86,14 +90,15 @@ export default class Session {
_messageListeners = new Map<string, Set<MessageListener>>();
_updateListeners = new Set<UpdateListener>();
_sendMessageCallbacks =
new Map<string, [ SuccessCallback?, ErrorCallback? ]>();
_sendMessageCallbacks = new Map<
string,
[SuccessCallback?, ErrorCallback?]
>();
/**
*
*
*/
#mediaMessageListener = (namespace: string, messageString: string) => {
#mediaMessageListener = (namespace: string, messageString: string) => {
if (namespace !== NS_MEDIA) return;
const message: ReceiverMediaMessage = JSON.parse(messageString);
@@ -102,18 +107,20 @@ export default class Session {
// Update media
for (const mediaStatus of message.status) {
let media = this.media.find(
media => media.mediaSessionId ===
mediaStatus.mediaSessionId);
media =>
media.mediaSessionId === mediaStatus.mediaSessionId
);
console.info(media);
// Handle Media creation
if (!media) {
media = new Media(
this.sessionId
, mediaStatus.mediaSessionId
, this.#sendMediaMessage);
this.sessionId,
mediaStatus.mediaSessionId,
this.#sendMediaMessage
);
this.media.push(media);
this.#loadMediaSuccessCallback?.(media);
}
@@ -128,44 +135,43 @@ export default class Session {
}
}
}
}
};
/**
* Sends a media message to the app receiver.
* urn:x-cast:com.google.cast.media
*/
#sendMediaMessage = (message: DistributiveOmit<
SenderMediaMessage, "requestId">) => {
#sendMediaMessage = (
message: DistributiveOmit<SenderMediaMessage, "requestId">
) => {
return new Promise<void>((resolve, reject) => {
this.sendMessage(
"urn:x-cast:com.google.cast.media"
, { ...message, requestId: 0 }
, resolve, reject);
"urn:x-cast:com.google.cast.media",
{ ...message, requestId: 0 },
resolve,
reject
);
});
}
#sendReceiverMessage = (message: DistributiveOmit<
SenderMessage, "requestId">) => {
};
#sendReceiverMessage = (
message: DistributiveOmit<SenderMessage, "requestId">
) => {
return new Promise<void>((resolve, reject) => {
const messageId = uuid();
sendMessageResponse({
subject: "bridge:sendCastReceiverMessage"
, data: {
sessionId: this.sessionId
, messageData: message as SenderMessage
, messageId
subject: "bridge:sendCastReceiverMessage",
data: {
sessionId: this.sessionId,
messageData: message as SenderMessage,
messageId
}
});
this._sendMessageCallbacks.set(
messageId, [ resolve, reject ]);
this._sendMessageCallbacks.set(messageId, [resolve, reject]);
});
}
};
media: Media[] = [];
namespaces: Array<{ name: string }> = [];
@@ -174,18 +180,18 @@ export default class Session {
statusText: Nullable<string> = null;
transportId: string;
constructor(public sessionId: string
, public appId: string
, public displayName: string
, public appImages: Image[]
, public receiver: Receiver) {
constructor(
public sessionId: string,
public appId: string,
public displayName: string,
public appImages: Image[],
public receiver: Receiver
) {
this.transportId = sessionId || "";
this.addMessageListener(NS_MEDIA, this.#mediaMessageListener);
}
addMediaListener(_mediaListener: MediaListener) {
logger.info("STUB :: Session#addMediaListener");
}
@@ -211,83 +217,80 @@ export default class Session {
this._updateListeners.delete(listener);
}
leave(_successCallback?: SuccessCallback
, _errorCallback?: ErrorCallback) {
leave(_successCallback?: SuccessCallback, _errorCallback?: ErrorCallback) {
logger.info("STUB :: Session#leave");
}
loadMedia(loadRequest: LoadRequest
, successCallback?: LoadSuccessCallback
, errorCallback?: ErrorCallback) {
loadMedia(
loadRequest: LoadRequest,
successCallback?: LoadSuccessCallback,
errorCallback?: ErrorCallback
) {
this.#loadMediaSuccessCallback = successCallback;
this.#loadMediaErrorCallback = errorCallback;
this.#loadMediaRequest = loadRequest;
loadRequest.sessionId = this.sessionId;
this.#sendMediaMessage(loadRequest)
.catch(errorCallback);
this.#sendMediaMessage(loadRequest).catch(errorCallback);
}
queueLoad(_queueLoadRequest: QueueLoadRequest
, _successCallback?: LoadSuccessCallback
, _errorCallback?: ErrorCallback) {
queueLoad(
_queueLoadRequest: QueueLoadRequest,
_successCallback?: LoadSuccessCallback,
_errorCallback?: ErrorCallback
) {
logger.info("STUB :: Session#queueLoad");
}
sendMessage(namespace: string
, message: object | string
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
sendMessage(
namespace: string,
message: object | string,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
const messageId = uuid();
sendMessageResponse({
subject: "bridge:sendCastSessionMessage"
, data: {
sessionId: this.sessionId
, namespace
, messageData: message
, messageId
subject: "bridge:sendCastSessionMessage",
data: {
sessionId: this.sessionId,
namespace,
messageData: message,
messageId
}
});
this._sendMessageCallbacks.set(messageId, [
successCallback
, errorCallback
successCallback,
errorCallback
]);
}
setReceiverMuted(muted: boolean
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendReceiverMessage(
{ type: "SET_VOLUME"
, volume: { muted }})
setReceiverMuted(
muted: boolean,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this.#sendReceiverMessage({ type: "SET_VOLUME", volume: { muted } })
.then(successCallback)
.catch(errorCallback);
}
setReceiverVolumeLevel(newLevel: number
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendReceiverMessage(
{ type: "SET_VOLUME"
, volume: { level: newLevel }})
setReceiverVolumeLevel(
newLevel: number,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this.#sendReceiverMessage({
type: "SET_VOLUME",
volume: { level: newLevel }
})
.then(successCallback)
.catch(errorCallback);
}
stop(successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendReceiverMessage(
{ type: "STOP"
, sessionId: this.sessionId })
stop(successCallback?: SuccessCallback, errorCallback?: ErrorCallback) {
this.#sendReceiverMessage({ type: "STOP", sessionId: this.sessionId })
.then(successCallback)
.catch(errorCallback);
}

View File

@@ -2,48 +2,44 @@
import Session from "./Session";
import { AutoJoinPolicy
, Capability
, DefaultActionPolicy
, ReceiverType
, VolumeControlType } from "./enums";
import {
AutoJoinPolicy,
Capability,
DefaultActionPolicy,
ReceiverType,
VolumeControlType
} from "./enums";
export class ApiConfig {
constructor(
public sessionRequest: SessionRequest
, public sessionListener: (session: Session) => void
, public receiverListener: (availability: string) => void
public sessionRequest: SessionRequest,
public sessionListener: (session: Session) => void,
public receiverListener: (availability: string) => void,
, public autoJoinPolicy: string
= AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED
, public defaultActionPolicy: string
= DefaultActionPolicy.CREATE_SESSION) {}
public autoJoinPolicy: string = AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED,
public defaultActionPolicy: string = DefaultActionPolicy.CREATE_SESSION
) {}
}
export class CredentialsData {
constructor(
public credentials: string
, public credentialsData: string) {}
constructor(public credentials: string, public credentialsData: string) {}
}
export class DialRequest {
constructor(
public appName: string
, public launchParameter: Nullable<string> = null) {}
public appName: string,
public launchParameter: Nullable<string> = null
) {}
}
export class Error {
constructor(
public code: string
, public description: Nullable<string> = null
, public details: any = null) {}
public code: string,
public description: Nullable<string> = null,
public details: any = null
) {}
}
export class Image {
width: Nullable<number> = null;
height: Nullable<number> = null;
@@ -51,29 +47,25 @@ export class Image {
constructor(public url: string) {}
}
export class Receiver {
displayStatus: Nullable<ReceiverDisplayStatus> = null;
isActiveInput: Nullable<boolean> = null;
receiverType = ReceiverType.CAST;
constructor(
public label: string
, public friendlyName: string
, public capabilities: Capability[] = []
, public volume: Nullable<Volume> = null) {}
public label: string,
public friendlyName: string,
public capabilities: Capability[] = [],
public volume: Nullable<Volume> = null
) {}
}
export class ReceiverDisplayStatus {
showStop: Nullable<boolean> = null;
constructor(
public statusText: string
, public appImages: Image[]) {}
constructor(public statusText: string, public appImages: Image[]) {}
}
export class SenderApplication {
packageId: Nullable<string> = null;
url: Nullable<string> = null;
@@ -81,20 +73,18 @@ export class SenderApplication {
constructor(public platform: string) {}
}
export class SessionRequest {
language: Nullable<string> = null;
constructor(
public appId: string
, public capabilities = [ Capability.VIDEO_OUT
, Capability.AUDIO_OUT ]
, public requestSessionTimeout = (new Timeout()).requestSession
, public androidReceiverCompatible = false
, public credentialsData: Nullable<CredentialsData> = null) {}
public appId: string,
public capabilities = [Capability.VIDEO_OUT, Capability.AUDIO_OUT],
public requestSessionTimeout = new Timeout().requestSession,
public androidReceiverCompatible = false,
public credentialsData: Nullable<CredentialsData> = null
) {}
}
export class Timeout {
leaveSession = 3000;
requestSession = 60000;
@@ -103,12 +93,12 @@ export class Timeout {
stopSession = 3000;
}
export class Volume {
controlType?: VolumeControlType;
stepInterval?: number;
constructor(
public level: Nullable<number> = null
, public muted: Nullable<boolean> = null) {}
public level: Nullable<number> = null,
public muted: Nullable<boolean> = null
) {}
}

View File

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

View File

@@ -7,29 +7,47 @@ import { ErrorCallback, SuccessCallback } from "../types";
import { onMessage, sendMessageResponse } from "../eventMessageChannel";
import { AutoJoinPolicy, Capability, DefaultActionPolicy, DialAppState
, ErrorCode, ReceiverAction, ReceiverAvailability, ReceiverType
, SenderPlatform, SessionStatus, VolumeControlType } from "./enums";
import {
AutoJoinPolicy,
Capability,
DefaultActionPolicy,
DialAppState,
ErrorCode,
ReceiverAction,
ReceiverAvailability,
ReceiverType,
SenderPlatform,
SessionStatus,
VolumeControlType
} from "./enums";
import { ApiConfig, CredentialsData, DialRequest, Error as Error_, Image
, Receiver, ReceiverDisplayStatus, SenderApplication, SessionRequest
, Timeout, Volume } from "./dataClasses";
import {
ApiConfig,
CredentialsData,
DialRequest,
Error as Error_,
Image,
Receiver,
ReceiverDisplayStatus,
SenderApplication,
SessionRequest,
Timeout,
Volume
} from "./dataClasses";
import Session from "./Session";
type ReceiverActionListener = (
receiver: Receiver
, receiverAction: string) => void;
receiver: Receiver,
receiverAction: string
) => void;
type RequestSessionSuccessCallback = (session: Session) => void;
let apiConfig: Nullable<ApiConfig>;
let sessionRequest: Nullable<SessionRequest>;
let requestSessionSuccessCallback: Nullable<
RequestSessionSuccessCallback>;
let requestSessionSuccessCallback: Nullable<RequestSessionSuccessCallback>;
let requestSessionErrorCallback: Nullable<ErrorCallback>;
const receiverActionListeners = new Set<ReceiverActionListener>();
@@ -37,16 +55,36 @@ const receiverActionListeners = new Set<ReceiverActionListener>();
const receiverDevices = new Map<string, ReceiverDevice>();
const sessions = new Map<string, Session>();
export {
AutoJoinPolicy,
Capability,
DefaultActionPolicy,
DialAppState,
ErrorCode,
ReceiverAction,
ReceiverAvailability,
ReceiverType,
SenderPlatform,
SessionStatus,
VolumeControlType
};
export { AutoJoinPolicy, Capability, DefaultActionPolicy, DialAppState
, ErrorCode, ReceiverAction, ReceiverAvailability, ReceiverType
, SenderPlatform, SessionStatus, VolumeControlType };
export {
ApiConfig,
CredentialsData,
DialRequest,
Error_ as Error,
Image,
Receiver,
ReceiverDisplayStatus,
SenderApplication,
SessionRequest,
Timeout,
Volume,
Session
};
export { ApiConfig, CredentialsData, DialRequest, Error_ as Error, Image
, Receiver, ReceiverDisplayStatus, SenderApplication, SessionRequest
, Timeout, Volume, Session };
export const VERSION = [ 1, 2 ];
export const VERSION = [1, 2];
export let isAvailable = false;
export const timeout = new Timeout();
@@ -54,31 +92,33 @@ export const timeout = new Timeout();
// chrome.cast.media namespace
export * as media from "./media";
function sendSessionRequest(sessionRequest: SessionRequest
, receiverDevice: ReceiverDevice) {
function sendSessionRequest(
sessionRequest: SessionRequest,
receiverDevice: ReceiverDevice
) {
for (const listener of receiverActionListeners) {
const receiver = new Receiver(
receiverDevice.id
, receiverDevice.friendlyName);
receiverDevice.id,
receiverDevice.friendlyName
);
listener(receiver, ReceiverAction.CAST);
}
sendMessageResponse({
subject: "bridge:createCastSession"
, data: {
appId: sessionRequest.appId
, receiverDevice: receiverDevice
subject: "bridge:createCastSession",
data: {
appId: sessionRequest.appId,
receiverDevice: receiverDevice
}
});
}
export function initialize(newApiConfig: ApiConfig
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
export function initialize(
newApiConfig: ApiConfig,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
logger.info("cast.initialize");
// Already initialized
@@ -90,22 +130,25 @@ export function initialize(newApiConfig: ApiConfig
apiConfig = newApiConfig;
sendMessageResponse({
subject: "main:shimReady"
, data: { appId: apiConfig.sessionRequest.appId }
subject: "main:shimReady",
data: { appId: apiConfig.sessionRequest.appId }
});
successCallback?.();
apiConfig.receiverListener(receiverDevices.size
? ReceiverAvailability.AVAILABLE
: ReceiverAvailability.UNAVAILABLE);
apiConfig.receiverListener(
receiverDevices.size
? ReceiverAvailability.AVAILABLE
: ReceiverAvailability.UNAVAILABLE
);
}
export function requestSession(successCallback: RequestSessionSuccessCallback
, errorCallback: ErrorCallback
, newSessionRequest?: SessionRequest
, receiverDevice?: ReceiverDevice) {
export function requestSession(
successCallback: RequestSessionSuccessCallback,
errorCallback: ErrorCallback,
newSessionRequest?: SessionRequest,
receiverDevice?: ReceiverDevice
) {
logger.info("cast.requestSession");
// Not yet initialized
@@ -116,9 +159,12 @@ export function requestSession(successCallback: RequestSessionSuccessCallback
// Already requesting session
if (sessionRequest) {
errorCallback?.(new Error_(
ErrorCode.INVALID_PARAMETER
, "Session request already in progress."));
errorCallback?.(
new Error_(
ErrorCode.INVALID_PARAMETER,
"Session request already in progress."
)
);
return;
}
@@ -157,9 +203,11 @@ export function requestSessionById(_sessionId: string): void {
logger.info("STUB :: cast.requestSessionById");
}
export function setCustomReceivers(_receivers: Receiver[]
, _successCallback?: SuccessCallback
, _errorCallback?: ErrorCallback): void {
export function setCustomReceivers(
_receivers: Receiver[],
_successCallback?: SuccessCallback,
_errorCallback?: ErrorCallback
): void {
logger.info("STUB :: cast.setCustomReceivers");
}
@@ -190,7 +238,6 @@ export function precache(_data: string) {
logger.info("STUB :: cast.precache");
}
onMessage(message => {
switch (message.subject) {
case "shim:initialized": {
@@ -212,18 +259,19 @@ onMessage(message => {
// TODO: Implement persistent per-origin receiver IDs
const receiver = new Receiver(
status.receiverFriendlyName // label
, status.receiverFriendlyName // friendlyName
, [ Capability.VIDEO_OUT
, Capability.AUDIO_OUT ] // capabilities
, status.volume); // volume
status.receiverFriendlyName, // label
status.receiverFriendlyName, // friendlyName
[Capability.VIDEO_OUT, Capability.AUDIO_OUT], // capabilities
status.volume // volume
);
const session = new Session(
status.sessionId // sessionId
, status.appId // appId
, status.displayName // displayName
, status.appImages // appImages
, receiver); // receiver
status.sessionId, // sessionId
status.appId, // appId
status.displayName, // displayName
status.appImages, // appImages
receiver // receiver
);
session.senderApps = status.senderApps;
session.transportId = status.transportId;
@@ -293,8 +341,8 @@ onMessage(message => {
const callbacks = session._sendMessageCallbacks.get(messageId);
if (callbacks) {
const [ successCallback, errorCallback ] = callbacks;
const [successCallback, errorCallback] = callbacks;
if (error) {
errorCallback?.(new Error_(error));
return;
@@ -316,8 +364,7 @@ onMessage(message => {
if (apiConfig) {
// Notify listeners of new cast destination
apiConfig.receiverListener(
ReceiverAvailability.AVAILABLE);
apiConfig.receiverListener(ReceiverAvailability.AVAILABLE);
}
break;
@@ -331,7 +378,8 @@ onMessage(message => {
if (receiverDevices.size === 0) {
if (apiConfig) {
apiConfig.receiverListener(
ReceiverAvailability.UNAVAILABLE);
ReceiverAvailability.UNAVAILABLE
);
}
}
@@ -359,8 +407,9 @@ onMessage(message => {
for (const listener of receiverActionListeners) {
const castReceiver = new Receiver(
receiver.id
, receiver.friendlyName);
receiver.id,
receiver.friendlyName
);
listener(castReceiver, ReceiverAction.STOP);
}
@@ -376,12 +425,10 @@ onMessage(message => {
if (sessionRequest) {
sessionRequest = null;
requestSessionErrorCallback?.(
new Error_(ErrorCode.CANCEL));
requestSessionErrorCallback?.(new Error_(ErrorCode.CANCEL));
}
break;
}
}
});

View File

@@ -5,22 +5,34 @@ import { v1 as uuid } from "uuid";
import logger from "../../../lib/logger";
import { Volume, Error as _Error } from "../dataClasses";
import { BreakStatus, EditTracksInfoRequest, GetStatusRequest, LiveSeekableRange
, MediaInfo, PauseRequest, PlayRequest, QueueData, QueueJumpRequest
, QueueInsertItemsRequest, QueueItem, QueueSetPropertiesRequest
, QueueRemoveItemsRequest, QueueReorderItemsRequest
, QueueUpdateItemsRequest, SeekRequest, StopRequest, VideoInformation
, VolumeRequest } from "./dataClasses";
import {
BreakStatus,
EditTracksInfoRequest,
GetStatusRequest,
LiveSeekableRange,
MediaInfo,
PauseRequest,
PlayRequest,
QueueData,
QueueJumpRequest,
QueueInsertItemsRequest,
QueueItem,
QueueSetPropertiesRequest,
QueueRemoveItemsRequest,
QueueReorderItemsRequest,
QueueUpdateItemsRequest,
SeekRequest,
StopRequest,
VideoInformation,
VolumeRequest
} from "./dataClasses";
import { PlayerState, RepeatMode } from "./enums";
import { ErrorCode } from "../enums";
import { ErrorCallback
, SuccessCallback
, UpdateListener } from "../../types";
import { ErrorCallback, SuccessCallback, UpdateListener } from "../../types";
import { SenderMediaMessage } from "../types";
export default class Media {
#id = uuid();
@@ -49,12 +61,13 @@ export default class Media {
preloadedItemId: Nullable<number> = null;
queueData?: QueueData;
constructor(public sessionId: string
, public mediaSessionId: number
, public _sendMediaMessage: (message: DistributiveOmit<
SenderMediaMessage, "requestId">) => Promise<void>) {
}
constructor(
public sessionId: string,
public mediaSessionId: number,
public _sendMediaMessage: (
message: DistributiveOmit<SenderMediaMessage, "requestId">
) => Promise<void>
) {}
addUpdateListener(listener: UpdateListener) {
this._updateListeners.add(listener);
@@ -63,14 +76,16 @@ export default class Media {
this._updateListeners.delete(listener);
}
editTracksInfo(editTracksInfoRequest: EditTracksInfoRequest
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this._sendMediaMessage(
{ ...editTracksInfoRequest
, type: "EDIT_TRACKS_INFO"
, mediaSessionId: this.mediaSessionId })
editTracksInfo(
editTracksInfoRequest: EditTracksInfoRequest,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...editTracksInfoRequest,
type: "EDIT_TRACKS_INFO",
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
@@ -92,14 +107,16 @@ export default class Media {
*/
getEstimatedTime(): number {
if (this.playerState === PlayerState.PLAYING && this._lastUpdateTime) {
let estimatedTime = this.currentTime +
(((Date.now() - this._lastUpdateTime) / 1000));
let estimatedTime =
this.currentTime + (Date.now() - this._lastUpdateTime) / 1000;
// Enforce valid range
if (estimatedTime < 0) {
estimatedTime = 0;
} else if (this.media?.duration &&
estimatedTime > this.media.duration) {
} else if (
this.media?.duration &&
estimatedTime > this.media.duration
) {
estimatedTime = this.media.duration;
}
@@ -113,92 +130,104 @@ export default class Media {
* Request media status from the receiver application. This
* will also trigger any added media update listeners.
*/
getStatus(getStatusRequest = new GetStatusRequest()
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this._sendMediaMessage(
{ ...getStatusRequest
, type: "MEDIA_GET_STATUS"
, mediaSessionId: this.mediaSessionId })
getStatus(
getStatusRequest = new GetStatusRequest(),
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...getStatusRequest,
type: "MEDIA_GET_STATUS",
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
pause(pauseRequest = new PauseRequest()
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this._sendMediaMessage(
{ ...pauseRequest
, type: "PAUSE"
, mediaSessionId: this.mediaSessionId })
pause(
pauseRequest = new PauseRequest(),
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...pauseRequest,
type: "PAUSE",
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
play(playRequest = new PlayRequest()
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this._sendMediaMessage(
{ ...playRequest
, type: "PLAY"
, mediaSessionId: this.mediaSessionId })
play(
playRequest = new PlayRequest(),
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...playRequest,
type: "PLAY",
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
queueAppendItem(item: QueueItem
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this._sendMediaMessage(
{ ...new QueueInsertItemsRequest([ item ])
, type: "QUEUE_INSERT"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
queueAppendItem(
item: QueueItem,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...new QueueInsertItemsRequest([item]),
type: "QUEUE_INSERT",
sessionId: this.sessionId,
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
queueInsertItems(queueInsertItemsRequest: QueueInsertItemsRequest
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this._sendMediaMessage(
{ ...queueInsertItemsRequest
, type: "QUEUE_INSERT"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
queueInsertItems(
queueInsertItemsRequest: QueueInsertItemsRequest,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...queueInsertItemsRequest,
type: "QUEUE_INSERT",
sessionId: this.sessionId,
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
queueJumpToItem(itemId: number
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
queueJumpToItem(
itemId: number,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
if (this.items?.find(item => item.itemId === itemId)) {
const jumpRequest = new QueueJumpRequest();
jumpRequest.currentItemId = itemId;
this._sendMediaMessage(
{ ...jumpRequest
, type: "QUEUE_UPDATE"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
this._sendMediaMessage({
...jumpRequest,
type: "QUEUE_UPDATE",
sessionId: this.sessionId,
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
}
queueMoveItemToNewIndex(itemId: number
, newIndex: number
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
queueMoveItemToNewIndex(
itemId: number,
newIndex: number,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
// Return early if not in queue
if (!this.items) {
return;
@@ -213,170 +242,194 @@ export default class Media {
errorCallback(new _Error(ErrorCode.INVALID_PARAMETER));
}
} else if (newIndex == itemIndex) {
if (successCallback) { successCallback(); }
if (successCallback) {
successCallback();
}
}
} else {
if (newIndex > itemIndex) {
newIndex++;
}
const reorderItemsRequest =
new QueueReorderItemsRequest([ itemId ]);
const reorderItemsRequest = new QueueReorderItemsRequest([itemId]);
if (newIndex < this.items.length) {
const existingItem = this.items[newIndex];
reorderItemsRequest.insertBefore = existingItem.itemId;
}
this._sendMediaMessage(
{ ...reorderItemsRequest
, type: "QUEUE_REORDER"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
this._sendMediaMessage({
...reorderItemsRequest,
type: "QUEUE_REORDER",
sessionId: this.sessionId,
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
}
queueNext(successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
queueNext(
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
const jumpRequest = new QueueJumpRequest();
jumpRequest.jump = 1;
this._sendMediaMessage(
{ ...jumpRequest
, type: "QUEUE_UPDATE"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
this._sendMediaMessage({
...jumpRequest,
type: "QUEUE_UPDATE",
sessionId: this.sessionId,
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
queuePrev(successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
queuePrev(
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
const jumpRequest = new QueueJumpRequest();
jumpRequest.jump = -1;
this._sendMediaMessage(
{ ...jumpRequest
, type: "QUEUE_UPDATE"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
this._sendMediaMessage({
...jumpRequest,
type: "QUEUE_UPDATE",
sessionId: this.sessionId,
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
queueRemoveItem(itemId: number
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
queueRemoveItem(
itemId: number,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
const item = this.items?.find(item => item.itemId === itemId);
if (item) {
this.queueRemoveItems(
new QueueRemoveItemsRequest([ itemId ])
, successCallback, errorCallback);
new QueueRemoveItemsRequest([itemId]),
successCallback,
errorCallback
);
}
}
queueRemoveItems(queueRemoveItemsRequest: QueueRemoveItemsRequest
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this._sendMediaMessage(
{ ...queueRemoveItemsRequest
, mediaSessionId: this.mediaSessionId
, type: "QUEUE_REMOVE"
, sessionId: this.sessionId })
queueRemoveItems(
queueRemoveItemsRequest: QueueRemoveItemsRequest,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...queueRemoveItemsRequest,
mediaSessionId: this.mediaSessionId,
type: "QUEUE_REMOVE",
sessionId: this.sessionId
})
.then(successCallback)
.catch(errorCallback);
}
queueReorderItems(queueReorderItemsRequest: QueueReorderItemsRequest
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
queueReorderItems(
queueReorderItemsRequest: QueueReorderItemsRequest,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...queueReorderItemsRequest,
this._sendMediaMessage(
{ ...queueReorderItemsRequest
, mediaSessionId: this.mediaSessionId
, type: "QUEUE_REORDER"
, sessionId: this.sessionId })
mediaSessionId: this.mediaSessionId,
type: "QUEUE_REORDER",
sessionId: this.sessionId
})
.then(successCallback)
.catch(errorCallback);
}
queueSetRepeatMode(repeatMode: string
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
queueSetRepeatMode(
repeatMode: string,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
const setPropertiesRequest = new QueueSetPropertiesRequest();
setPropertiesRequest.repeatMode = repeatMode;
this._sendMediaMessage(
{ ...setPropertiesRequest
, type: "QUEUE_UPDATE"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
this._sendMediaMessage({
...setPropertiesRequest,
type: "QUEUE_UPDATE",
sessionId: this.sessionId,
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
queueUpdateItems(queueUpdateItemsRequest: QueueUpdateItemsRequest
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this._sendMediaMessage(
{ ...queueUpdateItemsRequest
, type: "QUEUE_UPDATE"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
queueUpdateItems(
queueUpdateItemsRequest: QueueUpdateItemsRequest,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...queueUpdateItemsRequest,
type: "QUEUE_UPDATE",
sessionId: this.sessionId,
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
seek(seekRequest: SeekRequest
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this._sendMediaMessage(
{ ...seekRequest
, type: "SEEK"
, mediaSessionId: this.mediaSessionId })
seek(
seekRequest: SeekRequest,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...seekRequest,
type: "SEEK",
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
setVolume(volumeRequest: VolumeRequest
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this._sendMediaMessage(
{ ...volumeRequest
, type: "MEDIA_SET_VOLUME"
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
setVolume(
volumeRequest: VolumeRequest,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
this._sendMediaMessage({
...volumeRequest,
type: "MEDIA_SET_VOLUME",
mediaSessionId: this.mediaSessionId
})
.then(successCallback)
.catch(errorCallback);
}
stop(stopRequest?: StopRequest
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
stop(
stopRequest?: StopRequest,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
if (!stopRequest) {
stopRequest = new StopRequest();
}
this._sendMediaMessage({
...stopRequest
, type: "STOP"
, mediaSessionId: this.mediaSessionId
}).then(() => {
if (successCallback) {
successCallback();
}
}).catch(errorCallback);
...stopRequest,
type: "STOP",
mediaSessionId: this.mediaSessionId
})
.then(() => {
if (successCallback) {
successCallback();
}
})
.catch(errorCallback);
}
supportsCommand(command: string): boolean {

View File

@@ -2,15 +2,18 @@
import { Image, Volume } from "../dataClasses";
import { ContainerType
, HdrType
, HlsSegmentFormat
, HlsVideoSegmentFormat
, MetadataType
, RepeatMode
, ResumeState, StreamType
, TrackType, UserAction } from "./enums";
import {
ContainerType,
HdrType,
HlsSegmentFormat,
HlsVideoSegmentFormat,
MetadataType,
RepeatMode,
ResumeState,
StreamType,
TrackType,
UserAction
} from "./enums";
export class AudiobookChapterMediaMetadata {
bookTitle?: string;
@@ -35,9 +38,10 @@ export class Break {
isWatched = false;
constructor(
public id: string
, public breakClipIds: string[]
, public position: number) {}
public id: string,
public breakClipIds: string[],
public position: number
) {}
}
export class BreakClip {
@@ -71,17 +75,17 @@ export class ContainerMetadata {
title?: string;
constructor(
public containerType: ContainerType =
ContainerType.GENERIC_CONTAINER) {}
public containerType: ContainerType = ContainerType.GENERIC_CONTAINER
) {}
}
export class EditTracksInfoRequest {
requestId = 0;
constructor(
public activeTrackIds: Nullable<number[]> = null
, public textTrackStyle: Nullable<string> = null) {
}
public activeTrackIds: Nullable<number[]> = null,
public textTrackStyle: Nullable<string> = null
) {}
}
export class GenericMediaMetadata {
@@ -100,10 +104,11 @@ export class GetStatusRequest {
export class LiveSeekableRange {
constructor(
public start?: number
, public end?: number
, public isMovingWindow?: boolean
, public isLiveDone?: boolean) {}
public start?: number,
public end?: number,
public isMovingWindow?: boolean,
public isLiveDone?: boolean
) {}
}
export class LoadRequest {
@@ -123,13 +128,12 @@ export class LoadRequest {
}
}
export type Metadata =
GenericMediaMetadata
| MovieMediaMetadata
| MusicTrackMediaMetadata
| PhotoMediaMetadata
| TvShowMediaMetadata;
| GenericMediaMetadata
| MovieMediaMetadata
| MusicTrackMediaMetadata
| PhotoMediaMetadata
| TvShowMediaMetadata;
export class MediaInfo {
atvEntity?: string;
@@ -149,12 +153,9 @@ export class MediaInfo {
userActionStates?: UserActionState[];
vmapAdsRequest?: VastAdsRequest;
constructor(
public contentId: string
, public contentType: string) {}
constructor(public contentId: string, public contentType: string) {}
}
export class MediaMetadata {
queueItemId?: number;
sectionDuration?: number;
@@ -224,13 +225,14 @@ export class QueueData {
shuffle = false;
constructor(
public id?: string
, public name?: string
, public description?: string
, public repeatMode?: RepeatMode
, public items?: QueueItem[]
, public startIndex?: number
, public startTime?: number) {}
public id?: string,
public name?: string,
public description?: string,
public repeatMode?: RepeatMode,
public items?: QueueItem[],
public startIndex?: number,
public startTime?: number
) {}
}
export class QueueInsertItemsRequest {
@@ -240,8 +242,7 @@ export class QueueInsertItemsRequest {
sessionId: Nullable<string> = null;
type = "QUEUE_INSERT";
constructor(
public items: QueueItem[]) {}
constructor(public items: QueueItem[]) {}
}
export class QueueItem {
@@ -302,7 +303,6 @@ export class QueueUpdateItemsRequest {
constructor(public items: QueueItem[]) {}
}
export class SeekRequest {
currentTime: Nullable<number> = null;
customData: any = null;
@@ -336,9 +336,7 @@ export class Track {
trackContentId: Nullable<string> = null;
trackContentType: Nullable<string> = null;
constructor(
public trackId: number
, public type: TrackType) {}
constructor(public trackId: number, public type: TrackType) {}
}
export class TvShowMediaMetadata {
@@ -359,8 +357,7 @@ export class TvShowMediaMetadata {
export class UserActionState {
customData: any = null;
constructor(
public userAction: UserAction) {}
constructor(public userAction: UserAction) {}
}
export class VastAdsRequest {
@@ -370,14 +367,14 @@ export class VastAdsRequest {
export class VideoInformation {
constructor(
public width: number
, public height: number
, public hdrType: HdrType) {}
public width: number,
public height: number,
public hdrType: HdrType
) {}
}
export class VolumeRequest {
customData: any = null;
constructor(
public volume: Volume) {}
constructor(public volume: Volume) {}
}

View File

@@ -1,139 +1,139 @@
"use strict";
export enum ContainerType {
GENERIC_CONTAINER
, AUDIOBOOK_CONTAINER
GENERIC_CONTAINER,
AUDIOBOOK_CONTAINER
}
export enum HdrType {
SDR = "sdr"
, HDR = "hdr"
, DV = "dv"
SDR = "sdr",
HDR = "hdr",
DV = "dv"
}
export enum HlsSegmentFormat {
AAC = "aac"
, AC3 = "ac3"
, MP3 = "mp3"
, TS = "ts"
, TS_AAC = "ts_aac"
, E_AC3 = "e_ac3"
, FMP4 = "fmp4"
AAC = "aac",
AC3 = "ac3",
MP3 = "mp3",
TS = "ts",
TS_AAC = "ts_aac",
E_AC3 = "e_ac3",
FMP4 = "fmp4"
}
export enum HlsVideoSegmentFormat {
MPEG2_TS = "mpeg2_ts"
, FMP4 = "fmp4"
MPEG2_TS = "mpeg2_ts",
FMP4 = "fmp4"
}
export enum IdleReason {
CANCELLED = "CANCELLED"
, INTERRUPTED = "INTERRUPTED"
, FINISHED = "FINISHED"
, ERROR = "ERROR"
CANCELLED = "CANCELLED",
INTERRUPTED = "INTERRUPTED",
FINISHED = "FINISHED",
ERROR = "ERROR"
}
export enum MediaCommand {
PAUSE = "pause"
, SEEK = "seek"
, STREAM_VOLUME = "stream_volume"
, STREAM_MUTE = "stream_mute"
PAUSE = "pause",
SEEK = "seek",
STREAM_VOLUME = "stream_volume",
STREAM_MUTE = "stream_mute"
}
export enum MetadataType {
GENERIC
, MOVIE
, TV_SHOW
, MUSIC_TRACK
, PHOTO
, AUDIOBOOK_CHAPTER
GENERIC,
MOVIE,
TV_SHOW,
MUSIC_TRACK,
PHOTO,
AUDIOBOOK_CHAPTER
}
export enum PlayerState {
IDLE = "IDLE"
, PLAYING = "PLAYING"
, PAUSED = "PAUSED"
, BUFFERING = "BUFFERING"
IDLE = "IDLE",
PLAYING = "PLAYING",
PAUSED = "PAUSED",
BUFFERING = "BUFFERING"
}
export enum QueueType {
ALBUM = "ALBUM"
, PLAYLIST = "PLAYLIST"
, AUDIOBOOK = "AUDIOBOOK"
, RADIO_STATION = "RADIO_STATION"
, PODCAST_SERIES = "PODCAST_SERIES"
, TV_SERIES = "TV_SERIES"
, VIDEO_PLAYLIST = "VIDEO_PLAYLIST"
, LIVE_TV = "LIVETV"
, MOVIE = "MOVIE"
ALBUM = "ALBUM",
PLAYLIST = "PLAYLIST",
AUDIOBOOK = "AUDIOBOOK",
RADIO_STATION = "RADIO_STATION",
PODCAST_SERIES = "PODCAST_SERIES",
TV_SERIES = "TV_SERIES",
VIDEO_PLAYLIST = "VIDEO_PLAYLIST",
LIVE_TV = "LIVETV",
MOVIE = "MOVIE"
}
export enum RepeatMode {
OFF = "REPEAT_OFF"
, ALL = "REPEAT_ALL"
, SINGLE = "REPEAT_SINGLE"
, ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
OFF = "REPEAT_OFF",
ALL = "REPEAT_ALL",
SINGLE = "REPEAT_SINGLE",
ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
}
export enum ResumeState {
PLAYBACK_START = "PLAYBACK_START"
, PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
PLAYBACK_START = "PLAYBACK_START",
PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
}
export enum StreamType {
BUFFERED = "BUFFERED"
, LIVE = "LIVE"
, OTHER = "OTHER"
BUFFERED = "BUFFERED",
LIVE = "LIVE",
OTHER = "OTHER"
}
export enum TextTrackEdgeType {
NONE = "NONE"
, OUTLINE = "OUTLINE"
, DROP_SHADOW = "DROP_SHADOW"
, RAISED = "RAISED"
, DEPRESSED = "DEPRESSED"
NONE = "NONE",
OUTLINE = "OUTLINE",
DROP_SHADOW = "DROP_SHADOW",
RAISED = "RAISED",
DEPRESSED = "DEPRESSED"
}
export enum TextTrackFontGenericFamily {
SANS_SERIF = "SANS_SERIF"
, MONOSPACED_SANS_SERIF = "MONOSPACED_SANS_SERIF"
, SERIF = "SERIF"
, MONOSPACED_SERIF = "MONOSPACED_SERIF"
, CASUAL = "CASUAL"
, CURSIVE = "CURSIVE"
, SMALL_CAPITALS = "SMALL_CAPITALS"
SANS_SERIF = "SANS_SERIF",
MONOSPACED_SANS_SERIF = "MONOSPACED_SANS_SERIF",
SERIF = "SERIF",
MONOSPACED_SERIF = "MONOSPACED_SERIF",
CASUAL = "CASUAL",
CURSIVE = "CURSIVE",
SMALL_CAPITALS = "SMALL_CAPITALS"
}
export enum TextTrackFontStyle {
NORMAL = "NORMAL"
, BOLD = "BOLD"
, BOLD_ITALIC = "BOLD_ITALIC"
, ITALIC = "ITALIC"
NORMAL = "NORMAL",
BOLD = "BOLD",
BOLD_ITALIC = "BOLD_ITALIC",
ITALIC = "ITALIC"
}
export enum TextTrackType {
SUBTITLES = "SUBTITLES"
, CAPTIONS = "CAPTIONS"
, DESCRIPTIONS = "DESCRIPTIONS"
, CHAPTERS = "CHAPTERS"
, METADATA = "METADATA"
SUBTITLES = "SUBTITLES",
CAPTIONS = "CAPTIONS",
DESCRIPTIONS = "DESCRIPTIONS",
CHAPTERS = "CHAPTERS",
METADATA = "METADATA"
}
export enum TextTrackWindowType {
NONE = "NONE"
, NORMAL = "NORMAL"
, ROUNDED_CORNERS = "ROUNDED_CORNERS"
NONE = "NONE",
NORMAL = "NORMAL",
ROUNDED_CORNERS = "ROUNDED_CORNERS"
}
export enum TrackType {
TEXT = "TEXT"
, AUDIO = "AUDIO"
, VIDEO = "VIDEO"
TEXT = "TEXT",
AUDIO = "AUDIO",
VIDEO = "VIDEO"
}
export enum UserAction {
LIKE = "LIKE"
, DISLIKE = "DISLIKE"
, FOLLOW = "FOLLOW"
, UNFOLLOW = "UNFOLLOW"
LIKE = "LIKE",
DISLIKE = "DISLIKE",
FOLLOW = "FOLLOW",
UNFOLLOW = "UNFOLLOW"
}

View File

@@ -2,22 +2,21 @@
import Media from "./Media";
export { Media };
export * from "./dataClasses";
export * from "./enums";
export const timeout = {
editTracksInfo: 0
, getStatus: 0
, load: 0
, pause: 0
, play: 0
, queue: 0
, seek: 0
, setVolume: 0
, stop: 0
editTracksInfo: 0,
getStatus: 0,
load: 0,
pause: 0,
play: 0,
queue: 0,
seek: 0,
setVolume: 0,
stop: 0
};
export const DEFAULT_MEDIA_RECEIVER_APP_ID = "CC1AD845";

View File

@@ -7,12 +7,13 @@
import { SenderApplication, Volume, Image } from "./dataClasses";
import { MediaInfo, QueueItem } from "./media/dataClasses";
import { IdleReason
, PlayerState
, RepeatMode
, ResumeState } from "./media/enums";
import {
IdleReason,
PlayerState,
RepeatMode,
ResumeState
} from "./media/enums";
export interface MediaStatus {
mediaSessionId: number;
media?: MediaInfo;
@@ -23,65 +24,62 @@ export interface MediaStatus {
currentTime: number;
supportedMediaCommands: number;
repeatMode: RepeatMode;
volume: Volume
volume: Volume;
customData: unknown;
}
export interface ReceiverApplication {
appId: string
, appType?: string
, displayName: string
, iconUrl: string
, isIdleScreen: boolean
, launchedFromCloud: boolean
, namespaces: Array<{ name: string }>
, sessionId: string
, statusText: string
, transportId: string
, universalAppId: string
appId: string;
appType?: string;
displayName: string;
iconUrl: string;
isIdleScreen: boolean;
launchedFromCloud: boolean;
namespaces: Array<{ name: string }>;
sessionId: string;
statusText: string;
transportId: string;
universalAppId: string;
}
export interface ReceiverStatus {
applications?: ReceiverApplication[]
, isActiveInput?: boolean
, isStandBy?: boolean
, volume: Volume
applications?: ReceiverApplication[];
isActiveInput?: boolean;
isStandBy?: boolean;
volume: Volume;
}
export interface CastSessionUpdated {
sessionId: string
, statusText: string
, namespaces: Array<{ name: string }>
, volume: Volume
sessionId: string;
statusText: string;
namespaces: Array<{ name: string }>;
volume: Volume;
}
export interface CastSessionCreated extends CastSessionUpdated {
appId: string
, appImages: Image[]
, displayName: string
, receiverFriendlyName: string
, senderApps: SenderApplication[]
, transportId: string
appId: string;
appImages: Image[];
displayName: string;
receiverFriendlyName: string;
senderApps: SenderApplication[];
transportId: string;
}
interface ReqBase {
requestId: number;
}
// NS: urn:x-cast:com.google.cast.receiver
export type SenderMessage =
ReqBase & { type: "LAUNCH", appId: string }
| ReqBase & { type: "STOP", sessionId: string }
| ReqBase & { type: "GET_STATUS" }
| ReqBase & { type: "GET_APP_AVAILABILITY", appId: string[] }
| ReqBase & { type: "SET_VOLUME", volume: Partial<Volume> };
| (ReqBase & { type: "LAUNCH"; appId: string })
| (ReqBase & { type: "STOP"; sessionId: string })
| (ReqBase & { type: "GET_STATUS" })
| (ReqBase & { type: "GET_APP_AVAILABILITY"; appId: string[] })
| (ReqBase & { type: "SET_VOLUME"; volume: Partial<Volume> });
export type ReceiverMessage =
ReqBase & { type: "RECEIVER_STATUS", status: ReceiverStatus }
| ReqBase & { type: "LAUNCH_ERROR", reason: string }
| (ReqBase & { type: "RECEIVER_STATUS"; status: ReceiverStatus })
| (ReqBase & { type: "LAUNCH_ERROR"; reason: string });
interface MediaReqBase extends ReqBase {
mediaSessionId: number;
@@ -90,84 +88,84 @@ interface MediaReqBase extends ReqBase {
// NS: urn:x-cast:com.google.cast.media
export type SenderMediaMessage =
| MediaReqBase & { type: "PLAY" }
| MediaReqBase & { type: "PAUSE" }
| MediaReqBase & { type: "MEDIA_GET_STATUS" }
| MediaReqBase & { type: "STOP" }
| MediaReqBase & { type: "MEDIA_SET_VOLUME", volume: Partial<Volume> }
| MediaReqBase & { type: "SET_PLAYBACK_RATE" , playbackRate: number }
| ReqBase & {
type: "LOAD"
, activeTrackIds: Nullable<number[]>
, atvCredentials?: string
, atvCredentialsType?: string
, autoplay: Nullable<boolean>
, currentTime: Nullable<number>
, customData?: unknown
, media: MediaInfo
, sessionId: Nullable<string>
}
| MediaReqBase & {
type: "SEEK"
, resumeState: Nullable<ResumeState>
, currentTime: Nullable<number>
}
| MediaReqBase & {
type: "EDIT_TRACKS_INFO"
, activeTrackIds: Nullable<number[]>
, textTrackStyle: Nullable<string>
}
// QueueLoadRequest
| ReqBase & {
type: "QUEUE_LOAD"
, items: QueueItem[]
, startIndex: number
, repeatMode: string
, sessionId: Nullable<string>
}
// QueueInsertItemsRequest
| MediaReqBase & {
type: "QUEUE_INSERT"
, items: QueueItem[]
, insertBefore: Nullable<number>
, sessionId: Nullable<string>
}
// QueueUpdateItemsRequest
| MediaReqBase & {
type: "QUEUE_UPDATE"
, items: QueueItem[]
, sessionId: Nullable<string>
}
// QueueJumpRequest
| MediaReqBase & {
type: "QUEUE_UPDATE"
, jump: Nullable<number>
, currentItemId: Nullable<number>
, sessionId: Nullable<string>
}
// QueueRemoveItemsRequest
| MediaReqBase & {
type: "QUEUE_REMOVE"
, itemIds: number[]
, sessionId: Nullable<string>
}
// QueueReorderItemsRequest
| MediaReqBase & {
type: "QUEUE_REORDER"
, itemIds: number[]
, insertBefore: Nullable<number>
, sessionId: Nullable<string>
}
// QueueSetPropertiesRequest
| MediaReqBase & {
type: "QUEUE_UPDATE"
, repeatMode: Nullable<string>
, sessionId: Nullable<string>
};
| (MediaReqBase & { type: "PLAY" })
| (MediaReqBase & { type: "PAUSE" })
| (MediaReqBase & { type: "MEDIA_GET_STATUS" })
| (MediaReqBase & { type: "STOP" })
| (MediaReqBase & { type: "MEDIA_SET_VOLUME"; volume: Partial<Volume> })
| (MediaReqBase & { type: "SET_PLAYBACK_RATE"; playbackRate: number })
| (ReqBase & {
type: "LOAD";
activeTrackIds: Nullable<number[]>;
atvCredentials?: string;
atvCredentialsType?: string;
autoplay: Nullable<boolean>;
currentTime: Nullable<number>;
customData?: unknown;
media: MediaInfo;
sessionId: Nullable<string>;
})
| (MediaReqBase & {
type: "SEEK";
resumeState: Nullable<ResumeState>;
currentTime: Nullable<number>;
})
| (MediaReqBase & {
type: "EDIT_TRACKS_INFO";
activeTrackIds: Nullable<number[]>;
textTrackStyle: Nullable<string>;
})
// QueueLoadRequest
| (ReqBase & {
type: "QUEUE_LOAD";
items: QueueItem[];
startIndex: number;
repeatMode: string;
sessionId: Nullable<string>;
})
// QueueInsertItemsRequest
| (MediaReqBase & {
type: "QUEUE_INSERT";
items: QueueItem[];
insertBefore: Nullable<number>;
sessionId: Nullable<string>;
})
// QueueUpdateItemsRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
items: QueueItem[];
sessionId: Nullable<string>;
})
// QueueJumpRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
jump: Nullable<number>;
currentItemId: Nullable<number>;
sessionId: Nullable<string>;
})
// QueueRemoveItemsRequest
| (MediaReqBase & {
type: "QUEUE_REMOVE";
itemIds: number[];
sessionId: Nullable<string>;
})
// QueueReorderItemsRequest
| (MediaReqBase & {
type: "QUEUE_REORDER";
itemIds: number[];
insertBefore: Nullable<number>;
sessionId: Nullable<string>;
})
// QueueSetPropertiesRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
repeatMode: Nullable<string>;
sessionId: Nullable<string>;
});
export type ReceiverMediaMessage =
MediaReqBase & { type: "MEDIA_STATUS", status: MediaStatus[] }
| MediaReqBase & { type: "INVALID_PLAYER_STATE" }
| MediaReqBase & { type: "LOAD_FAILED" }
| MediaReqBase & { type: "LOAD_CANCELLED" }
| MediaReqBase & { type: "INVALID_REQUEST" };
| (MediaReqBase & { type: "MEDIA_STATUS"; status: MediaStatus[] })
| (MediaReqBase & { type: "INVALID_PLAYER_STATE" })
| (MediaReqBase & { type: "LOAD_FAILED" })
| (MediaReqBase & { type: "LOAD_CANCELLED" })
| (MediaReqBase & { type: "INVALID_REQUEST" });

View File

@@ -1,10 +1,8 @@
"use strict";
import { CAST_LOADER_SCRIPT_URL
, CAST_SCRIPT_URLS } from "../lib/endpoints";
import { CAST_LOADER_SCRIPT_URL, CAST_SCRIPT_URLS } from "../lib/endpoints";
const _window = (window.wrappedJSObject as any);
const _window = window.wrappedJSObject as any;
_window.chrome = cloneInto({}, window);
@@ -16,7 +14,6 @@ if (window.location.host === "www.youtube.com") {
_window.navigator.presentation = cloneInto({}, window);
}
/**
* Replace the src property setter on <script> elements to
* intercept the new value.
@@ -26,16 +23,16 @@ if (window.location.host === "www.youtube.com") {
* which is handled in the main script.
*/
const desc = Reflect.getOwnPropertyDescriptor(
HTMLScriptElement.prototype.wrappedJSObject, "src");
HTMLScriptElement.prototype.wrappedJSObject,
"src"
);
Reflect.defineProperty(
HTMLScriptElement.prototype.wrappedJSObject, "src", {
Reflect.defineProperty(HTMLScriptElement.prototype.wrappedJSObject, "src", {
configurable: true,
enumerable: true,
get: desc?.get,
configurable: true
, enumerable: true
, get: desc?.get
, set: exportFunction(function setFunc(this: HTMLScriptElement, value) {
set: exportFunction(function setFunc(this: HTMLScriptElement, value) {
if (CAST_SCRIPT_URLS.includes(value)) {
return desc?.set?.call(this, CAST_LOADER_SCRIPT_URL);
}

View File

@@ -5,7 +5,6 @@ import { onMessageResponse, sendMessage } from "./eventMessageChannel";
import messaging, { Message } from "../messaging";
// Message port to background script
export const backgroundPort = messaging.connect({ name: "shim" });

View File

@@ -2,14 +2,12 @@
import { Message } from "../messaging";
type ListenerFunc = (message: Message) => void;
export interface ListenerObject {
disconnect (): void;
disconnect(): void;
}
export function onMessage(listener: ListenerFunc): ListenerObject {
function on__castMessage(ev: CustomEvent) {
listener(JSON.parse(ev.detail));
@@ -26,16 +24,16 @@ export function onMessage(listener: ListenerFunc): ListenerObject {
}
// @ts-ignore
document.addEventListener(
"__castMessage"
, on__castMessage, true);
document.addEventListener("__castMessage", on__castMessage, true);
return {
disconnect() {
// @ts-ignore
document.removeEventListener(
"__castMessage"
, on__castMessage, true);
"__castMessage",
on__castMessage,
true
);
}
};
}
@@ -48,7 +46,6 @@ export function sendMessageResponse(message: Message) {
document.dispatchEvent(event);
}
export function onMessageResponse(listener: ListenerFunc): ListenerObject {
function on__castMessageResponse(ev: CustomEvent) {
listener(JSON.parse(ev.detail));
@@ -56,15 +53,19 @@ export function onMessageResponse(listener: ListenerFunc): ListenerObject {
// @ts-ignore
document.addEventListener(
"__castMessageResponse"
, on__castMessageResponse, true);
"__castMessageResponse",
on__castMessageResponse,
true
);
return {
disconnect() {
// @ts-ignore
document.removeEventListener(
"__castMessageResponse"
, on__castMessageResponse, true);
"__castMessageResponse",
on__castMessageResponse,
true
);
}
};
}

View File

@@ -6,10 +6,11 @@ import { Message } from "../messaging";
import { BridgeInfo } from "../lib/bridge";
import { TypedMessagePort } from "../lib/TypedMessagePort";
import { onMessage
, onMessageResponse
, sendMessage } from "./eventMessageChannel";
import {
onMessage,
onMessageResponse,
sendMessage
} from "./eventMessageChannel";
let initializedBridgeInfo: BridgeInfo;
let initializedBackgroundPort: MessagePort;
@@ -23,7 +24,6 @@ let initializedBackgroundPort: MessagePort;
*/
export function ensureInit(): Promise<TypedMessagePort<Message>> {
return new Promise(async (resolve, reject) => {
// If already initialized, just return existing bridge info
if (initializedBridgeInfo) {
if (initializedBridgeInfo.isVersionCompatible) {
@@ -45,8 +45,9 @@ export function ensureInit(): Promise<TypedMessagePort<Message>> {
* URL.
*/
if (window.location.protocol === "moz-extension:") {
const { default: ShimManager } =
await import("../background/ShimManager");
const { default: ShimManager } = await import(
"../background/ShimManager"
);
// port2 will post bridge messages to port 1
await ShimManager.init();

View File

@@ -2,7 +2,6 @@
import logger from "../../lib/logger";
/**
* Custom element for a cast button used by sites that injects
* a cast icon and manages visibility state and event handling.
@@ -48,19 +47,31 @@ export default class GoogleCastLauncher extends HTMLElement {
iconArch1.classList.add("cast_caf_state_d");
iconArch1.setAttribute("id", "cast_caf_icon_arch1");
iconArch1.setAttribute("d", "M1 14v2c2.76 0 5 2.2 5 5h2c0-3.87-3.13-7-7-7z");
iconArch1.setAttribute(
"d",
"M1 14v2c2.76 0 5 2.2 5 5h2c0-3.87-3.13-7-7-7z"
);
iconArch2.classList.add("cast_caf_state_d");
iconArch2.setAttribute("id", "cast_caf_icon_arch2");
iconArch2.setAttribute("d", "M1 10v2c4.97 0 9 4 9 9h2c0-6.08-4.93-11-11-11z");
iconArch2.setAttribute(
"d",
"M1 10v2c4.97 0 9 4 9 9h2c0-6.08-4.93-11-11-11z"
);
iconBox.classList.add("cast_caf_state_d");
iconBox.setAttribute("id", "cast_caf_icon_box");
iconBox.setAttribute("d", "M21 3H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z");
iconBox.setAttribute(
"d",
"M21 3H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
);
iconBoxFill.classList.add("cast_caf_state_h");
iconBoxFill.setAttribute("id", "cast_caf_icon_boxfill");
iconBoxFill.setAttribute("d", "M5 7v1.63C8 8.6 13.37 14 13.37 17H19V7z");
iconBoxFill.setAttribute(
"d",
"M5 7v1.63C8 8.6 13.37 14 13.37 17H19V7z"
);
// Add icon paths to SVG
icon.append(iconArch0, iconArch1, iconArch2, iconBox, iconBoxFill);
@@ -68,7 +79,6 @@ export default class GoogleCastLauncher extends HTMLElement {
const shadow = this.attachShadow({ mode: "open" });
shadow.append(icon, style);
this.addEventListener("click", () => {
logger.info("<google-cast-launcher> onClick");
});

View File

@@ -4,11 +4,8 @@ import EventData from "./EventData";
import { SessionEventType } from "../enums";
export default class ActiveInputStateEventData extends EventData {
constructor(
public activeInputState: number) {
constructor(public activeInputState: number) {
super(SessionEventType.ACTIVE_INPUT_STATE_CHANGED);
}
}

View File

@@ -2,7 +2,6 @@
import * as cast from "../../cast";
export default class ApplicationMetadata {
public applicationId: string;
public images: cast.Image[];
@@ -16,6 +15,7 @@ export default class ApplicationMetadata {
// Convert [{ name: <ns> }, ...] to [ <ns>, ... ]
this.namespaces = sessionObj.namespaces.map(
namespaceObj => namespaceObj.name);
namespaceObj => namespaceObj.name
);
}
}

View File

@@ -5,11 +5,8 @@ import EventData from "./EventData";
import { SessionEventType } from "../enums";
export default class ApplicationMetadataEventData extends EventData {
constructor(
public metadata: ApplicationMetadata) {
constructor(public metadata: ApplicationMetadata) {
super(SessionEventType.APPLICATION_METADATA_CHANGED);
}
}

View File

@@ -4,11 +4,8 @@ import EventData from "./EventData";
import { SessionEventType } from "../enums";
export default class ApplicationStatusEventData extends EventData {
constructor(
public status: string) {
constructor(public status: string) {
super(SessionEventType.APPLICATION_STATUS_CHANGED);
}
}

View File

@@ -5,7 +5,6 @@ import logger from "../../../lib/logger";
import CastOptions from "./CastOptions";
import CastSession from "./CastSession";
export default class CastContext extends EventTarget {
public endCurrentSession(_stopCasting: boolean): void {
logger.info("STUB :: CastContext#endCurrentSession");

View File

@@ -2,14 +2,13 @@
import * as cast from "../../cast";
export default class CastOptions {
public autoJoinPolicy: string = cast.AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED;
public language: (string | null) = null;
public receiverApplicationId: (string | null) = null;
public language: string | null = null;
public receiverApplicationId: string | null = null;
public resumeSavedSession = true;
constructor(options: CastOptions = ({} as CastOptions)) {
constructor(options: CastOptions = {} as CastOptions) {
if (options.autoJoinPolicy) {
this.autoJoinPolicy = options.autoJoinPolicy;
}

View File

@@ -6,10 +6,8 @@ import * as cast from "../../cast";
import ApplicationMetadata from "./ApplicationMetadata";
type MessageListener = (namespace: string, message: string) => void;
export default class CastSession extends EventTarget {
constructor(_sessionObj: cast.Session, _state: string) {
super();
@@ -17,9 +15,9 @@ export default class CastSession extends EventTarget {
}
public addMessageListener(
_namespace: string
, _listener: MessageListener): void {
_namespace: string,
_listener: MessageListener
): void {
logger.info("STUB :: CastSession#addMessageListener");
}
@@ -83,17 +81,17 @@ export default class CastSession extends EventTarget {
}
public removeMessageListener(
_namespace: string
, _listener: MessageListener): void {
_namespace: string,
_listener: MessageListener
): void {
logger.info("STUB :: CastSession#removeMessageListener");
}
public sendMessage(
_namespace: string
// @ts-ignore
, _data: any): Promise<string> {
_namespace: string,
// @ts-ignore
_data: any
): Promise<string> {
logger.info("STUB :: CastSession#sendMessage");
}

View File

@@ -4,11 +4,8 @@ import EventData from "./EventData";
import { CastContextEventType } from "../enums";
export default class CastStateEventData extends EventData {
constructor(
public castState: string) {
constructor(public castState: string) {
super(CastContextEventType.CAST_STATE_CHANGED);
}
}

View File

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

View File

@@ -6,11 +6,8 @@ import EventData from "./EventData";
import { SessionEventType } from "../enums";
export default class MediaSessionEventData extends EventData {
constructor(
public mediaSession: cast.media.Media) {
constructor(public mediaSession: cast.media.Media) {
super(SessionEventType.MEDIA_SESSION);
}
}

View File

@@ -4,7 +4,6 @@ import * as cast from "../../cast";
import RemotePlayerController from "./RemotePlayerController";
interface SavedPlayerState {
mediaInfo: string;
currentTime: number;
@@ -15,19 +14,19 @@ export default class RemotePlayer {
public canControlVolume = false;
public canPause = false;
public canSeek = false;
public controller: (RemotePlayerController | null) = null;
public controller: RemotePlayerController | null = null;
public currentTime = 0;
public displayName = "";
public displayStatus = "";
public duration = 0;
public imageUrl: (string | null) = null;
public imageUrl: string | null = null;
public isConnected = false;
public isMediaLoaded = false;
public isMuted = false;
public isPaused = false;
public mediaInfo: (cast.media.MediaInfo | null) = null;
public playerState: (string | null) = null;
public savedPlayerState: (SavedPlayerState | null) = null;
public mediaInfo: cast.media.MediaInfo | null = null;
public playerState: string | null = null;
public savedPlayerState: SavedPlayerState | null = null;
public statusText = "";
public title = "";
public volumeLevel = 1;

View File

@@ -1,8 +1,5 @@
"use strict";
export default class RemotePlayerChangedEvent {
constructor(
public type: string
, public field: string
, public value: any) {}
constructor(public type: string, public field: string, public value: any) {}
}

View File

@@ -4,7 +4,6 @@ import logger from "../../../lib/logger";
import RemotePlayer from "./RemotePlayer";
export default class RemotePlayerController extends EventTarget {
constructor(_player: RemotePlayer) {
super();
@@ -16,7 +15,7 @@ export default class RemotePlayerController extends EventTarget {
const minutes = Math.floor(timeInSec / 60) % 60;
const seconds = timeInSec % 60;
return [ hours, minutes, seconds ]
return [hours, minutes, seconds]
.map(c => c.toString().padStart(2, "0"))
.join(":");
}

View File

@@ -5,13 +5,12 @@ import EventData from "./EventData";
import { SessionEventType } from "../enums";
export default class SessionStateEventData extends EventData {
constructor(
public session: CastSession
, public sessionState: string
, public errorCode: (string | null) = null) {
public session: CastSession,
public sessionState: string,
public errorCode: string | null = null
) {
super(SessionEventType.APPLICATION_STATUS_CHANGED);
}
}

View File

@@ -2,11 +2,8 @@
import { SessionEventType } from "../enums";
export default class VolumeEventData {
public type = SessionEventType.VOLUME_CHANGED;
constructor(
public volume: number
, public isMute: boolean) {}
constructor(public volume: number, public isMute: boolean) {}
}

View File

@@ -1,64 +1,64 @@
"use strict";
export enum ActiveInputState {
ACTIVE_INPUT_STATE_UNKNOWN = -1
, ACTIVE_INPUT_STATE_NO = 0
, ACTIVE_INPUT_YES = 1
ACTIVE_INPUT_STATE_UNKNOWN = -1,
ACTIVE_INPUT_STATE_NO = 0,
ACTIVE_INPUT_YES = 1
}
export enum CastContextEventType {
CAST_STATE_CHANGED = "caststatechanged"
, SESSION_STATE_CHANGED = "sessionstatechanged"
CAST_STATE_CHANGED = "caststatechanged",
SESSION_STATE_CHANGED = "sessionstatechanged"
}
export enum CastState {
NO_DEVICES_AVAILABLE = "NO_DEVICES_AVAILABLE"
, NOT_CONNECTED = "NOT_CONNECTED"
, CONNECTING = "CONNECTING"
, CONNECTED = "CONNECTED"
NO_DEVICES_AVAILABLE = "NO_DEVICES_AVAILABLE",
NOT_CONNECTED = "NOT_CONNECTED",
CONNECTING = "CONNECTING",
CONNECTED = "CONNECTED"
}
export enum LoggerLevel {
DEBUG = 0
, INFO = 800
, WARNING = 900
, ERROR = 1000
, NONE = 1500
DEBUG = 0,
INFO = 800,
WARNING = 900,
ERROR = 1000,
NONE = 1500
}
export enum RemotePlayerEventType {
ANY_CHANGE = "anyChanged"
, IS_CONNECTED_CHANGE = "isConnectedChanged"
, IS_MEDIA_LOADED_CHANGED = "isMediaLoadedChanged"
, DURATION_CHANGED = "durationChanged"
, CURRENT_TIME_CHANGED = "currentTimeChanged"
, IS_PAUSED_CHANGED = "isPausedChanged"
, VOLUME_LEVEL_CHANGED = "volumeLevelChanged"
, CAN_CONTROL_VOLUME_CHANGED = "canControlVolumeChanged"
, IS_MUTED_CHANGED = "isMutedChanged"
, CAN_PAUSE_CHANGED = "canPauseChanged"
, CAN_SEEK_CHANGED = "canSeekChanged"
, DISPLAY_NAME_CHANGED = "displayNameChanged"
, STATUS_TEXT_CHANGED = "statusTextChanged"
, MEDIA_INFO_CHANGED = "mediaInfoChanged"
, IMAGE_URL_CHANGED = "imageUrlChanged"
, PLAYER_STATE_CHANGED = "playerStateChanged"
ANY_CHANGE = "anyChanged",
IS_CONNECTED_CHANGE = "isConnectedChanged",
IS_MEDIA_LOADED_CHANGED = "isMediaLoadedChanged",
DURATION_CHANGED = "durationChanged",
CURRENT_TIME_CHANGED = "currentTimeChanged",
IS_PAUSED_CHANGED = "isPausedChanged",
VOLUME_LEVEL_CHANGED = "volumeLevelChanged",
CAN_CONTROL_VOLUME_CHANGED = "canControlVolumeChanged",
IS_MUTED_CHANGED = "isMutedChanged",
CAN_PAUSE_CHANGED = "canPauseChanged",
CAN_SEEK_CHANGED = "canSeekChanged",
DISPLAY_NAME_CHANGED = "displayNameChanged",
STATUS_TEXT_CHANGED = "statusTextChanged",
MEDIA_INFO_CHANGED = "mediaInfoChanged",
IMAGE_URL_CHANGED = "imageUrlChanged",
PLAYER_STATE_CHANGED = "playerStateChanged"
}
export enum SessionEventType {
APPLICATION_STATUS_CHANGED = "applicationstatuschanged"
, APPLICATION_METADATA_CHANGED = "applicationmetadatachanged"
, ACTIVE_INPUT_STATE_CHANGED = "activeinputstatechanged"
, VOLUME_CHANGED = "volumechanged"
, MEDIA_SESSION = "mediasession"
APPLICATION_STATUS_CHANGED = "applicationstatuschanged",
APPLICATION_METADATA_CHANGED = "applicationmetadatachanged",
ACTIVE_INPUT_STATE_CHANGED = "activeinputstatechanged",
VOLUME_CHANGED = "volumechanged",
MEDIA_SESSION = "mediasession"
}
export enum SessionState {
NO_SESSION = "NO_SESSION"
, SESSION_STARTING = "SESSION_STARTING"
, SESSION_STARTED = "SESSION_STARTED"
, SESSION_START_FAILED = "SESSION_START_FAILED"
, SESSION_ENDING = "SESSION_ENDING"
, SESSION_ENDED = "SESSION_ENDED"
, SESSION_RESUMED = "SESSION_RESUMED"
NO_SESSION = "NO_SESSION",
SESSION_STARTING = "SESSION_STARTING",
SESSION_STARTED = "SESSION_STARTED",
SESSION_START_FAILED = "SESSION_START_FAILED",
SESSION_ENDING = "SESSION_ENDING",
SESSION_ENDED = "SESSION_ENDED",
SESSION_RESUMED = "SESSION_RESUMED"
}

View File

@@ -18,49 +18,63 @@ import RemotePlayerController from "./classes/RemotePlayerController";
import SessionStateEventData from "./classes/SessionStateEventData";
import VolumeEventData from "./classes/VolumeEventData";
import { ActiveInputState
, CastContextEventType
, CastState
, LoggerLevel
, RemotePlayerEventType
, SessionEventType
, SessionState } from "./enums";
import {
ActiveInputState,
CastContextEventType,
CastState,
LoggerLevel,
RemotePlayerEventType,
SessionEventType,
SessionState
} from "./enums";
import GoogleCastLauncher from "./GoogleCastLauncher";
export default {
// Enums
ActiveInputState, CastContextEventType, CastState, LoggerLevel
, RemotePlayerEventType, SessionEventType, SessionState
ActiveInputState,
CastContextEventType,
CastState,
LoggerLevel,
RemotePlayerEventType,
SessionEventType,
SessionState,
// Classes
, ActiveInputStateEventData, ApplicationMetadata
, ApplicationMetadataEventData, ApplicationStatusEventData, CastOptions
, CastSession, CastStateEventData, EventData, MediaSessionEventData
, RemotePlayer, RemotePlayerChangedEvent, RemotePlayerController
, SessionStateEventData, VolumeEventData
ActiveInputStateEventData,
ApplicationMetadata,
ApplicationMetadataEventData,
ApplicationStatusEventData,
CastOptions,
CastSession,
CastStateEventData,
EventData,
MediaSessionEventData,
RemotePlayer,
RemotePlayerChangedEvent,
RemotePlayerController,
SessionStateEventData,
VolumeEventData,
/**
* CastContext class with an extra getInstance method used to
* instantiate and fetch a singleton instance.
*/
, CastContext: {
...CastContext
CastContext: {
...CastContext,
, getInstance() {
getInstance() {
return instance;
}
}
},
, VERSION: "1.0.07"
VERSION: "1.0.07",
, setLoggerLevel(_level: number) {
setLoggerLevel(_level: number) {
logger.info("STUB :: cast.framework.setLoggerLevel");
}
};
/**
* The Framework API defines a <google-cast-launcher> element
* and a <button is="google-cast-button"> element extension,

View File

@@ -6,18 +6,15 @@ import { CAST_FRAMEWORK_SCRIPT_URL } from "../lib/endpoints";
import { loadScript } from "../lib/utils";
import { onMessage } from "./eventMessageChannel";
const _window = (window as any);
const _window = window as any;
if (!_window.chrome) {
_window.chrome = {};
}
// Create page-accessible API object
_window.chrome.cast = cast;
let bridgeInfo: any;
let isFramework = false;
@@ -29,14 +26,13 @@ function callPageReadyFunction() {
}
}
/**
* If loaded within a page via a <script> element,
* document.currentScript should exist and we can check its
* [src] query string for the loadCastFramework param.
*/
if (document.currentScript) {
const currentScript = (document.currentScript as HTMLScriptElement);
const currentScript = document.currentScript as HTMLScriptElement;
const currentScriptUrl = new URL(currentScript.src);
const currentScriptParams = new URLSearchParams(currentScriptUrl.search);

View File

@@ -3,7 +3,6 @@
import { Error as Error_ } from "./cast/dataClasses";
import { Media } from "./cast/media";
export type SuccessCallback = () => void;
export type ErrorCallback = (err: Error_) => void;

View File

@@ -3,9 +3,9 @@
import { ReceiverStatus } from "./shim/cast/types";
export interface ReceiverDevice {
host: string
friendlyName: string
, id: string
, port: number
, status?: ReceiverStatus
host: string;
friendlyName: string;
id: string;
port: number;
status?: ReceiverStatus;
}