@warren-bank/fx_cast_bridge 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/config.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "applicationName": "fx_cast_bridge",
3
+ "applicationVersion": "0.3.1"
4
+ }
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ var __createBinding =
3
+ (this && this.__createBinding) ||
4
+ (Object.create
5
+ ? function (o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (
9
+ !desc ||
10
+ ("get" in desc
11
+ ? !m.__esModule
12
+ : desc.writable || desc.configurable)
13
+ ) {
14
+ desc = {
15
+ enumerable: true,
16
+ get: function () {
17
+ return m[k];
18
+ }
19
+ };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }
23
+ : function (o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ });
27
+ var __setModuleDefault =
28
+ (this && this.__setModuleDefault) ||
29
+ (Object.create
30
+ ? function (o, v) {
31
+ Object.defineProperty(o, "default", {
32
+ enumerable: true,
33
+ value: v
34
+ });
35
+ }
36
+ : function (o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar =
40
+ (this && this.__importStar) ||
41
+ (function () {
42
+ var ownKeys = function (o) {
43
+ ownKeys =
44
+ Object.getOwnPropertyNames ||
45
+ function (o) {
46
+ var ar = [];
47
+ for (var k in o)
48
+ if (Object.prototype.hasOwnProperty.call(o, k))
49
+ ar[ar.length] = k;
50
+ return ar;
51
+ };
52
+ return ownKeys(o);
53
+ };
54
+ return function (mod) {
55
+ if (mod && mod.__esModule) return mod;
56
+ var result = {};
57
+ if (mod != null)
58
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++)
59
+ if (k[i] !== "default") __createBinding(result, mod, k[i]);
60
+ __setModuleDefault(result, mod);
61
+ return result;
62
+ };
63
+ })();
64
+ var __awaiter =
65
+ (this && this.__awaiter) ||
66
+ function (thisArg, _arguments, P, generator) {
67
+ function adopt(value) {
68
+ return value instanceof P
69
+ ? value
70
+ : new P(function (resolve) {
71
+ resolve(value);
72
+ });
73
+ }
74
+ return new (P || (P = Promise))(function (resolve, reject) {
75
+ function fulfilled(value) {
76
+ try {
77
+ step(generator.next(value));
78
+ } catch (e) {
79
+ reject(e);
80
+ }
81
+ }
82
+ function rejected(value) {
83
+ try {
84
+ step(generator["throw"](value));
85
+ } catch (e) {
86
+ reject(e);
87
+ }
88
+ }
89
+ function step(result) {
90
+ result.done
91
+ ? resolve(result.value)
92
+ : adopt(result.value).then(fulfilled, rejected);
93
+ }
94
+ step(
95
+ (generator = generator.apply(thisArg, _arguments || [])).next()
96
+ );
97
+ });
98
+ };
99
+ var __importDefault =
100
+ (this && this.__importDefault) ||
101
+ function (mod) {
102
+ return mod && mod.__esModule ? mod : { default: mod };
103
+ };
104
+ Object.defineProperty(exports, "__esModule", { value: true });
105
+ exports.AirPlayAuth = exports.AirPlayAuthCredentials = void 0;
106
+ const crypto_1 = __importDefault(require("crypto"));
107
+ const fast_srp_hap_1 = __importDefault(require("fast-srp-hap"));
108
+ const node_fetch_1 = __importStar(require("node-fetch"));
109
+ const tweetnacl_1 = __importDefault(require("tweetnacl"));
110
+ const bplist_1 = __importDefault(require("./bplist"));
111
+ const AIRPLAY_PORT = 7000;
112
+ const MIMETYPE_BPLIST = "application/x-apple-binary-plist";
113
+ class AirPlayAuthCredentials {
114
+ constructor(clientId, clientSk, clientPk) {
115
+ if (clientId && clientSk && clientPk) {
116
+ this.clientId = clientId;
117
+ this.clientSk = clientSk;
118
+ this.clientPk = clientPk;
119
+ } else {
120
+ const keyPair = tweetnacl_1.default.sign.keyPair();
121
+ this.clientId = crypto_1.default.randomBytes(8).toString("hex");
122
+ this.clientSk = keyPair.secretKey.slice(0, 32);
123
+ this.clientPk = keyPair.publicKey;
124
+ }
125
+ }
126
+ }
127
+ exports.AirPlayAuthCredentials = AirPlayAuthCredentials;
128
+ class AirPlayAuth {
129
+ constructor(address, credentials) {
130
+ this.address = address;
131
+ this.credentials = credentials;
132
+ this.baseUrl = new URL(`http://${this.address}:${AIRPLAY_PORT}`);
133
+ }
134
+ beginPairing() {
135
+ return __awaiter(this, void 0, void 0, function* () {
136
+ return this.sendPostRequest("/pair-pin-start");
137
+ });
138
+ }
139
+ finishPairing(pin) {
140
+ return __awaiter(this, void 0, void 0, function* () {
141
+ const { pk: serverPk, salt: serverSalt } =
142
+ yield this.pairSetupPin1();
143
+ const srpParams = fast_srp_hap_1.default.params[2048];
144
+ srpParams.hash = "sha1";
145
+ const srpClient = new fast_srp_hap_1.default.Client(
146
+ srpParams,
147
+ serverSalt,
148
+ Buffer.from(this.credentials.clientId),
149
+ Buffer.from(pin),
150
+ Buffer.from(this.credentials.clientSk)
151
+ );
152
+ srpClient.setB(serverPk);
153
+ yield this.pairSetupPin2(
154
+ srpClient.computeA(),
155
+ srpClient.computeM1()
156
+ );
157
+ yield this.pairSetupPin3(srpClient.computeK());
158
+ });
159
+ }
160
+ pairSetupPin1() {
161
+ return __awaiter(this, void 0, void 0, function* () {
162
+ const [response] = yield this.sendPostRequestBplist(
163
+ "/pair-setup-pin",
164
+ {
165
+ method: "pin",
166
+ user: this.credentials.clientId
167
+ }
168
+ );
169
+ return response;
170
+ });
171
+ }
172
+ pairSetupPin2(pk, proof) {
173
+ return __awaiter(this, void 0, void 0, function* () {
174
+ const [response] = yield this.sendPostRequestBplist(
175
+ "/pair-setup-pin",
176
+ {
177
+ pk,
178
+ proof
179
+ }
180
+ );
181
+ return response;
182
+ });
183
+ }
184
+ pairSetupPin3(sharedSecretHash) {
185
+ return __awaiter(this, void 0, void 0, function* () {
186
+ const aesKey = crypto_1.default
187
+ .createHash("sha512")
188
+ .update("Pair-Setup-AES-Key")
189
+ .update(sharedSecretHash)
190
+ .digest()
191
+ .slice(0, 16);
192
+ const aesIv = crypto_1.default
193
+ .createHash("sha512")
194
+ .update("Pair-Setup-AES-IV")
195
+ .update(sharedSecretHash)
196
+ .digest()
197
+ .slice(0, 16);
198
+ aesIv[15]++;
199
+ const cipher = crypto_1.default.createCipheriv(
200
+ "aes-128-gcm",
201
+ aesKey,
202
+ aesIv
203
+ );
204
+ const epk = cipher.update(this.credentials.clientPk);
205
+ cipher.final();
206
+ const authTag = cipher.getAuthTag();
207
+ const [response] = yield this.sendPostRequestBplist(
208
+ "/pair-setup-pin",
209
+ {
210
+ epk,
211
+ authTag
212
+ }
213
+ );
214
+ return response;
215
+ });
216
+ }
217
+ sendPostRequest(path, contentType, data) {
218
+ return __awaiter(this, void 0, void 0, function* () {
219
+ const requestUrl = new URL(path, this.baseUrl);
220
+ const requestHeaders = new node_fetch_1.Headers({
221
+ "User-Agent": "AirPlay/320.20"
222
+ });
223
+ if (data && contentType) {
224
+ requestHeaders.append("Content-Type", contentType);
225
+ }
226
+ const response = yield (0, node_fetch_1.default)(requestUrl.href, {
227
+ method: "POST",
228
+ headers: requestHeaders,
229
+ body: data
230
+ });
231
+ if (!response.ok) {
232
+ throw new Error(`AirPlay request error: ${response.status}`);
233
+ }
234
+ return yield response.buffer();
235
+ });
236
+ }
237
+ sendPostRequestBplist(path, data) {
238
+ return __awaiter(this, void 0, void 0, function* () {
239
+ const requestBody = data
240
+ ? bplist_1.default.create(data)
241
+ : undefined;
242
+ const response = yield this.sendPostRequest(
243
+ path,
244
+ MIMETYPE_BPLIST,
245
+ requestBody
246
+ );
247
+ return bplist_1.default.parse.parseBuffer(response);
248
+ });
249
+ }
250
+ }
251
+ exports.AirPlayAuth = AirPlayAuth;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ var __importDefault =
3
+ (this && this.__importDefault) ||
4
+ function (mod) {
5
+ return mod && mod.__esModule ? mod : { default: mod };
6
+ };
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ const bplist_creator_1 = __importDefault(require("bplist-creator"));
9
+ const bplist_parser_1 = __importDefault(require("bplist-parser"));
10
+ exports.default = {
11
+ create: bplist_creator_1.default,
12
+ parse: bplist_parser_1.default
13
+ };
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ var __createBinding =
3
+ (this && this.__createBinding) ||
4
+ (Object.create
5
+ ? function (o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (
9
+ !desc ||
10
+ ("get" in desc
11
+ ? !m.__esModule
12
+ : desc.writable || desc.configurable)
13
+ ) {
14
+ desc = {
15
+ enumerable: true,
16
+ get: function () {
17
+ return m[k];
18
+ }
19
+ };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }
23
+ : function (o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ });
27
+ var __setModuleDefault =
28
+ (this && this.__setModuleDefault) ||
29
+ (Object.create
30
+ ? function (o, v) {
31
+ Object.defineProperty(o, "default", {
32
+ enumerable: true,
33
+ value: v
34
+ });
35
+ }
36
+ : function (o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar =
40
+ (this && this.__importStar) ||
41
+ (function () {
42
+ var ownKeys = function (o) {
43
+ ownKeys =
44
+ Object.getOwnPropertyNames ||
45
+ function (o) {
46
+ var ar = [];
47
+ for (var k in o)
48
+ if (Object.prototype.hasOwnProperty.call(o, k))
49
+ ar[ar.length] = k;
50
+ return ar;
51
+ };
52
+ return ownKeys(o);
53
+ };
54
+ return function (mod) {
55
+ if (mod && mod.__esModule) return mod;
56
+ var result = {};
57
+ if (mod != null)
58
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++)
59
+ if (k[i] !== "default") __createBinding(result, mod, k[i]);
60
+ __setModuleDefault(result, mod);
61
+ return result;
62
+ };
63
+ })();
64
+ Object.defineProperty(exports, "__esModule", { value: true });
65
+ const client_1 = __importStar(require("./client"));
66
+ class Session extends client_1.default {
67
+ establishAppConnection(transportId) {
68
+ this.transportConnection = this.createChannel(
69
+ client_1.NS_CONNECTION,
70
+ this.sourceId,
71
+ transportId
72
+ );
73
+ this.transportHeartbeat = this.createChannel(
74
+ client_1.NS_HEARTBEAT,
75
+ this.sourceId,
76
+ transportId
77
+ );
78
+ this.transportConnection.send({ type: "CONNECT" });
79
+ }
80
+ sendMessage(namespace, message) {
81
+ let channel = this.namespaceChannelMap.get(namespace);
82
+ if (!channel) {
83
+ channel = this.createChannel(
84
+ namespace,
85
+ this.sourceId,
86
+ this.transportId
87
+ );
88
+ channel.on("message", messageData => {
89
+ if (!this.sessionId) {
90
+ return;
91
+ }
92
+ messageData = JSON.stringify(messageData);
93
+ this.messaging.sendMessage({
94
+ subject: "cast:sessionMessageReceived",
95
+ data: {
96
+ sessionId: this.sessionId,
97
+ namespace,
98
+ messageData
99
+ }
100
+ });
101
+ });
102
+ this.namespaceChannelMap.set(namespace, channel);
103
+ }
104
+ channel.send(message);
105
+ }
106
+ constructor(appId, receiverDevice, messaging, onSessionCreated) {
107
+ super();
108
+ this.appId = appId;
109
+ this.receiverDevice = receiverDevice;
110
+ this.messaging = messaging;
111
+ this.onSessionCreated = onSessionCreated;
112
+ this.namespaceChannelMap = new Map();
113
+ this.onReceiverMessage = message => {
114
+ var _a, _b;
115
+ switch (message.type) {
116
+ case "RECEIVER_STATUS": {
117
+ const { status } = message;
118
+ const application =
119
+ (_a = status.applications) === null || _a === void 0
120
+ ? void 0
121
+ : _a.find(app => app.appId === this.appId);
122
+ if (!this.sessionId) {
123
+ if (message.requestId !== this.launchRequestId) {
124
+ break;
125
+ }
126
+ if (application) {
127
+ this.sessionId = application.sessionId;
128
+ this.transportId = application.transportId;
129
+ this.establishAppConnection(this.transportId);
130
+ (_b = this.onSessionCreated) === null ||
131
+ _b === void 0
132
+ ? void 0
133
+ : _b.call(this, this.sessionId);
134
+ this.messaging.sendMessage({
135
+ subject: "main:castSessionCreated",
136
+ data: {
137
+ sessionId: this.sessionId,
138
+ statusText: application.statusText,
139
+ namespaces: application.namespaces,
140
+ volume: status.volume,
141
+ appId: application.appId,
142
+ displayName: application.displayName,
143
+ receiverId: this.receiverDevice.id,
144
+ receiverFriendlyName:
145
+ this.receiverDevice.friendlyName,
146
+ transportId: this.sessionId,
147
+ senderApps: [],
148
+ appImages: []
149
+ }
150
+ });
151
+ }
152
+ break;
153
+ }
154
+ if (!application) {
155
+ this.client.close();
156
+ break;
157
+ }
158
+ this.messaging.sendMessage({
159
+ subject: "main:castSessionUpdated",
160
+ data: {
161
+ sessionId: this.sessionId,
162
+ statusText: application.statusText,
163
+ namespaces: application.namespaces,
164
+ volume: message.status.volume
165
+ }
166
+ });
167
+ break;
168
+ }
169
+ case "LAUNCH_ERROR": {
170
+ console.error(`err: LAUNCH_ERROR, ${message.reason}`);
171
+ this.client.close();
172
+ break;
173
+ }
174
+ }
175
+ };
176
+ super
177
+ .connect(receiverDevice.host, {
178
+ port: receiverDevice.port,
179
+ onHeartbeat: () => {
180
+ if (this.transportHeartbeat) {
181
+ this.transportHeartbeat.send({ type: "PING" });
182
+ }
183
+ },
184
+ onReceiverMessage: message => {
185
+ this.onReceiverMessage(message);
186
+ }
187
+ })
188
+ .then(() => {
189
+ this.launchRequestId = this.sendReceiverMessage({
190
+ type: "LAUNCH",
191
+ appId: this.appId
192
+ });
193
+ });
194
+ this.client.on("close", () => {
195
+ if (this.sessionId) {
196
+ messaging.sendMessage({
197
+ subject: "cast:sessionStopped",
198
+ data: { sessionId: this.sessionId }
199
+ });
200
+ }
201
+ });
202
+ }
203
+ }
204
+ exports.default = Session;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NS_RECEIVER = exports.NS_HEARTBEAT = exports.NS_CONNECTION = void 0;
4
+ const castv2_1 = require("castv2");
5
+ exports.NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection";
6
+ exports.NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
7
+ exports.NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
8
+ const DEFAULT_PORT = 8009;
9
+ const HEARTBEAT_INTERVAL_MS = 5000;
10
+ class CastClient {
11
+ constructor(sourceId = "sender-0", destinationId = "receiver-0") {
12
+ this.sourceId = sourceId;
13
+ this.destinationId = destinationId;
14
+ this.client = new castv2_1.Client();
15
+ this.receiverRequestId = Math.floor(Math.random() * 1e6);
16
+ }
17
+ createChannel(
18
+ namespace,
19
+ sourceId = this.sourceId,
20
+ destinationId = this.destinationId
21
+ ) {
22
+ return this.client.createChannel(
23
+ sourceId,
24
+ destinationId,
25
+ namespace,
26
+ "JSON"
27
+ );
28
+ }
29
+ sendReceiverMessage(message) {
30
+ if (!this.receiverChannel) return;
31
+ const requestId = this.receiverRequestId++;
32
+ this.receiverChannel.send(
33
+ Object.assign(Object.assign({}, message), { requestId })
34
+ );
35
+ return requestId;
36
+ }
37
+ connect(host, options) {
38
+ return new Promise((resolve, reject) => {
39
+ var _a;
40
+ this.client.on("error", reject);
41
+ this.client.on("close", () => {
42
+ if (this.heartbeatChannel && this.heartbeatIntervalId) {
43
+ clearInterval(this.heartbeatIntervalId);
44
+ }
45
+ });
46
+ this.client.connect(
47
+ {
48
+ host,
49
+ port:
50
+ (_a =
51
+ options === null || options === void 0
52
+ ? void 0
53
+ : options.port) !== null && _a !== void 0
54
+ ? _a
55
+ : DEFAULT_PORT
56
+ },
57
+ () => {
58
+ this.connectionChannel = this.createChannel(
59
+ exports.NS_CONNECTION
60
+ );
61
+ this.heartbeatChannel = this.createChannel(
62
+ exports.NS_HEARTBEAT
63
+ );
64
+ this.receiverChannel = this.createChannel(
65
+ exports.NS_RECEIVER
66
+ );
67
+ this.receiverChannel.on("message", message => {
68
+ var _a;
69
+ (_a =
70
+ options === null || options === void 0
71
+ ? void 0
72
+ : options.onReceiverMessage) === null ||
73
+ _a === void 0
74
+ ? void 0
75
+ : _a.call(options, message);
76
+ });
77
+ this.connectionChannel.send({ type: "CONNECT" });
78
+ this.heartbeatChannel.send({ type: "PING" });
79
+ this.heartbeatIntervalId = setInterval(() => {
80
+ var _a, _b;
81
+ (_a = this.heartbeatChannel) === null || _a === void 0
82
+ ? void 0
83
+ : _a.send({ type: "PING" });
84
+ (_b =
85
+ options === null || options === void 0
86
+ ? void 0
87
+ : options.onHeartbeat) === null || _b === void 0
88
+ ? void 0
89
+ : _b.call(options);
90
+ }, HEARTBEAT_INTERVAL_MS);
91
+ resolve();
92
+ }
93
+ );
94
+ });
95
+ }
96
+ disconnect() {
97
+ var _a;
98
+ if (this.heartbeatIntervalId) {
99
+ clearInterval(this.heartbeatIntervalId);
100
+ }
101
+ (_a = this.connectionChannel) === null || _a === void 0
102
+ ? void 0
103
+ : _a.send({ type: "CLOSE" });
104
+ this.client.close();
105
+ }
106
+ }
107
+ exports.default = CastClient;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __importDefault =
3
+ (this && this.__importDefault) ||
4
+ function (mod) {
5
+ return mod && mod.__esModule ? mod : { default: mod };
6
+ };
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ const mdns_1 = __importDefault(require("../../lib/mdns"));
9
+ class Discovery {
10
+ constructor(opts) {
11
+ this.browser = mdns_1.default.createBrowser(
12
+ "googlecast",
13
+ function hasServiceChanged(a, b) {
14
+ const txtFields = ['id', 'fn', 'md', 'ca'];
15
+ for (const key of txtFields) {
16
+ if (a.txtRecord[key] !== b.txtRecord[key]) {
17
+ return true;
18
+ }
19
+ }
20
+ return false;
21
+ }
22
+ );
23
+ this.browser.on("serviceUp", service => {
24
+ if (!service.txtRecord || !service.name) return;
25
+ const record = service.txtRecord;
26
+ const device = {
27
+ id: record.id,
28
+ friendlyName: record.fn,
29
+ modelName: record.md,
30
+ capabilities: parseInt(record.ca),
31
+ host: service.address,
32
+ port: service.port
33
+ };
34
+ opts.onDeviceFound(device);
35
+ });
36
+ this.browser.on("serviceDown", service => {
37
+ if (!service.txtRecord || !service.name) return;
38
+ const record = service.txtRecord;
39
+ opts.onDeviceDown(record.id);
40
+ });
41
+ }
42
+ start() {
43
+ this.browser.start();
44
+ }
45
+ stop() {
46
+ this.browser.stop();
47
+ }
48
+ }
49
+ exports.default = Discovery;