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

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