UNPKG

60.8 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.SiriAudioSession = exports.SiriAudioSessionEvents = exports.RemoteController = exports.RemoteControllerEvents = exports.TargetUpdates = exports.AudioCodecTypes = exports.ButtonState = exports.TargetCategory = exports.ButtonType = void 0;
4const tslib_1 = require("tslib");
5const assert_1 = tslib_1.__importDefault(require("assert"));
6const debug_1 = tslib_1.__importDefault(require("debug"));
7const events_1 = require("events");
8const Characteristic_1 = require("../Characteristic");
9const datastream_1 = require("../datastream");
10const Service_1 = require("../Service");
11const tlv = tslib_1.__importStar(require("../util/tlv"));
12const debug = (0, debug_1.default)("HAP-NodeJS:Remote:Controller");
13var TargetControlCommands;
14(function (TargetControlCommands) {
15 TargetControlCommands[TargetControlCommands["MAXIMUM_TARGETS"] = 1] = "MAXIMUM_TARGETS";
16 TargetControlCommands[TargetControlCommands["TICKS_PER_SECOND"] = 2] = "TICKS_PER_SECOND";
17 TargetControlCommands[TargetControlCommands["SUPPORTED_BUTTON_CONFIGURATION"] = 3] = "SUPPORTED_BUTTON_CONFIGURATION";
18 TargetControlCommands[TargetControlCommands["TYPE"] = 4] = "TYPE";
19})(TargetControlCommands || (TargetControlCommands = {}));
20var SupportedButtonConfigurationTypes;
21(function (SupportedButtonConfigurationTypes) {
22 SupportedButtonConfigurationTypes[SupportedButtonConfigurationTypes["BUTTON_ID"] = 1] = "BUTTON_ID";
23 SupportedButtonConfigurationTypes[SupportedButtonConfigurationTypes["BUTTON_TYPE"] = 2] = "BUTTON_TYPE";
24})(SupportedButtonConfigurationTypes || (SupportedButtonConfigurationTypes = {}));
25/**
26 * @group Apple TV Remote
27 */
28var ButtonType;
29(function (ButtonType) {
30 // noinspection JSUnusedGlobalSymbols
31 ButtonType[ButtonType["UNDEFINED"] = 0] = "UNDEFINED";
32 ButtonType[ButtonType["MENU"] = 1] = "MENU";
33 ButtonType[ButtonType["PLAY_PAUSE"] = 2] = "PLAY_PAUSE";
34 ButtonType[ButtonType["TV_HOME"] = 3] = "TV_HOME";
35 ButtonType[ButtonType["SELECT"] = 4] = "SELECT";
36 ButtonType[ButtonType["ARROW_UP"] = 5] = "ARROW_UP";
37 ButtonType[ButtonType["ARROW_RIGHT"] = 6] = "ARROW_RIGHT";
38 ButtonType[ButtonType["ARROW_DOWN"] = 7] = "ARROW_DOWN";
39 ButtonType[ButtonType["ARROW_LEFT"] = 8] = "ARROW_LEFT";
40 ButtonType[ButtonType["VOLUME_UP"] = 9] = "VOLUME_UP";
41 ButtonType[ButtonType["VOLUME_DOWN"] = 10] = "VOLUME_DOWN";
42 ButtonType[ButtonType["SIRI"] = 11] = "SIRI";
43 ButtonType[ButtonType["POWER"] = 12] = "POWER";
44 ButtonType[ButtonType["GENERIC"] = 13] = "GENERIC";
45})(ButtonType || (exports.ButtonType = ButtonType = {}));
46var TargetControlList;
47(function (TargetControlList) {
48 TargetControlList[TargetControlList["OPERATION"] = 1] = "OPERATION";
49 TargetControlList[TargetControlList["TARGET_CONFIGURATION"] = 2] = "TARGET_CONFIGURATION";
50})(TargetControlList || (TargetControlList = {}));
51var Operation;
52(function (Operation) {
53 // noinspection JSUnusedGlobalSymbols
54 Operation[Operation["UNDEFINED"] = 0] = "UNDEFINED";
55 Operation[Operation["LIST"] = 1] = "LIST";
56 Operation[Operation["ADD"] = 2] = "ADD";
57 Operation[Operation["REMOVE"] = 3] = "REMOVE";
58 Operation[Operation["RESET"] = 4] = "RESET";
59 Operation[Operation["UPDATE"] = 5] = "UPDATE";
60})(Operation || (Operation = {}));
61var TargetConfigurationTypes;
62(function (TargetConfigurationTypes) {
63 TargetConfigurationTypes[TargetConfigurationTypes["TARGET_IDENTIFIER"] = 1] = "TARGET_IDENTIFIER";
64 TargetConfigurationTypes[TargetConfigurationTypes["TARGET_NAME"] = 2] = "TARGET_NAME";
65 TargetConfigurationTypes[TargetConfigurationTypes["TARGET_CATEGORY"] = 3] = "TARGET_CATEGORY";
66 TargetConfigurationTypes[TargetConfigurationTypes["BUTTON_CONFIGURATION"] = 4] = "BUTTON_CONFIGURATION";
67})(TargetConfigurationTypes || (TargetConfigurationTypes = {}));
68/**
69 * @group Apple TV Remote
70 */
71var TargetCategory;
72(function (TargetCategory) {
73 // noinspection JSUnusedGlobalSymbols
74 TargetCategory[TargetCategory["UNDEFINED"] = 0] = "UNDEFINED";
75 TargetCategory[TargetCategory["APPLE_TV"] = 24] = "APPLE_TV";
76})(TargetCategory || (exports.TargetCategory = TargetCategory = {}));
77var ButtonConfigurationTypes;
78(function (ButtonConfigurationTypes) {
79 ButtonConfigurationTypes[ButtonConfigurationTypes["BUTTON_ID"] = 1] = "BUTTON_ID";
80 ButtonConfigurationTypes[ButtonConfigurationTypes["BUTTON_TYPE"] = 2] = "BUTTON_TYPE";
81 ButtonConfigurationTypes[ButtonConfigurationTypes["BUTTON_NAME"] = 3] = "BUTTON_NAME";
82})(ButtonConfigurationTypes || (ButtonConfigurationTypes = {}));
83var ButtonEvent;
84(function (ButtonEvent) {
85 ButtonEvent[ButtonEvent["BUTTON_ID"] = 1] = "BUTTON_ID";
86 ButtonEvent[ButtonEvent["BUTTON_STATE"] = 2] = "BUTTON_STATE";
87 ButtonEvent[ButtonEvent["TIMESTAMP"] = 3] = "TIMESTAMP";
88 ButtonEvent[ButtonEvent["ACTIVE_IDENTIFIER"] = 4] = "ACTIVE_IDENTIFIER";
89})(ButtonEvent || (ButtonEvent = {}));
90/**
91 * @group Apple TV Remote
92 */
93var ButtonState;
94(function (ButtonState) {
95 ButtonState[ButtonState["UP"] = 0] = "UP";
96 ButtonState[ButtonState["DOWN"] = 1] = "DOWN";
97})(ButtonState || (exports.ButtonState = ButtonState = {}));
98var SelectedAudioInputStreamConfigurationTypes;
99(function (SelectedAudioInputStreamConfigurationTypes) {
100 SelectedAudioInputStreamConfigurationTypes[SelectedAudioInputStreamConfigurationTypes["SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION"] = 1] = "SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION";
101})(SelectedAudioInputStreamConfigurationTypes || (SelectedAudioInputStreamConfigurationTypes = {}));
102// ----------
103var SupportedAudioStreamConfigurationTypes;
104(function (SupportedAudioStreamConfigurationTypes) {
105 // noinspection JSUnusedGlobalSymbols
106 SupportedAudioStreamConfigurationTypes[SupportedAudioStreamConfigurationTypes["AUDIO_CODEC_CONFIGURATION"] = 1] = "AUDIO_CODEC_CONFIGURATION";
107 SupportedAudioStreamConfigurationTypes[SupportedAudioStreamConfigurationTypes["COMFORT_NOISE_SUPPORT"] = 2] = "COMFORT_NOISE_SUPPORT";
108})(SupportedAudioStreamConfigurationTypes || (SupportedAudioStreamConfigurationTypes = {}));
109var AudioCodecConfigurationTypes;
110(function (AudioCodecConfigurationTypes) {
111 AudioCodecConfigurationTypes[AudioCodecConfigurationTypes["CODEC_TYPE"] = 1] = "CODEC_TYPE";
112 AudioCodecConfigurationTypes[AudioCodecConfigurationTypes["CODEC_PARAMETERS"] = 2] = "CODEC_PARAMETERS";
113})(AudioCodecConfigurationTypes || (AudioCodecConfigurationTypes = {}));
114/**
115 * @group Camera
116 */
117var AudioCodecTypes;
118(function (AudioCodecTypes) {
119 // noinspection JSUnusedGlobalSymbols
120 AudioCodecTypes[AudioCodecTypes["PCMU"] = 0] = "PCMU";
121 AudioCodecTypes[AudioCodecTypes["PCMA"] = 1] = "PCMA";
122 AudioCodecTypes[AudioCodecTypes["AAC_ELD"] = 2] = "AAC_ELD";
123 AudioCodecTypes[AudioCodecTypes["OPUS"] = 3] = "OPUS";
124 AudioCodecTypes[AudioCodecTypes["MSBC"] = 4] = "MSBC";
125 AudioCodecTypes[AudioCodecTypes["AMR"] = 5] = "AMR";
126 AudioCodecTypes[AudioCodecTypes["AMR_WB"] = 6] = "AMR_WB";
127})(AudioCodecTypes || (exports.AudioCodecTypes = AudioCodecTypes = {}));
128var AudioCodecParametersTypes;
129(function (AudioCodecParametersTypes) {
130 AudioCodecParametersTypes[AudioCodecParametersTypes["CHANNEL"] = 1] = "CHANNEL";
131 AudioCodecParametersTypes[AudioCodecParametersTypes["BIT_RATE"] = 2] = "BIT_RATE";
132 AudioCodecParametersTypes[AudioCodecParametersTypes["SAMPLE_RATE"] = 3] = "SAMPLE_RATE";
133 AudioCodecParametersTypes[AudioCodecParametersTypes["PACKET_TIME"] = 4] = "PACKET_TIME"; // only present in selected audio codec parameters tlv
134})(AudioCodecParametersTypes || (AudioCodecParametersTypes = {}));
135var SiriAudioSessionState;
136(function (SiriAudioSessionState) {
137 SiriAudioSessionState[SiriAudioSessionState["STARTING"] = 0] = "STARTING";
138 SiriAudioSessionState[SiriAudioSessionState["SENDING"] = 1] = "SENDING";
139 SiriAudioSessionState[SiriAudioSessionState["CLOSING"] = 2] = "CLOSING";
140 SiriAudioSessionState[SiriAudioSessionState["CLOSED"] = 3] = "CLOSED";
141})(SiriAudioSessionState || (SiriAudioSessionState = {}));
142/**
143 * @group Apple TV Remote
144 */
145var TargetUpdates;
146(function (TargetUpdates) {
147 TargetUpdates[TargetUpdates["NAME"] = 0] = "NAME";
148 TargetUpdates[TargetUpdates["CATEGORY"] = 1] = "CATEGORY";
149 TargetUpdates[TargetUpdates["UPDATED_BUTTONS"] = 2] = "UPDATED_BUTTONS";
150 TargetUpdates[TargetUpdates["REMOVED_BUTTONS"] = 3] = "REMOVED_BUTTONS";
151})(TargetUpdates || (exports.TargetUpdates = TargetUpdates = {}));
152/**
153 * @group Apple TV Remote
154 */
155var RemoteControllerEvents;
156(function (RemoteControllerEvents) {
157 /**
158 * This event is emitted when the active state of the remote has changed.
159 * active = true indicates that there is currently an Apple TV listening of button presses and audio streams.
160 */
161 RemoteControllerEvents["ACTIVE_CHANGE"] = "active-change";
162 /**
163 * This event is emitted when the currently selected target has changed.
164 * Possible reasons for a changed active identifier: manual change via api call, first target configuration
165 * gets added, active target gets removed, accessory gets unpaired, reset request was sent.
166 * An activeIdentifier of 0 indicates that no target is selected.
167 */
168 RemoteControllerEvents["ACTIVE_IDENTIFIER_CHANGE"] = "active-identifier-change";
169 /**
170 * This event is emitted when a new target configuration is received. As we currently do not persistently store
171 * configured targets, this will be called at every startup for every Apple TV configured in the home.
172 */
173 RemoteControllerEvents["TARGET_ADDED"] = "target-add";
174 /**
175 * This event is emitted when an existing target was updated.
176 * The 'updates' array indicates what exactly was changed for the target.
177 */
178 RemoteControllerEvents["TARGET_UPDATED"] = "target-update";
179 /**
180 * This event is emitted when an existing configuration for a target was removed.
181 */
182 RemoteControllerEvents["TARGET_REMOVED"] = "target-remove";
183 /**
184 * This event is emitted when a reset of the target configuration is requested.
185 * With this event every configuration made should be reset. This event is also called
186 * when the accessory gets unpaired.
187 */
188 RemoteControllerEvents["TARGETS_RESET"] = "targets-reset";
189})(RemoteControllerEvents || (exports.RemoteControllerEvents = RemoteControllerEvents = {}));
190/**
191 * Handles everything needed to implement a fully working HomeKit remote controller.
192 *
193 * @group Apple TV Remote
194 */
195// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
196class RemoteController extends events_1.EventEmitter {
197 stateChangeDelegate;
198 audioSupported;
199 audioProducerConstructor;
200 // eslint-disable-next-line @typescript-eslint/no-explicit-any
201 audioProducerOptions;
202 targetControlManagementService;
203 targetControlService;
204 siriService;
205 audioStreamManagementService;
206 dataStreamManagement;
207 buttons = {}; // internal mapping of buttonId to buttonType for supported buttons
208 supportedConfiguration;
209 targetConfigurations = new Map();
210 targetConfigurationsString = "";
211 lastButtonEvent = "";
212 activeIdentifier = 0; // id of 0 means no device selected
213 activeConnection; // session which marked this remote as active and listens for events and siri
214 activeConnectionDisconnectListener;
215 supportedAudioConfiguration;
216 selectedAudioConfiguration;
217 selectedAudioConfigurationString;
218 dataStreamConnections = new Map(); // maps targetIdentifiers to active data stream connections
219 activeAudioSession;
220 nextAudioSession;
221 /**
222 * @private
223 */
224 eventHandler;
225 /**
226 * @private
227 */
228 requestHandler;
229 /**
230 * Creates a new RemoteController.
231 * If siri voice input is supported the constructor to an SiriAudioStreamProducer needs to be supplied.
232 * Otherwise, a remote without voice support will be created.
233 *
234 * For every audio session a new SiriAudioStreamProducer will be constructed.
235 *
236 * @param audioProducerConstructor - constructor for a SiriAudioStreamProducer
237 * @param producerOptions - if supplied this argument will be supplied as third argument of the SiriAudioStreamProducer
238 * constructor. This should be used to supply configurations to the stream producer.
239 */
240 // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types
241 constructor(audioProducerConstructor, producerOptions) {
242 super();
243 this.audioSupported = audioProducerConstructor !== undefined;
244 this.audioProducerConstructor = audioProducerConstructor;
245 this.audioProducerOptions = producerOptions;
246 const configuration = this.constructSupportedConfiguration();
247 this.supportedConfiguration = this.buildTargetControlSupportedConfigurationTLV(configuration);
248 const audioConfiguration = this.constructSupportedAudioConfiguration();
249 this.supportedAudioConfiguration = RemoteController.buildSupportedAudioConfigurationTLV(audioConfiguration);
250 this.selectedAudioConfiguration = {
251 codecType: 3 /* AudioCodecTypes.OPUS */,
252 parameters: {
253 channels: 1,
254 bitrate: 0 /* AudioBitrate.VARIABLE */,
255 samplerate: 1 /* AudioSamplerate.KHZ_16 */,
256 rtpTime: 20,
257 },
258 };
259 this.selectedAudioConfigurationString = RemoteController.buildSelectedAudioConfigurationTLV({
260 audioCodecConfiguration: this.selectedAudioConfiguration,
261 });
262 }
263 /**
264 * @private
265 */
266 controllerId() {
267 return "remote" /* DefaultControllerType.REMOTE */;
268 }
269 /**
270 * Set a new target as active target. A value of 0 indicates that no target is selected currently.
271 *
272 * @param activeIdentifier - target identifier
273 */
274 setActiveIdentifier(activeIdentifier) {
275 if (activeIdentifier === this.activeIdentifier) {
276 return;
277 }
278 if (activeIdentifier !== 0 && !this.targetConfigurations.has(activeIdentifier)) {
279 throw Error("Tried setting unconfigured targetIdentifier to active");
280 }
281 debug("%d is now the active target", activeIdentifier);
282 this.activeIdentifier = activeIdentifier;
283 this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.ActiveIdentifier).updateValue(activeIdentifier);
284 if (this.activeAudioSession) {
285 this.handleSiriAudioStop();
286 }
287 setTimeout(() => this.emit("active-identifier-change" /* RemoteControllerEvents.ACTIVE_IDENTIFIER_CHANGE */, activeIdentifier), 0);
288 this.setInactive();
289 }
290 /**
291 * @returns if the current target is active, meaning the active device is listening for button events or audio sessions
292 */
293 isActive() {
294 return !!this.activeConnection;
295 }
296 /**
297 * Checks if the supplied targetIdentifier is configured.
298 *
299 * @param targetIdentifier - The target identifier.
300 */
301 isConfigured(targetIdentifier) {
302 return this.targetConfigurations.has(targetIdentifier);
303 }
304 /**
305 * Returns the targetIdentifier for a give device name
306 *
307 * @param name - The name of the device.
308 * @returns The targetIdentifier of the device or undefined if not existent.
309 */
310 getTargetIdentifierByName(name) {
311 for (const [activeIdentifier, configuration] of Object.entries(this.targetConfigurations)) {
312 if (configuration.targetName === name) {
313 return parseInt(activeIdentifier, 10);
314 }
315 }
316 return undefined;
317 }
318 /**
319 * Sends a button event to press the supplied button.
320 *
321 * @param button - button to be pressed
322 */
323 pushButton(button) {
324 this.sendButtonEvent(button, 1 /* ButtonState.DOWN */);
325 }
326 /**
327 * Sends a button event that the supplied button was released.
328 *
329 * @param button - button which was released
330 */
331 releaseButton(button) {
332 this.sendButtonEvent(button, 0 /* ButtonState.UP */);
333 }
334 /**
335 * Presses a supplied button for a given time.
336 *
337 * @param button - button to be pressed and released
338 * @param time - time in milliseconds (defaults to 200ms)
339 */
340 pushAndReleaseButton(button, time = 200) {
341 this.pushButton(button);
342 setTimeout(() => this.releaseButton(button), time);
343 }
344 // ---------------------------------- CONFIGURATION ----------------------------------
345 // override methods if you would like to change anything (but should not be necessary most likely)
346 constructSupportedConfiguration() {
347 const configuration = {
348 maximumTargets: 10, // some random number. (ten should be okay?)
349 ticksPerSecond: 1000, // we rely on unix timestamps
350 supportedButtonConfiguration: [],
351 hardwareImplemented: this.audioSupported, // siri is only allowed for hardware implemented remotes
352 };
353 const supportedButtons = [
354 1 /* ButtonType.MENU */, 2 /* ButtonType.PLAY_PAUSE */, 3 /* ButtonType.TV_HOME */, 4 /* ButtonType.SELECT */,
355 5 /* ButtonType.ARROW_UP */, 6 /* ButtonType.ARROW_RIGHT */, 7 /* ButtonType.ARROW_DOWN */, 8 /* ButtonType.ARROW_LEFT */,
356 9 /* ButtonType.VOLUME_UP */, 10 /* ButtonType.VOLUME_DOWN */, 12 /* ButtonType.POWER */, 13 /* ButtonType.GENERIC */,
357 ];
358 if (this.audioSupported) { // add siri button if this remote supports it
359 supportedButtons.push(11 /* ButtonType.SIRI */);
360 }
361 supportedButtons.forEach(button => {
362 const buttonConfiguration = {
363 buttonID: 100 + button,
364 buttonType: button,
365 };
366 configuration.supportedButtonConfiguration.push(buttonConfiguration);
367 this.buttons[button] = buttonConfiguration.buttonID; // also saving mapping of type to id locally
368 });
369 return configuration;
370 }
371 constructSupportedAudioConfiguration() {
372 // the following parameters are expected from HomeKit for a remote
373 return {
374 audioCodecConfiguration: {
375 codecType: 3 /* AudioCodecTypes.OPUS */,
376 parameters: {
377 channels: 1,
378 bitrate: 0 /* AudioBitrate.VARIABLE */,
379 samplerate: 1 /* AudioSamplerate.KHZ_16 */,
380 },
381 },
382 };
383 }
384 // --------------------------------- TARGET CONTROL ----------------------------------
385 // eslint-disable-next-line @typescript-eslint/no-explicit-any
386 handleTargetControlWrite(value, callback) {
387 const data = Buffer.from(value, "base64");
388 const objects = tlv.decode(data);
389 const operation = objects[1 /* TargetControlList.OPERATION */][0];
390 let targetConfiguration = undefined;
391 if (objects[2 /* TargetControlList.TARGET_CONFIGURATION */]) { // if target configuration was sent, parse it
392 targetConfiguration = this.parseTargetConfigurationTLV(objects[2 /* TargetControlList.TARGET_CONFIGURATION */]);
393 }
394 debug("Received TargetControl write operation %s", Operation[operation]);
395 let handler;
396 switch (operation) {
397 case Operation.ADD:
398 handler = this.handleAddTarget.bind(this);
399 break;
400 case Operation.UPDATE:
401 handler = this.handleUpdateTarget.bind(this);
402 break;
403 case Operation.REMOVE:
404 handler = this.handleRemoveTarget.bind(this);
405 break;
406 case Operation.RESET:
407 handler = this.handleResetTargets.bind(this);
408 break;
409 case Operation.LIST:
410 handler = this.handleListTargets.bind(this);
411 break;
412 default:
413 callback(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */, undefined);
414 return;
415 }
416 const status = handler(targetConfiguration);
417 if (status === 0 /* HAPStatus.SUCCESS */) {
418 callback(undefined, this.targetConfigurationsString); // passing value for write response
419 if (operation === Operation.ADD && this.activeIdentifier === 0) {
420 this.setActiveIdentifier(targetConfiguration.targetIdentifier);
421 }
422 }
423 else {
424 callback(new Error(status + ""));
425 }
426 }
427 handleAddTarget(targetConfiguration) {
428 if (!targetConfiguration) {
429 return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
430 }
431 this.targetConfigurations.set(targetConfiguration.targetIdentifier, targetConfiguration);
432 debug("Configured new target '" + targetConfiguration.targetName + "' with targetIdentifier '" + targetConfiguration.targetIdentifier + "'");
433 setTimeout(() => this.emit("target-add" /* RemoteControllerEvents.TARGET_ADDED */, targetConfiguration), 0);
434 this.updatedTargetConfiguration(); // set response
435 return 0 /* HAPStatus.SUCCESS */;
436 }
437 handleUpdateTarget(targetConfiguration) {
438 if (!targetConfiguration) {
439 return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
440 }
441 const updates = [];
442 const configuredTarget = this.targetConfigurations.get(targetConfiguration.targetIdentifier);
443 if (!configuredTarget) {
444 return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
445 }
446 if (targetConfiguration.targetName) {
447 debug("Target name was updated '%s' => '%s' (%d)", configuredTarget.targetName, targetConfiguration.targetName, configuredTarget.targetIdentifier);
448 configuredTarget.targetName = targetConfiguration.targetName;
449 updates.push(0 /* TargetUpdates.NAME */);
450 }
451 if (targetConfiguration.targetCategory) {
452 debug("Target category was updated '%d' => '%d' for target '%s' (%d)", configuredTarget.targetCategory, targetConfiguration.targetCategory, configuredTarget.targetName, configuredTarget.targetIdentifier);
453 configuredTarget.targetCategory = targetConfiguration.targetCategory;
454 updates.push(1 /* TargetUpdates.CATEGORY */);
455 }
456 if (targetConfiguration.buttonConfiguration) {
457 debug("%d button configurations were updated for target '%s' (%d)", Object.keys(targetConfiguration.buttonConfiguration).length, configuredTarget.targetName, configuredTarget.targetIdentifier);
458 for (const configuration of Object.values(targetConfiguration.buttonConfiguration)) {
459 const savedConfiguration = configuredTarget.buttonConfiguration[configuration.buttonID];
460 savedConfiguration.buttonType = configuration.buttonType;
461 savedConfiguration.buttonName = configuration.buttonName;
462 }
463 updates.push(2 /* TargetUpdates.UPDATED_BUTTONS */);
464 }
465 setTimeout(() => this.emit("target-update" /* RemoteControllerEvents.TARGET_UPDATED */, targetConfiguration, updates), 0);
466 this.updatedTargetConfiguration(); // set response
467 return 0 /* HAPStatus.SUCCESS */;
468 }
469 handleRemoveTarget(targetConfiguration) {
470 if (!targetConfiguration) {
471 return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
472 }
473 const configuredTarget = this.targetConfigurations.get(targetConfiguration.targetIdentifier);
474 if (!configuredTarget) {
475 return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
476 }
477 if (targetConfiguration.buttonConfiguration) {
478 for (const key in targetConfiguration.buttonConfiguration) {
479 if (Object.prototype.hasOwnProperty.call(targetConfiguration.buttonConfiguration, key)) {
480 delete configuredTarget.buttonConfiguration[key];
481 }
482 }
483 debug("Removed %d button configurations of target '%s' (%d)", Object.keys(targetConfiguration.buttonConfiguration).length, configuredTarget.targetName, configuredTarget.targetIdentifier);
484 setTimeout(() => this.emit("target-update" /* RemoteControllerEvents.TARGET_UPDATED */, configuredTarget, [3 /* TargetUpdates.REMOVED_BUTTONS */]), 0);
485 }
486 else {
487 this.targetConfigurations.delete(targetConfiguration.targetIdentifier);
488 debug("Target '%s' (%d) was removed", configuredTarget.targetName, configuredTarget.targetIdentifier);
489 setTimeout(() => this.emit("target-remove" /* RemoteControllerEvents.TARGET_REMOVED */, targetConfiguration.targetIdentifier), 0);
490 const keys = Object.keys(this.targetConfigurations);
491 this.setActiveIdentifier(keys.length === 0 ? 0 : parseInt(keys[0], 10)); // switch to next available remote
492 }
493 this.updatedTargetConfiguration(); // set response
494 return 0 /* HAPStatus.SUCCESS */;
495 }
496 handleResetTargets(targetConfiguration) {
497 if (targetConfiguration) {
498 return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
499 }
500 debug("Resetting all target configurations");
501 this.targetConfigurations = new Map();
502 this.updatedTargetConfiguration(); // set response
503 setTimeout(() => this.emit("targets-reset" /* RemoteControllerEvents.TARGETS_RESET */), 0);
504 this.setActiveIdentifier(0); // resetting active identifier (also sets active to false)
505 return 0 /* HAPStatus.SUCCESS */;
506 }
507 handleListTargets(targetConfiguration) {
508 if (targetConfiguration) {
509 return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
510 }
511 // this.targetConfigurationsString is updated after each change, so we basically don't need to do anything here
512 debug("Returning " + Object.keys(this.targetConfigurations).length + " target configurations");
513 return 0 /* HAPStatus.SUCCESS */;
514 }
515 handleActiveWrite(value, callback, connection) {
516 if (this.activeIdentifier === 0) {
517 debug("Tried to change active state. There is no active target set though");
518 callback(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */);
519 return;
520 }
521 if (this.activeConnection) {
522 this.activeConnection.removeListener("closed" /* HAPConnectionEvent.CLOSED */, this.activeConnectionDisconnectListener);
523 this.activeConnection = undefined;
524 this.activeConnectionDisconnectListener = undefined;
525 }
526 this.activeConnection = value ? connection : undefined;
527 if (this.activeConnection) { // register listener when hap connection disconnects
528 this.activeConnectionDisconnectListener = this.handleActiveSessionDisconnected.bind(this, this.activeConnection);
529 this.activeConnection.on("closed" /* HAPConnectionEvent.CLOSED */, this.activeConnectionDisconnectListener);
530 }
531 const activeTarget = this.targetConfigurations.get(this.activeIdentifier);
532 if (!activeTarget) {
533 callback(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */);
534 return;
535 }
536 debug("Remote with activeTarget '%s' (%d) was set to %s", activeTarget.targetName, this.activeIdentifier, value ? "ACTIVE" : "INACTIVE");
537 callback();
538 this.emit("active-change" /* RemoteControllerEvents.ACTIVE_CHANGE */, value);
539 }
540 setInactive() {
541 if (this.activeConnection === undefined) {
542 return;
543 }
544 this.activeConnection.removeListener("closed" /* HAPConnectionEvent.CLOSED */, this.activeConnectionDisconnectListener);
545 this.activeConnection = undefined;
546 this.activeConnectionDisconnectListener = undefined;
547 this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.Active).updateValue(false);
548 debug("Remote was set to INACTIVE");
549 setTimeout(() => this.emit("active-change" /* RemoteControllerEvents.ACTIVE_CHANGE */, false), 0);
550 }
551 handleActiveSessionDisconnected(connection) {
552 if (connection !== this.activeConnection) {
553 return;
554 }
555 debug("Active hap session disconnected!");
556 this.setInactive();
557 }
558 sendButtonEvent(button, buttonState) {
559 const buttonID = this.buttons[button];
560 if (buttonID === undefined || buttonID === 0) {
561 throw new Error("Tried sending button event for unsupported button (" + button + ")");
562 }
563 if (this.activeIdentifier === 0) { // cannot press button if no device is selected
564 throw new Error("Tried sending button event although no target was selected");
565 }
566 if (!this.isActive()) { // cannot press button if device is not active (aka no Apple TV is listening)
567 throw new Error("Tried sending button event although target was not marked as active");
568 }
569 if (button === 11 /* ButtonType.SIRI */ && this.audioSupported) {
570 if (buttonState === 1 /* ButtonState.DOWN */) { // start streaming session
571 this.handleSiriAudioStart();
572 }
573 else if (buttonState === 0 /* ButtonState.UP */) { // stop streaming session
574 this.handleSiriAudioStop();
575 }
576 return;
577 }
578 const buttonIdTlv = tlv.encode(1 /* ButtonEvent.BUTTON_ID */, buttonID);
579 const buttonStateTlv = tlv.encode(2 /* ButtonEvent.BUTTON_STATE */, buttonState);
580 const timestampTlv = tlv.encode(3 /* ButtonEvent.TIMESTAMP */, tlv.writeVariableUIntLE(new Date().getTime()));
581 const activeIdentifierTlv = tlv.encode(4 /* ButtonEvent.ACTIVE_IDENTIFIER */, tlv.writeUInt32(this.activeIdentifier));
582 this.lastButtonEvent = Buffer.concat([
583 buttonIdTlv, buttonStateTlv, timestampTlv, activeIdentifierTlv,
584 ]).toString("base64");
585 this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.ButtonEvent).sendEventNotification(this.lastButtonEvent);
586 }
587 parseTargetConfigurationTLV(data) {
588 const configTLV = tlv.decode(data);
589 const identifier = tlv.readUInt32(configTLV[1 /* TargetConfigurationTypes.TARGET_IDENTIFIER */]);
590 let name = undefined;
591 if (configTLV[2 /* TargetConfigurationTypes.TARGET_NAME */]) {
592 name = configTLV[2 /* TargetConfigurationTypes.TARGET_NAME */].toString();
593 }
594 let category = undefined;
595 if (configTLV[3 /* TargetConfigurationTypes.TARGET_CATEGORY */]) {
596 category = tlv.readUInt16(configTLV[3 /* TargetConfigurationTypes.TARGET_CATEGORY */]);
597 }
598 const buttonConfiguration = {};
599 if (configTLV[4 /* TargetConfigurationTypes.BUTTON_CONFIGURATION */]) {
600 const buttonConfigurationTLV = tlv.decodeList(configTLV[4 /* TargetConfigurationTypes.BUTTON_CONFIGURATION */], 1 /* ButtonConfigurationTypes.BUTTON_ID */);
601 buttonConfigurationTLV.forEach(entry => {
602 const buttonId = entry[1 /* ButtonConfigurationTypes.BUTTON_ID */][0];
603 const buttonType = tlv.readUInt16(entry[2 /* ButtonConfigurationTypes.BUTTON_TYPE */]);
604 let buttonName;
605 if (entry[3 /* ButtonConfigurationTypes.BUTTON_NAME */]) {
606 buttonName = entry[3 /* ButtonConfigurationTypes.BUTTON_NAME */].toString();
607 }
608 else {
609 // @ts-expect-error: forceConsistentCasingInFileNames compiler option
610 buttonName = ButtonType[buttonType];
611 }
612 buttonConfiguration[buttonId] = {
613 buttonID: buttonId,
614 buttonType: buttonType,
615 buttonName: buttonName,
616 };
617 });
618 }
619 return {
620 targetIdentifier: identifier,
621 targetName: name,
622 targetCategory: category,
623 buttonConfiguration: buttonConfiguration,
624 };
625 }
626 updatedTargetConfiguration() {
627 const bufferList = [];
628 for (const configuration of Object.values(this.targetConfigurations)) {
629 const targetIdentifier = tlv.encode(1 /* TargetConfigurationTypes.TARGET_IDENTIFIER */, tlv.writeUInt32(configuration.targetIdentifier));
630 const targetName = tlv.encode(2 /* TargetConfigurationTypes.TARGET_NAME */, configuration.targetName);
631 const targetCategory = tlv.encode(3 /* TargetConfigurationTypes.TARGET_CATEGORY */, tlv.writeUInt16(configuration.targetCategory));
632 const buttonConfigurationBuffers = [];
633 for (const value of configuration.buttonConfiguration.values()) {
634 let tlvBuffer = tlv.encode(1 /* ButtonConfigurationTypes.BUTTON_ID */, value.buttonID, 2 /* ButtonConfigurationTypes.BUTTON_TYPE */, tlv.writeUInt16(value.buttonType));
635 if (value.buttonName) {
636 tlvBuffer = Buffer.concat([
637 tlvBuffer,
638 tlv.encode(3 /* ButtonConfigurationTypes.BUTTON_NAME */, value.buttonName),
639 ]);
640 }
641 buttonConfigurationBuffers.push(tlvBuffer);
642 }
643 const buttonConfiguration = tlv.encode(4 /* TargetConfigurationTypes.BUTTON_CONFIGURATION */, Buffer.concat(buttonConfigurationBuffers));
644 const targetConfiguration = Buffer.concat([targetIdentifier, targetName, targetCategory, buttonConfiguration]);
645 bufferList.push(tlv.encode(2 /* TargetControlList.TARGET_CONFIGURATION */, targetConfiguration));
646 }
647 this.targetConfigurationsString = Buffer.concat(bufferList).toString("base64");
648 this.stateChangeDelegate?.();
649 }
650 buildTargetControlSupportedConfigurationTLV(configuration) {
651 const maximumTargets = tlv.encode(1 /* TargetControlCommands.MAXIMUM_TARGETS */, configuration.maximumTargets);
652 const ticksPerSecond = tlv.encode(2 /* TargetControlCommands.TICKS_PER_SECOND */, tlv.writeVariableUIntLE(configuration.ticksPerSecond));
653 const supportedButtonConfigurationBuffers = [];
654 configuration.supportedButtonConfiguration.forEach(value => {
655 const tlvBuffer = tlv.encode(1 /* SupportedButtonConfigurationTypes.BUTTON_ID */, value.buttonID, 2 /* SupportedButtonConfigurationTypes.BUTTON_TYPE */, tlv.writeUInt16(value.buttonType));
656 supportedButtonConfigurationBuffers.push(tlvBuffer);
657 });
658 const supportedButtonConfiguration = tlv.encode(3 /* TargetControlCommands.SUPPORTED_BUTTON_CONFIGURATION */, Buffer.concat(supportedButtonConfigurationBuffers));
659 const type = tlv.encode(4 /* TargetControlCommands.TYPE */, configuration.hardwareImplemented ? 1 : 0);
660 return Buffer.concat([maximumTargets, ticksPerSecond, supportedButtonConfiguration, type]).toString("base64");
661 }
662 // --------------------------------- SIRI/DATA STREAM --------------------------------
663 // eslint-disable-next-line @typescript-eslint/no-explicit-any
664 handleTargetControlWhoAmI(connection, message) {
665 const targetIdentifier = message.identifier;
666 this.dataStreamConnections.set(targetIdentifier, connection);
667 debug("Discovered HDS connection for targetIdentifier %s", targetIdentifier);
668 connection.addProtocolHandler("dataSend" /* Protocols.DATA_SEND */, this);
669 }
670 handleSiriAudioStart() {
671 if (!this.audioSupported) {
672 throw new Error("Cannot start siri stream on remote where siri is not supported");
673 }
674 if (!this.isActive()) {
675 debug("Tried opening Siri audio stream, however no controller is connected!");
676 return;
677 }
678 if (this.activeAudioSession && (!this.activeAudioSession.isClosing() || this.nextAudioSession)) {
679 // there is already a session running, which is not in closing state and/or there is even already a
680 // nextAudioSession running. ignoring start request
681 debug("Tried opening Siri audio stream, however there is already one in progress");
682 return;
683 }
684 const connection = this.dataStreamConnections.get(this.activeIdentifier); // get connection for current target
685 if (connection === undefined) { // target seems not connected, ignore it
686 debug("Tried opening Siri audio stream however target is not connected via HDS");
687 return;
688 }
689 // eslint-disable-next-line @typescript-eslint/no-use-before-define
690 const audioSession = new SiriAudioSession(connection, this.selectedAudioConfiguration, this.audioProducerConstructor, this.audioProducerOptions);
691 if (!this.activeAudioSession) {
692 this.activeAudioSession = audioSession;
693 }
694 else {
695 // we checked above that this only happens if the activeAudioSession is in closing state,
696 // so no collision with the input device can happen
697 this.nextAudioSession = audioSession;
698 }
699 audioSession.on("close" /* SiriAudioSessionEvents.CLOSE */, this.handleSiriAudioSessionClosed.bind(this, audioSession));
700 audioSession.start();
701 }
702 handleSiriAudioStop() {
703 if (this.activeAudioSession) {
704 if (!this.activeAudioSession.isClosing()) {
705 this.activeAudioSession.stop();
706 return;
707 }
708 else if (this.nextAudioSession && !this.nextAudioSession.isClosing()) {
709 this.nextAudioSession.stop();
710 return;
711 }
712 }
713 debug("handleSiriAudioStop called although no audio session was started");
714 }
715 // eslint-disable-next-line @typescript-eslint/no-explicit-any
716 handleDataSendAckEvent(message) {
717 const streamId = message.streamId;
718 const endOfStream = message.endOfStream;
719 if (this.activeAudioSession && this.activeAudioSession.streamId === streamId) {
720 this.activeAudioSession.handleDataSendAckEvent(endOfStream);
721 }
722 else if (this.nextAudioSession && this.nextAudioSession.streamId === streamId) {
723 this.nextAudioSession.handleDataSendAckEvent(endOfStream);
724 }
725 else {
726 debug("Received dataSend acknowledgment event for unknown streamId '%s'", streamId);
727 }
728 }
729 // eslint-disable-next-line @typescript-eslint/no-explicit-any
730 handleDataSendCloseEvent(message) {
731 const streamId = message.streamId;
732 const reason = message.reason;
733 if (this.activeAudioSession && this.activeAudioSession.streamId === streamId) {
734 this.activeAudioSession.handleDataSendCloseEvent(reason);
735 }
736 else if (this.nextAudioSession && this.nextAudioSession.streamId === streamId) {
737 this.nextAudioSession.handleDataSendCloseEvent(reason);
738 }
739 else {
740 debug("Received dataSend close event for unknown streamId '%s'", streamId);
741 }
742 }
743 handleSiriAudioSessionClosed(session) {
744 if (session === this.activeAudioSession) {
745 this.activeAudioSession = this.nextAudioSession;
746 this.nextAudioSession = undefined;
747 }
748 else if (session === this.nextAudioSession) {
749 this.nextAudioSession = undefined;
750 }
751 }
752 handleDataStreamConnectionClosed(connection) {
753 for (const [targetIdentifier, connection0] of this.dataStreamConnections) {
754 if (connection === connection0) {
755 debug("HDS connection disconnected for targetIdentifier %s", targetIdentifier);
756 this.dataStreamConnections.delete(targetIdentifier);
757 break;
758 }
759 }
760 }
761 // ------------------------------- AUDIO CONFIGURATION -------------------------------
762 // eslint-disable-next-line @typescript-eslint/no-explicit-any
763 handleSelectedAudioConfigurationWrite(value, callback) {
764 const data = Buffer.from(value, "base64");
765 const objects = tlv.decode(data);
766 const selectedAudioStreamConfiguration = tlv.decode(objects[1 /* SelectedAudioInputStreamConfigurationTypes.SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION */]);
767 const codec = selectedAudioStreamConfiguration[1 /* AudioCodecConfigurationTypes.CODEC_TYPE */][0];
768 const parameters = tlv.decode(selectedAudioStreamConfiguration[2 /* AudioCodecConfigurationTypes.CODEC_PARAMETERS */]);
769 const channels = parameters[1 /* AudioCodecParametersTypes.CHANNEL */][0];
770 const bitrate = parameters[2 /* AudioCodecParametersTypes.BIT_RATE */][0];
771 const samplerate = parameters[3 /* AudioCodecParametersTypes.SAMPLE_RATE */][0];
772 this.selectedAudioConfiguration = {
773 codecType: codec,
774 parameters: {
775 channels: channels,
776 bitrate: bitrate,
777 samplerate: samplerate,
778 rtpTime: 20,
779 },
780 };
781 this.selectedAudioConfigurationString = RemoteController.buildSelectedAudioConfigurationTLV({
782 audioCodecConfiguration: this.selectedAudioConfiguration,
783 });
784 callback();
785 }
786 static buildSupportedAudioConfigurationTLV(configuration) {
787 const codecConfigurationTLV = RemoteController.buildCodecConfigurationTLV(configuration.audioCodecConfiguration);
788 const supportedAudioStreamConfiguration = tlv.encode(1 /* SupportedAudioStreamConfigurationTypes.AUDIO_CODEC_CONFIGURATION */, codecConfigurationTLV);
789 return supportedAudioStreamConfiguration.toString("base64");
790 }
791 static buildSelectedAudioConfigurationTLV(configuration) {
792 const codecConfigurationTLV = RemoteController.buildCodecConfigurationTLV(configuration.audioCodecConfiguration);
793 const supportedAudioStreamConfiguration = tlv.encode(1 /* SelectedAudioInputStreamConfigurationTypes.SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION */, codecConfigurationTLV);
794 return supportedAudioStreamConfiguration.toString("base64");
795 }
796 static buildCodecConfigurationTLV(codecConfiguration) {
797 const parameters = codecConfiguration.parameters;
798 let parametersTLV = tlv.encode(1 /* AudioCodecParametersTypes.CHANNEL */, parameters.channels, 2 /* AudioCodecParametersTypes.BIT_RATE */, parameters.bitrate, 3 /* AudioCodecParametersTypes.SAMPLE_RATE */, parameters.samplerate);
799 if (parameters.rtpTime) {
800 parametersTLV = Buffer.concat([
801 parametersTLV,
802 tlv.encode(4 /* AudioCodecParametersTypes.PACKET_TIME */, parameters.rtpTime),
803 ]);
804 }
805 return tlv.encode(1 /* AudioCodecConfigurationTypes.CODEC_TYPE */, codecConfiguration.codecType, 2 /* AudioCodecConfigurationTypes.CODEC_PARAMETERS */, parametersTLV);
806 }
807 // -----------------------------------------------------------------------------------
808 /**
809 * @private
810 */
811 constructServices() {
812 this.targetControlManagementService = new Service_1.Service.TargetControlManagement("", "");
813 this.targetControlManagementService.setCharacteristic(Characteristic_1.Characteristic.TargetControlSupportedConfiguration, this.supportedConfiguration);
814 this.targetControlManagementService.setCharacteristic(Characteristic_1.Characteristic.TargetControlList, this.targetConfigurationsString);
815 this.targetControlManagementService.setPrimaryService();
816 // you can also expose multiple TargetControl services to control multiple apple tvs simultaneously.
817 // should we extend this class to support multiple TargetControl services or should users just create a second accessory?
818 this.targetControlService = new Service_1.Service.TargetControl("", "");
819 this.targetControlService.setCharacteristic(Characteristic_1.Characteristic.ActiveIdentifier, 0);
820 this.targetControlService.setCharacteristic(Characteristic_1.Characteristic.Active, false);
821 this.targetControlService.setCharacteristic(Characteristic_1.Characteristic.ButtonEvent, this.lastButtonEvent);
822 if (this.audioSupported) {
823 this.siriService = new Service_1.Service.Siri("", "");
824 this.siriService.setCharacteristic(Characteristic_1.Characteristic.SiriInputType, Characteristic_1.Characteristic.SiriInputType.PUSH_BUTTON_TRIGGERED_APPLE_TV);
825 this.audioStreamManagementService = new Service_1.Service.AudioStreamManagement("", "");
826 this.audioStreamManagementService.setCharacteristic(Characteristic_1.Characteristic.SupportedAudioStreamConfiguration, this.supportedAudioConfiguration);
827 this.audioStreamManagementService.setCharacteristic(Characteristic_1.Characteristic.SelectedAudioStreamConfiguration, this.selectedAudioConfigurationString);
828 this.dataStreamManagement = new datastream_1.DataStreamManagement();
829 this.siriService.addLinkedService(this.dataStreamManagement.getService());
830 this.siriService.addLinkedService(this.audioStreamManagementService);
831 }
832 return {
833 targetControlManagement: this.targetControlManagementService,
834 targetControl: this.targetControlService,
835 siri: this.siriService,
836 audioStreamManagement: this.audioStreamManagementService,
837 dataStreamTransportManagement: this.dataStreamManagement?.getService(),
838 };
839 }
840 /**
841 * @private
842 */
843 initWithServices(serviceMap) {
844 this.targetControlManagementService = serviceMap.targetControlManagement;
845 this.targetControlService = serviceMap.targetControl;
846 this.siriService = serviceMap.siri;
847 this.audioStreamManagementService = serviceMap.audioStreamManagement;
848 this.dataStreamManagement = new datastream_1.DataStreamManagement(serviceMap.dataStreamTransportManagement);
849 }
850 /**
851 * @private
852 */
853 configureServices() {
854 if (!this.targetControlManagementService || !this.targetControlService) {
855 throw new Error("Unexpected state: Services not configured!"); // playing it save
856 }
857 this.targetControlManagementService.getCharacteristic(Characteristic_1.Characteristic.TargetControlList)
858 .on("get" /* CharacteristicEventTypes.GET */, callback => {
859 callback(null, this.targetConfigurationsString);
860 })
861 .on("set" /* CharacteristicEventTypes.SET */, this.handleTargetControlWrite.bind(this));
862 this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.ActiveIdentifier)
863 .on("get" /* CharacteristicEventTypes.GET */, callback => {
864 callback(undefined, this.activeIdentifier);
865 });
866 this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.Active)
867 .on("get" /* CharacteristicEventTypes.GET */, callback => {
868 callback(undefined, this.isActive());
869 })
870 .on("set" /* CharacteristicEventTypes.SET */, (value, callback, context, connection) => {
871 if (!connection) {
872 debug("Set event handler for Remote.Active cannot be called from plugin. Connection undefined!");
873 callback(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */);
874 return;
875 }
876 this.handleActiveWrite(value, callback, connection);
877 });
878 this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.ButtonEvent)
879 .on("get" /* CharacteristicEventTypes.GET */, (callback) => {
880 callback(undefined, this.lastButtonEvent);
881 });
882 if (this.audioSupported) {
883 this.audioStreamManagementService.getCharacteristic(Characteristic_1.Characteristic.SelectedAudioStreamConfiguration)
884 .on("get" /* CharacteristicEventTypes.GET */, callback => {
885 callback(null, this.selectedAudioConfigurationString);
886 })
887 .on("set" /* CharacteristicEventTypes.SET */, this.handleSelectedAudioConfigurationWrite.bind(this))
888 .updateValue(this.selectedAudioConfigurationString);
889 this.dataStreamManagement
890 .onEventMessage("targetControl" /* Protocols.TARGET_CONTROL */, "whoami" /* Topics.WHOAMI */, this.handleTargetControlWhoAmI.bind(this))
891 .onServerEvent("connection-closed" /* DataStreamServerEvent.CONNECTION_CLOSED */, this.handleDataStreamConnectionClosed.bind(this));
892 this.eventHandler = {
893 ["ack" /* Topics.ACK */]: this.handleDataSendAckEvent.bind(this),
894 ["close" /* Topics.CLOSE */]: this.handleDataSendCloseEvent.bind(this),
895 };
896 }
897 }
898 /**
899 * @private
900 */
901 handleControllerRemoved() {
902 this.targetControlManagementService = undefined;
903 this.targetControlService = undefined;
904 this.siriService = undefined;
905 this.audioStreamManagementService = undefined;
906 this.eventHandler = undefined;
907 this.requestHandler = undefined;
908 this.dataStreamManagement?.destroy();
909 this.dataStreamManagement = undefined;
910 // the call to dataStreamManagement.destroy will close any open data stream connection
911 // which will result in a call to this.handleDataStreamConnectionClosed, cleaning up this.dataStreamConnections.
912 // It will also result in a call to SiriAudioSession.handleDataStreamConnectionClosed (if there are any open session)
913 // which again results in a call to this.handleSiriAudioSessionClosed,cleaning up this.activeAudioSession and this.nextAudioSession.
914 }
915 /**
916 * @private
917 */
918 handleFactoryReset() {
919 debug("Running factory reset. Resetting targets...");
920 this.handleResetTargets(undefined);
921 this.lastButtonEvent = "";
922 }
923 /**
924 * @private
925 */
926 serialize() {
927 if (!this.activeIdentifier && Object.keys(this.targetConfigurations).length === 0) {
928 return undefined;
929 }
930 return {
931 activeIdentifier: this.activeIdentifier,
932 targetConfigurations: [...this.targetConfigurations].reduce((obj, [key, value]) => {
933 obj[key] = value;
934 return obj;
935 }, {}),
936 };
937 }
938 /**
939 * @private
940 */
941 deserialize(serialized) {
942 this.activeIdentifier = serialized.activeIdentifier;
943 this.targetConfigurations = Object.entries(serialized.targetConfigurations).reduce((map, [key, value]) => {
944 const identifier = parseInt(key, 10);
945 map.set(identifier, value);
946 return map;
947 }, new Map());
948 this.updatedTargetConfiguration();
949 }
950 /**
951 * @private
952 */
953 setupStateChangeDelegate(delegate) {
954 this.stateChangeDelegate = delegate;
955 }
956}
957exports.RemoteController = RemoteController;
958/**
959 * @group Apple TV Remote
960 */
961var SiriAudioSessionEvents;
962(function (SiriAudioSessionEvents) {
963 SiriAudioSessionEvents["CLOSE"] = "close";
964})(SiriAudioSessionEvents || (exports.SiriAudioSessionEvents = SiriAudioSessionEvents = {}));
965/**
966 * Represents an ongoing audio transmission
967 * @group Apple TV Remote
968 */
969// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
970class SiriAudioSession extends events_1.EventEmitter {
971 connection;
972 selectedAudioConfiguration;
973 producer;
974 producerRunning = false; // indicates if the producer is running
975 producerTimer; // producer has a 3s timeout to produce the first frame, otherwise transmission will be cancelled
976 /**
977 * @private file private API
978 */
979 state = 0 /* SiriAudioSessionState.STARTING */;
980 streamId; // present when state >= SENDING
981 endOfStream = false;
982 audioFrameQueue = [];
983 maxQueueSize = 1024;
984 sequenceNumber = 0;
985 closeListener;
986 constructor(connection, selectedAudioConfiguration, producerConstructor,
987 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any
988 producerOptions) {
989 super();
990 this.connection = connection;
991 this.selectedAudioConfiguration = selectedAudioConfiguration;
992 this.producer = new producerConstructor(this.handleSiriAudioFrame.bind(this), this.handleProducerError.bind(this), producerOptions);
993 this.connection.on("closed" /* DataStreamConnectionEvent.CLOSED */, this.closeListener = this.handleDataStreamConnectionClosed.bind(this));
994 }
995 /**
996 * Called when siri button is pressed
997 */
998 start() {
999 debug("Sending request to start siri audio stream");
1000 // opening dataSend
1001 this.connection.sendRequest("dataSend" /* Protocols.DATA_SEND */, "open" /* Topics.OPEN */, {
1002 target: "controller",
1003 type: "audio.siri",
1004 }, (error, status, message) => {
1005 if (this.state === 3 /* SiriAudioSessionState.CLOSED */) {
1006 debug("Ignoring dataSend open response as the session is already closed");
1007 return;
1008 }
1009 assert_1.default.strictEqual(this.state, 0 /* SiriAudioSessionState.STARTING */);
1010 this.state = 1 /* SiriAudioSessionState.SENDING */;
1011 if (error || status) {
1012 if (error) { // errors get produced by hap-nodejs
1013 debug("Error occurred trying to start siri audio stream: %s", error.message);
1014 }
1015 else if (status) { // status codes are those returned by the hds response
1016 debug("Controller responded with non-zero status code: %s", datastream_1.HDSStatus[status]);
1017 }
1018 this.closed();
1019 }
1020 else {
1021 this.streamId = message.streamId;
1022 if (!this.producerRunning) { // audio producer errored in the meantime
1023 this.sendDataSendCloseEvent(3 /* HDSProtocolSpecificErrorReason.CANCELLED */);
1024 }
1025 else {
1026 debug("Successfully setup siri audio stream with streamId %d", this.streamId);
1027 }
1028 }
1029 });
1030 this.startAudioProducer(); // start audio producer and queue frames in the meantime
1031 }
1032 /**
1033 * @returns if the audio session is closing
1034 */
1035 isClosing() {
1036 return this.state >= 2 /* SiriAudioSessionState.CLOSING */;
1037 }
1038 /**
1039 * Called when siri button is released (or active identifier is changed to another device)
1040 */
1041 stop() {
1042 (0, assert_1.default)(this.state <= 1 /* SiriAudioSessionState.SENDING */, "state was higher than SENDING");
1043 debug("Stopping siri audio stream with streamId %d", this.streamId);
1044 this.endOfStream = true; // mark as endOfStream
1045 this.stopAudioProducer();
1046 if (this.state === 1 /* SiriAudioSessionState.SENDING */) {
1047 this.handleSiriAudioFrame(undefined); // send out last few audio frames with endOfStream property set
1048 this.state = 2 /* SiriAudioSessionState.CLOSING */; // we are waiting for an acknowledgment (triggered by endOfStream property)
1049 }
1050 else { // if state is not SENDING (aka state is STARTING) the callback for DATA_SEND OPEN did not yet return (or never will)
1051 this.closed();
1052 }
1053 }
1054 startAudioProducer() {
1055 this.producer.startAudioProduction(this.selectedAudioConfiguration);
1056 this.producerRunning = true;
1057 this.producerTimer = setTimeout(() => {
1058 debug("Didn't receive any frames from audio producer for stream with streamId %s. Canceling the stream now.", this.streamId);
1059 this.producerTimer = undefined;
1060 this.handleProducerError(3 /* HDSProtocolSpecificErrorReason.CANCELLED */);
1061 }, 3000);
1062 this.producerTimer.unref();
1063 }
1064 stopAudioProducer() {
1065 this.producer.stopAudioProduction();
1066 this.producerRunning = false;
1067 if (this.producerTimer) {
1068 clearTimeout(this.producerTimer);
1069 this.producerTimer = undefined;
1070 }
1071 }
1072 handleSiriAudioFrame(frame) {
1073 if (this.state >= 2 /* SiriAudioSessionState.CLOSING */) {
1074 return;
1075 }
1076 if (this.producerTimer) { // if producerTimer is defined, then this is the first frame we are receiving
1077 clearTimeout(this.producerTimer);
1078 this.producerTimer = undefined;
1079 }
1080 if (frame && this.audioFrameQueue.length < this.maxQueueSize) { // add frame to queue whilst it is not full
1081 this.audioFrameQueue.push(frame);
1082 }
1083 if (this.state !== 1 /* SiriAudioSessionState.SENDING */) { // dataSend isn't open yet
1084 return;
1085 }
1086 let queued;
1087 while ((queued = this.popSome()) !== null) { // send packets
1088 const packets = [];
1089 queued.forEach(frame => {
1090 const packetData = {
1091 data: frame.data,
1092 metadata: {
1093 rms: new datastream_1.Float32(frame.rms),
1094 sequenceNumber: new datastream_1.Int64(this.sequenceNumber++),
1095 },
1096 };
1097 packets.push(packetData);
1098 });
1099 const message = {
1100 packets: packets,
1101 streamId: new datastream_1.Int64(this.streamId),
1102 endOfStream: this.endOfStream,
1103 };
1104 try {
1105 this.connection.sendEvent("dataSend" /* Protocols.DATA_SEND */, "data" /* Topics.DATA */, message);
1106 }
1107 catch (error) {
1108 debug("Error occurred when trying to send audio frame of hds connection: %s", error.message);
1109 this.stopAudioProducer();
1110 this.closed();
1111 }
1112 if (this.endOfStream) {
1113 break; // popSome() returns empty list if endOfStream=true
1114 }
1115 }
1116 }
1117 handleProducerError(error) {
1118 if (this.state >= 2 /* SiriAudioSessionState.CLOSING */) {
1119 return;
1120 }
1121 this.stopAudioProducer(); // ensure backend is closed
1122 if (this.state === 1 /* SiriAudioSessionState.SENDING */) { // if state is less than sending dataSend isn't open (yet)
1123 this.sendDataSendCloseEvent(error); // cancel submission
1124 }
1125 }
1126 handleDataSendAckEvent(endOfStream) {
1127 assert_1.default.strictEqual(endOfStream, true);
1128 debug("Received acknowledgment for siri audio stream with streamId %s, closing it now", this.streamId);
1129 this.sendDataSendCloseEvent(0 /* HDSProtocolSpecificErrorReason.NORMAL */);
1130 }
1131 handleDataSendCloseEvent(reason) {
1132 // @ts-expect-error: forceConsistentCasingInFileNames compiler option
1133 debug("Received close event from controller with reason %s for stream with streamId %s", datastream_1.HDSProtocolSpecificErrorReason[reason], this.streamId);
1134 if (this.state <= 1 /* SiriAudioSessionState.SENDING */) {
1135 this.stopAudioProducer();
1136 }
1137 this.closed();
1138 }
1139 sendDataSendCloseEvent(reason) {
1140 (0, assert_1.default)(this.state >= 1 /* SiriAudioSessionState.SENDING */, "state was less than SENDING");
1141 (0, assert_1.default)(this.state <= 2 /* SiriAudioSessionState.CLOSING */, "state was higher than CLOSING");
1142 this.connection.sendEvent("dataSend" /* Protocols.DATA_SEND */, "close" /* Topics.CLOSE */, {
1143 streamId: new datastream_1.Int64(this.streamId),
1144 reason: new datastream_1.Int64(reason),
1145 });
1146 this.closed();
1147 }
1148 handleDataStreamConnectionClosed() {
1149 debug("Closing audio session with streamId %d", this.streamId);
1150 if (this.state <= 1 /* SiriAudioSessionState.SENDING */) {
1151 this.stopAudioProducer();
1152 }
1153 this.closed();
1154 }
1155 closed() {
1156 const lastState = this.state;
1157 this.state = 3 /* SiriAudioSessionState.CLOSED */;
1158 if (lastState !== 3 /* SiriAudioSessionState.CLOSED */) {
1159 this.emit("close" /* SiriAudioSessionEvents.CLOSE */);
1160 this.connection.removeListener("closed" /* DataStreamConnectionEvent.CLOSED */, this.closeListener);
1161 }
1162 this.removeAllListeners();
1163 }
1164 popSome() {
1165 if (this.audioFrameQueue.length < 5 && !this.endOfStream) {
1166 return null;
1167 }
1168 const size = Math.min(this.audioFrameQueue.length, 5); // 5 frames per hap packet seems fine
1169 const result = [];
1170 for (let i = 0; i < size; i++) {
1171 const element = this.audioFrameQueue.shift(); // removes first element
1172 result.push(element);
1173 }
1174 return result;
1175 }
1176}
1177exports.SiriAudioSession = SiriAudioSession;
1178//# sourceMappingURL=RemoteController.js.map
\No newline at end of file