hap-nodejs
Version:
HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.
929 lines (928 loc) • 60.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SiriAudioSession = exports.SiriAudioSessionEvents = exports.RemoteController = exports.RemoteControllerEvents = exports.TargetUpdates = exports.AudioCodecTypes = exports.ButtonState = exports.TargetCategory = exports.ButtonType = void 0;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const debug_1 = tslib_1.__importDefault(require("debug"));
const events_1 = require("events");
const Characteristic_1 = require("../Characteristic");
const datastream_1 = require("../datastream");
const Service_1 = require("../Service");
const tlv = tslib_1.__importStar(require("../util/tlv"));
const debug = (0, debug_1.default)("HAP-NodeJS:Remote:Controller");
var TargetControlCommands;
(function (TargetControlCommands) {
TargetControlCommands[TargetControlCommands["MAXIMUM_TARGETS"] = 1] = "MAXIMUM_TARGETS";
TargetControlCommands[TargetControlCommands["TICKS_PER_SECOND"] = 2] = "TICKS_PER_SECOND";
TargetControlCommands[TargetControlCommands["SUPPORTED_BUTTON_CONFIGURATION"] = 3] = "SUPPORTED_BUTTON_CONFIGURATION";
TargetControlCommands[TargetControlCommands["TYPE"] = 4] = "TYPE";
})(TargetControlCommands || (TargetControlCommands = {}));
var SupportedButtonConfigurationTypes;
(function (SupportedButtonConfigurationTypes) {
SupportedButtonConfigurationTypes[SupportedButtonConfigurationTypes["BUTTON_ID"] = 1] = "BUTTON_ID";
SupportedButtonConfigurationTypes[SupportedButtonConfigurationTypes["BUTTON_TYPE"] = 2] = "BUTTON_TYPE";
})(SupportedButtonConfigurationTypes || (SupportedButtonConfigurationTypes = {}));
/**
* @group Apple TV Remote
*/
var ButtonType;
(function (ButtonType) {
// noinspection JSUnusedGlobalSymbols
ButtonType[ButtonType["UNDEFINED"] = 0] = "UNDEFINED";
ButtonType[ButtonType["MENU"] = 1] = "MENU";
ButtonType[ButtonType["PLAY_PAUSE"] = 2] = "PLAY_PAUSE";
ButtonType[ButtonType["TV_HOME"] = 3] = "TV_HOME";
ButtonType[ButtonType["SELECT"] = 4] = "SELECT";
ButtonType[ButtonType["ARROW_UP"] = 5] = "ARROW_UP";
ButtonType[ButtonType["ARROW_RIGHT"] = 6] = "ARROW_RIGHT";
ButtonType[ButtonType["ARROW_DOWN"] = 7] = "ARROW_DOWN";
ButtonType[ButtonType["ARROW_LEFT"] = 8] = "ARROW_LEFT";
ButtonType[ButtonType["VOLUME_UP"] = 9] = "VOLUME_UP";
ButtonType[ButtonType["VOLUME_DOWN"] = 10] = "VOLUME_DOWN";
ButtonType[ButtonType["SIRI"] = 11] = "SIRI";
ButtonType[ButtonType["POWER"] = 12] = "POWER";
ButtonType[ButtonType["GENERIC"] = 13] = "GENERIC";
})(ButtonType || (exports.ButtonType = ButtonType = {}));
var TargetControlList;
(function (TargetControlList) {
TargetControlList[TargetControlList["OPERATION"] = 1] = "OPERATION";
TargetControlList[TargetControlList["TARGET_CONFIGURATION"] = 2] = "TARGET_CONFIGURATION";
})(TargetControlList || (TargetControlList = {}));
var Operation;
(function (Operation) {
// noinspection JSUnusedGlobalSymbols
Operation[Operation["UNDEFINED"] = 0] = "UNDEFINED";
Operation[Operation["LIST"] = 1] = "LIST";
Operation[Operation["ADD"] = 2] = "ADD";
Operation[Operation["REMOVE"] = 3] = "REMOVE";
Operation[Operation["RESET"] = 4] = "RESET";
Operation[Operation["UPDATE"] = 5] = "UPDATE";
})(Operation || (Operation = {}));
var TargetConfigurationTypes;
(function (TargetConfigurationTypes) {
TargetConfigurationTypes[TargetConfigurationTypes["TARGET_IDENTIFIER"] = 1] = "TARGET_IDENTIFIER";
TargetConfigurationTypes[TargetConfigurationTypes["TARGET_NAME"] = 2] = "TARGET_NAME";
TargetConfigurationTypes[TargetConfigurationTypes["TARGET_CATEGORY"] = 3] = "TARGET_CATEGORY";
TargetConfigurationTypes[TargetConfigurationTypes["BUTTON_CONFIGURATION"] = 4] = "BUTTON_CONFIGURATION";
})(TargetConfigurationTypes || (TargetConfigurationTypes = {}));
/**
* @group Apple TV Remote
*/
var TargetCategory;
(function (TargetCategory) {
// noinspection JSUnusedGlobalSymbols
TargetCategory[TargetCategory["UNDEFINED"] = 0] = "UNDEFINED";
TargetCategory[TargetCategory["APPLE_TV"] = 24] = "APPLE_TV";
})(TargetCategory || (exports.TargetCategory = TargetCategory = {}));
var ButtonConfigurationTypes;
(function (ButtonConfigurationTypes) {
ButtonConfigurationTypes[ButtonConfigurationTypes["BUTTON_ID"] = 1] = "BUTTON_ID";
ButtonConfigurationTypes[ButtonConfigurationTypes["BUTTON_TYPE"] = 2] = "BUTTON_TYPE";
ButtonConfigurationTypes[ButtonConfigurationTypes["BUTTON_NAME"] = 3] = "BUTTON_NAME";
})(ButtonConfigurationTypes || (ButtonConfigurationTypes = {}));
var ButtonEvent;
(function (ButtonEvent) {
ButtonEvent[ButtonEvent["BUTTON_ID"] = 1] = "BUTTON_ID";
ButtonEvent[ButtonEvent["BUTTON_STATE"] = 2] = "BUTTON_STATE";
ButtonEvent[ButtonEvent["TIMESTAMP"] = 3] = "TIMESTAMP";
ButtonEvent[ButtonEvent["ACTIVE_IDENTIFIER"] = 4] = "ACTIVE_IDENTIFIER";
})(ButtonEvent || (ButtonEvent = {}));
/**
* @group Apple TV Remote
*/
var ButtonState;
(function (ButtonState) {
ButtonState[ButtonState["UP"] = 0] = "UP";
ButtonState[ButtonState["DOWN"] = 1] = "DOWN";
})(ButtonState || (exports.ButtonState = ButtonState = {}));
var SelectedAudioInputStreamConfigurationTypes;
(function (SelectedAudioInputStreamConfigurationTypes) {
SelectedAudioInputStreamConfigurationTypes[SelectedAudioInputStreamConfigurationTypes["SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION"] = 1] = "SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION";
})(SelectedAudioInputStreamConfigurationTypes || (SelectedAudioInputStreamConfigurationTypes = {}));
// ----------
var SupportedAudioStreamConfigurationTypes;
(function (SupportedAudioStreamConfigurationTypes) {
// noinspection JSUnusedGlobalSymbols
SupportedAudioStreamConfigurationTypes[SupportedAudioStreamConfigurationTypes["AUDIO_CODEC_CONFIGURATION"] = 1] = "AUDIO_CODEC_CONFIGURATION";
SupportedAudioStreamConfigurationTypes[SupportedAudioStreamConfigurationTypes["COMFORT_NOISE_SUPPORT"] = 2] = "COMFORT_NOISE_SUPPORT";
})(SupportedAudioStreamConfigurationTypes || (SupportedAudioStreamConfigurationTypes = {}));
var AudioCodecConfigurationTypes;
(function (AudioCodecConfigurationTypes) {
AudioCodecConfigurationTypes[AudioCodecConfigurationTypes["CODEC_TYPE"] = 1] = "CODEC_TYPE";
AudioCodecConfigurationTypes[AudioCodecConfigurationTypes["CODEC_PARAMETERS"] = 2] = "CODEC_PARAMETERS";
})(AudioCodecConfigurationTypes || (AudioCodecConfigurationTypes = {}));
/**
* @group Camera
*/
var AudioCodecTypes;
(function (AudioCodecTypes) {
// noinspection JSUnusedGlobalSymbols
AudioCodecTypes[AudioCodecTypes["PCMU"] = 0] = "PCMU";
AudioCodecTypes[AudioCodecTypes["PCMA"] = 1] = "PCMA";
AudioCodecTypes[AudioCodecTypes["AAC_ELD"] = 2] = "AAC_ELD";
AudioCodecTypes[AudioCodecTypes["OPUS"] = 3] = "OPUS";
AudioCodecTypes[AudioCodecTypes["MSBC"] = 4] = "MSBC";
AudioCodecTypes[AudioCodecTypes["AMR"] = 5] = "AMR";
AudioCodecTypes[AudioCodecTypes["AMR_WB"] = 6] = "AMR_WB";
})(AudioCodecTypes || (exports.AudioCodecTypes = AudioCodecTypes = {}));
var AudioCodecParametersTypes;
(function (AudioCodecParametersTypes) {
AudioCodecParametersTypes[AudioCodecParametersTypes["CHANNEL"] = 1] = "CHANNEL";
AudioCodecParametersTypes[AudioCodecParametersTypes["BIT_RATE"] = 2] = "BIT_RATE";
AudioCodecParametersTypes[AudioCodecParametersTypes["SAMPLE_RATE"] = 3] = "SAMPLE_RATE";
AudioCodecParametersTypes[AudioCodecParametersTypes["PACKET_TIME"] = 4] = "PACKET_TIME"; // only present in selected audio codec parameters tlv
})(AudioCodecParametersTypes || (AudioCodecParametersTypes = {}));
var SiriAudioSessionState;
(function (SiriAudioSessionState) {
SiriAudioSessionState[SiriAudioSessionState["STARTING"] = 0] = "STARTING";
SiriAudioSessionState[SiriAudioSessionState["SENDING"] = 1] = "SENDING";
SiriAudioSessionState[SiriAudioSessionState["CLOSING"] = 2] = "CLOSING";
SiriAudioSessionState[SiriAudioSessionState["CLOSED"] = 3] = "CLOSED";
})(SiriAudioSessionState || (SiriAudioSessionState = {}));
/**
* @group Apple TV Remote
*/
var TargetUpdates;
(function (TargetUpdates) {
TargetUpdates[TargetUpdates["NAME"] = 0] = "NAME";
TargetUpdates[TargetUpdates["CATEGORY"] = 1] = "CATEGORY";
TargetUpdates[TargetUpdates["UPDATED_BUTTONS"] = 2] = "UPDATED_BUTTONS";
TargetUpdates[TargetUpdates["REMOVED_BUTTONS"] = 3] = "REMOVED_BUTTONS";
})(TargetUpdates || (exports.TargetUpdates = TargetUpdates = {}));
/**
* @group Apple TV Remote
*/
var RemoteControllerEvents;
(function (RemoteControllerEvents) {
/**
* This event is emitted when the active state of the remote has changed.
* active = true indicates that there is currently an Apple TV listening of button presses and audio streams.
*/
RemoteControllerEvents["ACTIVE_CHANGE"] = "active-change";
/**
* This event is emitted when the currently selected target has changed.
* Possible reasons for a changed active identifier: manual change via api call, first target configuration
* gets added, active target gets removed, accessory gets unpaired, reset request was sent.
* An activeIdentifier of 0 indicates that no target is selected.
*/
RemoteControllerEvents["ACTIVE_IDENTIFIER_CHANGE"] = "active-identifier-change";
/**
* This event is emitted when a new target configuration is received. As we currently do not persistently store
* configured targets, this will be called at every startup for every Apple TV configured in the home.
*/
RemoteControllerEvents["TARGET_ADDED"] = "target-add";
/**
* This event is emitted when an existing target was updated.
* The 'updates' array indicates what exactly was changed for the target.
*/
RemoteControllerEvents["TARGET_UPDATED"] = "target-update";
/**
* This event is emitted when an existing configuration for a target was removed.
*/
RemoteControllerEvents["TARGET_REMOVED"] = "target-remove";
/**
* This event is emitted when a reset of the target configuration is requested.
* With this event every configuration made should be reset. This event is also called
* when the accessory gets unpaired.
*/
RemoteControllerEvents["TARGETS_RESET"] = "targets-reset";
})(RemoteControllerEvents || (exports.RemoteControllerEvents = RemoteControllerEvents = {}));
/**
* Handles everything needed to implement a fully working HomeKit remote controller.
*
* @group Apple TV Remote
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
class RemoteController extends events_1.EventEmitter {
stateChangeDelegate;
audioSupported;
audioProducerConstructor;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
audioProducerOptions;
targetControlManagementService;
targetControlService;
siriService;
audioStreamManagementService;
dataStreamManagement;
buttons = {}; // internal mapping of buttonId to buttonType for supported buttons
supportedConfiguration;
targetConfigurations = new Map();
targetConfigurationsString = "";
lastButtonEvent = "";
activeIdentifier = 0; // id of 0 means no device selected
activeConnection; // session which marked this remote as active and listens for events and siri
activeConnectionDisconnectListener;
supportedAudioConfiguration;
selectedAudioConfiguration;
selectedAudioConfigurationString;
dataStreamConnections = new Map(); // maps targetIdentifiers to active data stream connections
activeAudioSession;
nextAudioSession;
/**
* @private
*/
eventHandler;
/**
* @private
*/
requestHandler;
/**
* Creates a new RemoteController.
* If siri voice input is supported the constructor to an SiriAudioStreamProducer needs to be supplied.
* Otherwise, a remote without voice support will be created.
*
* For every audio session a new SiriAudioStreamProducer will be constructed.
*
* @param audioProducerConstructor - constructor for a SiriAudioStreamProducer
* @param producerOptions - if supplied this argument will be supplied as third argument of the SiriAudioStreamProducer
* constructor. This should be used to supply configurations to the stream producer.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types
constructor(audioProducerConstructor, producerOptions) {
super();
this.audioSupported = audioProducerConstructor !== undefined;
this.audioProducerConstructor = audioProducerConstructor;
this.audioProducerOptions = producerOptions;
const configuration = this.constructSupportedConfiguration();
this.supportedConfiguration = this.buildTargetControlSupportedConfigurationTLV(configuration);
const audioConfiguration = this.constructSupportedAudioConfiguration();
this.supportedAudioConfiguration = RemoteController.buildSupportedAudioConfigurationTLV(audioConfiguration);
this.selectedAudioConfiguration = {
codecType: 3 /* AudioCodecTypes.OPUS */,
parameters: {
channels: 1,
bitrate: 0 /* AudioBitrate.VARIABLE */,
samplerate: 1 /* AudioSamplerate.KHZ_16 */,
rtpTime: 20,
},
};
this.selectedAudioConfigurationString = RemoteController.buildSelectedAudioConfigurationTLV({
audioCodecConfiguration: this.selectedAudioConfiguration,
});
}
/**
* @private
*/
controllerId() {
return "remote" /* DefaultControllerType.REMOTE */;
}
/**
* Set a new target as active target. A value of 0 indicates that no target is selected currently.
*
* @param activeIdentifier - target identifier
*/
setActiveIdentifier(activeIdentifier) {
if (activeIdentifier === this.activeIdentifier) {
return;
}
if (activeIdentifier !== 0 && !this.targetConfigurations.has(activeIdentifier)) {
throw Error("Tried setting unconfigured targetIdentifier to active");
}
debug("%d is now the active target", activeIdentifier);
this.activeIdentifier = activeIdentifier;
this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.ActiveIdentifier).updateValue(activeIdentifier);
if (this.activeAudioSession) {
this.handleSiriAudioStop();
}
setTimeout(() => this.emit("active-identifier-change" /* RemoteControllerEvents.ACTIVE_IDENTIFIER_CHANGE */, activeIdentifier), 0);
this.setInactive();
}
/**
* @returns if the current target is active, meaning the active device is listening for button events or audio sessions
*/
isActive() {
return !!this.activeConnection;
}
/**
* Checks if the supplied targetIdentifier is configured.
*
* @param targetIdentifier - The target identifier.
*/
isConfigured(targetIdentifier) {
return this.targetConfigurations.has(targetIdentifier);
}
/**
* Returns the targetIdentifier for a give device name
*
* @param name - The name of the device.
* @returns The targetIdentifier of the device or undefined if not existent.
*/
getTargetIdentifierByName(name) {
for (const [activeIdentifier, configuration] of Object.entries(this.targetConfigurations)) {
if (configuration.targetName === name) {
return parseInt(activeIdentifier, 10);
}
}
return undefined;
}
/**
* Sends a button event to press the supplied button.
*
* @param button - button to be pressed
*/
pushButton(button) {
this.sendButtonEvent(button, 1 /* ButtonState.DOWN */);
}
/**
* Sends a button event that the supplied button was released.
*
* @param button - button which was released
*/
releaseButton(button) {
this.sendButtonEvent(button, 0 /* ButtonState.UP */);
}
/**
* Presses a supplied button for a given time.
*
* @param button - button to be pressed and released
* @param time - time in milliseconds (defaults to 200ms)
*/
pushAndReleaseButton(button, time = 200) {
this.pushButton(button);
setTimeout(() => this.releaseButton(button), time);
}
// ---------------------------------- CONFIGURATION ----------------------------------
// override methods if you would like to change anything (but should not be necessary most likely)
constructSupportedConfiguration() {
const configuration = {
maximumTargets: 10, // some random number. (ten should be okay?)
ticksPerSecond: 1000, // we rely on unix timestamps
supportedButtonConfiguration: [],
hardwareImplemented: this.audioSupported, // siri is only allowed for hardware implemented remotes
};
const supportedButtons = [
1 /* ButtonType.MENU */, 2 /* ButtonType.PLAY_PAUSE */, 3 /* ButtonType.TV_HOME */, 4 /* ButtonType.SELECT */,
5 /* ButtonType.ARROW_UP */, 6 /* ButtonType.ARROW_RIGHT */, 7 /* ButtonType.ARROW_DOWN */, 8 /* ButtonType.ARROW_LEFT */,
9 /* ButtonType.VOLUME_UP */, 10 /* ButtonType.VOLUME_DOWN */, 12 /* ButtonType.POWER */, 13 /* ButtonType.GENERIC */,
];
if (this.audioSupported) { // add siri button if this remote supports it
supportedButtons.push(11 /* ButtonType.SIRI */);
}
supportedButtons.forEach(button => {
const buttonConfiguration = {
buttonID: 100 + button,
buttonType: button,
};
configuration.supportedButtonConfiguration.push(buttonConfiguration);
this.buttons[button] = buttonConfiguration.buttonID; // also saving mapping of type to id locally
});
return configuration;
}
constructSupportedAudioConfiguration() {
// the following parameters are expected from HomeKit for a remote
return {
audioCodecConfiguration: {
codecType: 3 /* AudioCodecTypes.OPUS */,
parameters: {
channels: 1,
bitrate: 0 /* AudioBitrate.VARIABLE */,
samplerate: 1 /* AudioSamplerate.KHZ_16 */,
},
},
};
}
// --------------------------------- TARGET CONTROL ----------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleTargetControlWrite(value, callback) {
const data = Buffer.from(value, "base64");
const objects = tlv.decode(data);
const operation = objects[1 /* TargetControlList.OPERATION */][0];
let targetConfiguration = undefined;
if (objects[2 /* TargetControlList.TARGET_CONFIGURATION */]) { // if target configuration was sent, parse it
targetConfiguration = this.parseTargetConfigurationTLV(objects[2 /* TargetControlList.TARGET_CONFIGURATION */]);
}
debug("Received TargetControl write operation %s", Operation[operation]);
let handler;
switch (operation) {
case Operation.ADD:
handler = this.handleAddTarget.bind(this);
break;
case Operation.UPDATE:
handler = this.handleUpdateTarget.bind(this);
break;
case Operation.REMOVE:
handler = this.handleRemoveTarget.bind(this);
break;
case Operation.RESET:
handler = this.handleResetTargets.bind(this);
break;
case Operation.LIST:
handler = this.handleListTargets.bind(this);
break;
default:
callback(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */, undefined);
return;
}
const status = handler(targetConfiguration);
if (status === 0 /* HAPStatus.SUCCESS */) {
callback(undefined, this.targetConfigurationsString); // passing value for write response
if (operation === Operation.ADD && this.activeIdentifier === 0) {
this.setActiveIdentifier(targetConfiguration.targetIdentifier);
}
}
else {
callback(new Error(status + ""));
}
}
handleAddTarget(targetConfiguration) {
if (!targetConfiguration) {
return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
}
this.targetConfigurations.set(targetConfiguration.targetIdentifier, targetConfiguration);
debug("Configured new target '" + targetConfiguration.targetName + "' with targetIdentifier '" + targetConfiguration.targetIdentifier + "'");
setTimeout(() => this.emit("target-add" /* RemoteControllerEvents.TARGET_ADDED */, targetConfiguration), 0);
this.updatedTargetConfiguration(); // set response
return 0 /* HAPStatus.SUCCESS */;
}
handleUpdateTarget(targetConfiguration) {
if (!targetConfiguration) {
return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
}
const updates = [];
const configuredTarget = this.targetConfigurations.get(targetConfiguration.targetIdentifier);
if (!configuredTarget) {
return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
}
if (targetConfiguration.targetName) {
debug("Target name was updated '%s' => '%s' (%d)", configuredTarget.targetName, targetConfiguration.targetName, configuredTarget.targetIdentifier);
configuredTarget.targetName = targetConfiguration.targetName;
updates.push(0 /* TargetUpdates.NAME */);
}
if (targetConfiguration.targetCategory) {
debug("Target category was updated '%d' => '%d' for target '%s' (%d)", configuredTarget.targetCategory, targetConfiguration.targetCategory, configuredTarget.targetName, configuredTarget.targetIdentifier);
configuredTarget.targetCategory = targetConfiguration.targetCategory;
updates.push(1 /* TargetUpdates.CATEGORY */);
}
if (targetConfiguration.buttonConfiguration) {
debug("%d button configurations were updated for target '%s' (%d)", Object.keys(targetConfiguration.buttonConfiguration).length, configuredTarget.targetName, configuredTarget.targetIdentifier);
for (const configuration of Object.values(targetConfiguration.buttonConfiguration)) {
const savedConfiguration = configuredTarget.buttonConfiguration[configuration.buttonID];
savedConfiguration.buttonType = configuration.buttonType;
savedConfiguration.buttonName = configuration.buttonName;
}
updates.push(2 /* TargetUpdates.UPDATED_BUTTONS */);
}
setTimeout(() => this.emit("target-update" /* RemoteControllerEvents.TARGET_UPDATED */, targetConfiguration, updates), 0);
this.updatedTargetConfiguration(); // set response
return 0 /* HAPStatus.SUCCESS */;
}
handleRemoveTarget(targetConfiguration) {
if (!targetConfiguration) {
return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
}
const configuredTarget = this.targetConfigurations.get(targetConfiguration.targetIdentifier);
if (!configuredTarget) {
return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
}
if (targetConfiguration.buttonConfiguration) {
for (const key in targetConfiguration.buttonConfiguration) {
if (Object.prototype.hasOwnProperty.call(targetConfiguration.buttonConfiguration, key)) {
delete configuredTarget.buttonConfiguration[key];
}
}
debug("Removed %d button configurations of target '%s' (%d)", Object.keys(targetConfiguration.buttonConfiguration).length, configuredTarget.targetName, configuredTarget.targetIdentifier);
setTimeout(() => this.emit("target-update" /* RemoteControllerEvents.TARGET_UPDATED */, configuredTarget, [3 /* TargetUpdates.REMOVED_BUTTONS */]), 0);
}
else {
this.targetConfigurations.delete(targetConfiguration.targetIdentifier);
debug("Target '%s' (%d) was removed", configuredTarget.targetName, configuredTarget.targetIdentifier);
setTimeout(() => this.emit("target-remove" /* RemoteControllerEvents.TARGET_REMOVED */, targetConfiguration.targetIdentifier), 0);
const keys = Object.keys(this.targetConfigurations);
this.setActiveIdentifier(keys.length === 0 ? 0 : parseInt(keys[0], 10)); // switch to next available remote
}
this.updatedTargetConfiguration(); // set response
return 0 /* HAPStatus.SUCCESS */;
}
handleResetTargets(targetConfiguration) {
if (targetConfiguration) {
return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
}
debug("Resetting all target configurations");
this.targetConfigurations = new Map();
this.updatedTargetConfiguration(); // set response
setTimeout(() => this.emit("targets-reset" /* RemoteControllerEvents.TARGETS_RESET */), 0);
this.setActiveIdentifier(0); // resetting active identifier (also sets active to false)
return 0 /* HAPStatus.SUCCESS */;
}
handleListTargets(targetConfiguration) {
if (targetConfiguration) {
return -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */;
}
// this.targetConfigurationsString is updated after each change, so we basically don't need to do anything here
debug("Returning " + Object.keys(this.targetConfigurations).length + " target configurations");
return 0 /* HAPStatus.SUCCESS */;
}
handleActiveWrite(value, callback, connection) {
if (this.activeIdentifier === 0) {
debug("Tried to change active state. There is no active target set though");
callback(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */);
return;
}
if (this.activeConnection) {
this.activeConnection.removeListener("closed" /* HAPConnectionEvent.CLOSED */, this.activeConnectionDisconnectListener);
this.activeConnection = undefined;
this.activeConnectionDisconnectListener = undefined;
}
this.activeConnection = value ? connection : undefined;
if (this.activeConnection) { // register listener when hap connection disconnects
this.activeConnectionDisconnectListener = this.handleActiveSessionDisconnected.bind(this, this.activeConnection);
this.activeConnection.on("closed" /* HAPConnectionEvent.CLOSED */, this.activeConnectionDisconnectListener);
}
const activeTarget = this.targetConfigurations.get(this.activeIdentifier);
if (!activeTarget) {
callback(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */);
return;
}
debug("Remote with activeTarget '%s' (%d) was set to %s", activeTarget.targetName, this.activeIdentifier, value ? "ACTIVE" : "INACTIVE");
callback();
this.emit("active-change" /* RemoteControllerEvents.ACTIVE_CHANGE */, value);
}
setInactive() {
if (this.activeConnection === undefined) {
return;
}
this.activeConnection.removeListener("closed" /* HAPConnectionEvent.CLOSED */, this.activeConnectionDisconnectListener);
this.activeConnection = undefined;
this.activeConnectionDisconnectListener = undefined;
this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.Active).updateValue(false);
debug("Remote was set to INACTIVE");
setTimeout(() => this.emit("active-change" /* RemoteControllerEvents.ACTIVE_CHANGE */, false), 0);
}
handleActiveSessionDisconnected(connection) {
if (connection !== this.activeConnection) {
return;
}
debug("Active hap session disconnected!");
this.setInactive();
}
sendButtonEvent(button, buttonState) {
const buttonID = this.buttons[button];
if (buttonID === undefined || buttonID === 0) {
throw new Error("Tried sending button event for unsupported button (" + button + ")");
}
if (this.activeIdentifier === 0) { // cannot press button if no device is selected
throw new Error("Tried sending button event although no target was selected");
}
if (!this.isActive()) { // cannot press button if device is not active (aka no Apple TV is listening)
throw new Error("Tried sending button event although target was not marked as active");
}
if (button === 11 /* ButtonType.SIRI */ && this.audioSupported) {
if (buttonState === 1 /* ButtonState.DOWN */) { // start streaming session
this.handleSiriAudioStart();
}
else if (buttonState === 0 /* ButtonState.UP */) { // stop streaming session
this.handleSiriAudioStop();
}
return;
}
const buttonIdTlv = tlv.encode(1 /* ButtonEvent.BUTTON_ID */, buttonID);
const buttonStateTlv = tlv.encode(2 /* ButtonEvent.BUTTON_STATE */, buttonState);
const timestampTlv = tlv.encode(3 /* ButtonEvent.TIMESTAMP */, tlv.writeVariableUIntLE(new Date().getTime()));
const activeIdentifierTlv = tlv.encode(4 /* ButtonEvent.ACTIVE_IDENTIFIER */, tlv.writeUInt32(this.activeIdentifier));
this.lastButtonEvent = Buffer.concat([
buttonIdTlv, buttonStateTlv, timestampTlv, activeIdentifierTlv,
]).toString("base64");
this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.ButtonEvent).sendEventNotification(this.lastButtonEvent);
}
parseTargetConfigurationTLV(data) {
const configTLV = tlv.decode(data);
const identifier = tlv.readUInt32(configTLV[1 /* TargetConfigurationTypes.TARGET_IDENTIFIER */]);
let name = undefined;
if (configTLV[2 /* TargetConfigurationTypes.TARGET_NAME */]) {
name = configTLV[2 /* TargetConfigurationTypes.TARGET_NAME */].toString();
}
let category = undefined;
if (configTLV[3 /* TargetConfigurationTypes.TARGET_CATEGORY */]) {
category = tlv.readUInt16(configTLV[3 /* TargetConfigurationTypes.TARGET_CATEGORY */]);
}
const buttonConfiguration = {};
if (configTLV[4 /* TargetConfigurationTypes.BUTTON_CONFIGURATION */]) {
const buttonConfigurationTLV = tlv.decodeList(configTLV[4 /* TargetConfigurationTypes.BUTTON_CONFIGURATION */], 1 /* ButtonConfigurationTypes.BUTTON_ID */);
buttonConfigurationTLV.forEach(entry => {
const buttonId = entry[1 /* ButtonConfigurationTypes.BUTTON_ID */][0];
const buttonType = tlv.readUInt16(entry[2 /* ButtonConfigurationTypes.BUTTON_TYPE */]);
let buttonName;
if (entry[3 /* ButtonConfigurationTypes.BUTTON_NAME */]) {
buttonName = entry[3 /* ButtonConfigurationTypes.BUTTON_NAME */].toString();
}
else {
// @ts-expect-error: forceConsistentCasingInFileNames compiler option
buttonName = ButtonType[buttonType];
}
buttonConfiguration[buttonId] = {
buttonID: buttonId,
buttonType: buttonType,
buttonName: buttonName,
};
});
}
return {
targetIdentifier: identifier,
targetName: name,
targetCategory: category,
buttonConfiguration: buttonConfiguration,
};
}
updatedTargetConfiguration() {
const bufferList = [];
for (const configuration of Object.values(this.targetConfigurations)) {
const targetIdentifier = tlv.encode(1 /* TargetConfigurationTypes.TARGET_IDENTIFIER */, tlv.writeUInt32(configuration.targetIdentifier));
const targetName = tlv.encode(2 /* TargetConfigurationTypes.TARGET_NAME */, configuration.targetName);
const targetCategory = tlv.encode(3 /* TargetConfigurationTypes.TARGET_CATEGORY */, tlv.writeUInt16(configuration.targetCategory));
const buttonConfigurationBuffers = [];
for (const value of configuration.buttonConfiguration.values()) {
let tlvBuffer = tlv.encode(1 /* ButtonConfigurationTypes.BUTTON_ID */, value.buttonID, 2 /* ButtonConfigurationTypes.BUTTON_TYPE */, tlv.writeUInt16(value.buttonType));
if (value.buttonName) {
tlvBuffer = Buffer.concat([
tlvBuffer,
tlv.encode(3 /* ButtonConfigurationTypes.BUTTON_NAME */, value.buttonName),
]);
}
buttonConfigurationBuffers.push(tlvBuffer);
}
const buttonConfiguration = tlv.encode(4 /* TargetConfigurationTypes.BUTTON_CONFIGURATION */, Buffer.concat(buttonConfigurationBuffers));
const targetConfiguration = Buffer.concat([targetIdentifier, targetName, targetCategory, buttonConfiguration]);
bufferList.push(tlv.encode(2 /* TargetControlList.TARGET_CONFIGURATION */, targetConfiguration));
}
this.targetConfigurationsString = Buffer.concat(bufferList).toString("base64");
this.stateChangeDelegate?.();
}
buildTargetControlSupportedConfigurationTLV(configuration) {
const maximumTargets = tlv.encode(1 /* TargetControlCommands.MAXIMUM_TARGETS */, configuration.maximumTargets);
const ticksPerSecond = tlv.encode(2 /* TargetControlCommands.TICKS_PER_SECOND */, tlv.writeVariableUIntLE(configuration.ticksPerSecond));
const supportedButtonConfigurationBuffers = [];
configuration.supportedButtonConfiguration.forEach(value => {
const tlvBuffer = tlv.encode(1 /* SupportedButtonConfigurationTypes.BUTTON_ID */, value.buttonID, 2 /* SupportedButtonConfigurationTypes.BUTTON_TYPE */, tlv.writeUInt16(value.buttonType));
supportedButtonConfigurationBuffers.push(tlvBuffer);
});
const supportedButtonConfiguration = tlv.encode(3 /* TargetControlCommands.SUPPORTED_BUTTON_CONFIGURATION */, Buffer.concat(supportedButtonConfigurationBuffers));
const type = tlv.encode(4 /* TargetControlCommands.TYPE */, configuration.hardwareImplemented ? 1 : 0);
return Buffer.concat([maximumTargets, ticksPerSecond, supportedButtonConfiguration, type]).toString("base64");
}
// --------------------------------- SIRI/DATA STREAM --------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleTargetControlWhoAmI(connection, message) {
const targetIdentifier = message.identifier;
this.dataStreamConnections.set(targetIdentifier, connection);
debug("Discovered HDS connection for targetIdentifier %s", targetIdentifier);
connection.addProtocolHandler("dataSend" /* Protocols.DATA_SEND */, this);
}
handleSiriAudioStart() {
if (!this.audioSupported) {
throw new Error("Cannot start siri stream on remote where siri is not supported");
}
if (!this.isActive()) {
debug("Tried opening Siri audio stream, however no controller is connected!");
return;
}
if (this.activeAudioSession && (!this.activeAudioSession.isClosing() || this.nextAudioSession)) {
// there is already a session running, which is not in closing state and/or there is even already a
// nextAudioSession running. ignoring start request
debug("Tried opening Siri audio stream, however there is already one in progress");
return;
}
const connection = this.dataStreamConnections.get(this.activeIdentifier); // get connection for current target
if (connection === undefined) { // target seems not connected, ignore it
debug("Tried opening Siri audio stream however target is not connected via HDS");
return;
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const audioSession = new SiriAudioSession(connection, this.selectedAudioConfiguration, this.audioProducerConstructor, this.audioProducerOptions);
if (!this.activeAudioSession) {
this.activeAudioSession = audioSession;
}
else {
// we checked above that this only happens if the activeAudioSession is in closing state,
// so no collision with the input device can happen
this.nextAudioSession = audioSession;
}
audioSession.on("close" /* SiriAudioSessionEvents.CLOSE */, this.handleSiriAudioSessionClosed.bind(this, audioSession));
audioSession.start();
}
handleSiriAudioStop() {
if (this.activeAudioSession) {
if (!this.activeAudioSession.isClosing()) {
this.activeAudioSession.stop();
return;
}
else if (this.nextAudioSession && !this.nextAudioSession.isClosing()) {
this.nextAudioSession.stop();
return;
}
}
debug("handleSiriAudioStop called although no audio session was started");
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleDataSendAckEvent(message) {
const streamId = message.streamId;
const endOfStream = message.endOfStream;
if (this.activeAudioSession && this.activeAudioSession.streamId === streamId) {
this.activeAudioSession.handleDataSendAckEvent(endOfStream);
}
else if (this.nextAudioSession && this.nextAudioSession.streamId === streamId) {
this.nextAudioSession.handleDataSendAckEvent(endOfStream);
}
else {
debug("Received dataSend acknowledgment event for unknown streamId '%s'", streamId);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleDataSendCloseEvent(message) {
const streamId = message.streamId;
const reason = message.reason;
if (this.activeAudioSession && this.activeAudioSession.streamId === streamId) {
this.activeAudioSession.handleDataSendCloseEvent(reason);
}
else if (this.nextAudioSession && this.nextAudioSession.streamId === streamId) {
this.nextAudioSession.handleDataSendCloseEvent(reason);
}
else {
debug("Received dataSend close event for unknown streamId '%s'", streamId);
}
}
handleSiriAudioSessionClosed(session) {
if (session === this.activeAudioSession) {
this.activeAudioSession = this.nextAudioSession;
this.nextAudioSession = undefined;
}
else if (session === this.nextAudioSession) {
this.nextAudioSession = undefined;
}
}
handleDataStreamConnectionClosed(connection) {
for (const [targetIdentifier, connection0] of this.dataStreamConnections) {
if (connection === connection0) {
debug("HDS connection disconnected for targetIdentifier %s", targetIdentifier);
this.dataStreamConnections.delete(targetIdentifier);
break;
}
}
}
// ------------------------------- AUDIO CONFIGURATION -------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleSelectedAudioConfigurationWrite(value, callback) {
const data = Buffer.from(value, "base64");
const objects = tlv.decode(data);
const selectedAudioStreamConfiguration = tlv.decode(objects[1 /* SelectedAudioInputStreamConfigurationTypes.SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION */]);
const codec = selectedAudioStreamConfiguration[1 /* AudioCodecConfigurationTypes.CODEC_TYPE */][0];
const parameters = tlv.decode(selectedAudioStreamConfiguration[2 /* AudioCodecConfigurationTypes.CODEC_PARAMETERS */]);
const channels = parameters[1 /* AudioCodecParametersTypes.CHANNEL */][0];
const bitrate = parameters[2 /* AudioCodecParametersTypes.BIT_RATE */][0];
const samplerate = parameters[3 /* AudioCodecParametersTypes.SAMPLE_RATE */][0];
this.selectedAudioConfiguration = {
codecType: codec,
parameters: {
channels: channels,
bitrate: bitrate,
samplerate: samplerate,
rtpTime: 20,
},
};
this.selectedAudioConfigurationString = RemoteController.buildSelectedAudioConfigurationTLV({
audioCodecConfiguration: this.selectedAudioConfiguration,
});
callback();
}
static buildSupportedAudioConfigurationTLV(configuration) {
const codecConfigurationTLV = RemoteController.buildCodecConfigurationTLV(configuration.audioCodecConfiguration);
const supportedAudioStreamConfiguration = tlv.encode(1 /* SupportedAudioStreamConfigurationTypes.AUDIO_CODEC_CONFIGURATION */, codecConfigurationTLV);
return supportedAudioStreamConfiguration.toString("base64");
}
static buildSelectedAudioConfigurationTLV(configuration) {
const codecConfigurationTLV = RemoteController.buildCodecConfigurationTLV(configuration.audioCodecConfiguration);
const supportedAudioStreamConfiguration = tlv.encode(1 /* SelectedAudioInputStreamConfigurationTypes.SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION */, codecConfigurationTLV);
return supportedAudioStreamConfiguration.toString("base64");
}
static buildCodecConfigurationTLV(codecConfiguration) {
const parameters = codecConfiguration.parameters;
let parametersTLV = tlv.encode(1 /* AudioCodecParametersTypes.CHANNEL */, parameters.channels, 2 /* AudioCodecParametersTypes.BIT_RATE */, parameters.bitrate, 3 /* AudioCodecParametersTypes.SAMPLE_RATE */, parameters.samplerate);
if (parameters.rtpTime) {
parametersTLV = Buffer.concat([
parametersTLV,
tlv.encode(4 /* AudioCodecParametersTypes.PACKET_TIME */, parameters.rtpTime),
]);
}
return tlv.encode(1 /* AudioCodecConfigurationTypes.CODEC_TYPE */, codecConfiguration.codecType, 2 /* AudioCodecConfigurationTypes.CODEC_PARAMETERS */, parametersTLV);
}
// -----------------------------------------------------------------------------------
/**
* @private
*/
constructServices() {
this.targetControlManagementService = new Service_1.Service.TargetControlManagement("", "");
this.targetControlManagementService.setCharacteristic(Characteristic_1.Characteristic.TargetControlSupportedConfiguration, this.supportedConfiguration);
this.targetControlManagementService.setCharacteristic(Characteristic_1.Characteristic.TargetControlList, this.targetConfigurationsString);
this.targetControlManagementService.setPrimaryService();
// you can also expose multiple TargetControl services to control multiple apple tvs simultaneously.
// should we extend this class to support multiple TargetControl services or should users just create a second accessory?
this.targetControlService = new Service_1.Service.TargetControl("", "");
this.targetControlService.setCharacteristic(Characteristic_1.Characteristic.ActiveIdentifier, 0);
this.targetControlService.setCharacteristic(Characteristic_1.Characteristic.Active, false);
this.targetControlService.setCharacteristic(Characteristic_1.Characteristic.ButtonEvent, this.lastButtonEvent);
if (this.audioSupported) {
this.siriService = new Service_1.Service.Siri("", "");
this.siriService.setCharacteristic(Characteristic_1.Characteristic.SiriInputType, Characteristic_1.Characteristic.SiriInputType.PUSH_BUTTON_TRIGGERED_APPLE_TV);
this.audioStreamManagementService = new Service_1.Service.AudioStreamManagement("", "");
this.audioStreamManagementService.setCharacteristic(Characteristic_1.Characteristic.SupportedAudioStreamConfiguration, this.supportedAudioConfiguration);
this.audioStreamManagementService.setCharacteristic(Characteristic_1.Characteristic.SelectedAudioStreamConfiguration, this.selectedAudioConfigurationString);
this.dataStreamManagement = new datastream_1.DataStreamManagement();
this.siriService.addLinkedService(this.dataStreamManagement.getService());
this.siriService.addLinkedService(this.audioStreamManagementService);
}
return {
targetControlManagement: this.targetControlManagementService,
targetControl: this.targetControlService,
siri: this.siriService,
audioStreamManagement: this.audioStreamManagementService,
dataStreamTransportManagement: this.dataStreamManagement?.getService(),
};
}
/**
* @private
*/
initWithServices(serviceMap) {
this.targetControlManagementService = serviceMap.targetControlManagement;
this.targetControlService = serviceMap.targetControl;
this.siriService = serviceMap.siri;
this.audioStreamManagementService = serviceMap.audioStreamManagement;
this.dataStreamManagement = new datastream_1.DataStreamManagement(serviceMap.dataStreamTransportManagement);
}
/**
* @private
*/
configureServices() {
if (!this.targetControlManagementService || !this.targetControlService) {
throw new Error("Unexpected state: Services not configured!"); // playing it save
}
this.targetControlManagementService.getCharacteristic(Characteristic_1.Characteristic.TargetControlList)
.on("get" /* CharacteristicEventTypes.GET */, callback => {
callback(null, this.targetConfigurationsString);
})
.on("set" /* CharacteristicEventTypes.SET */, this.handleTargetControlWrite.bind(this));
this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.ActiveIdentifier)
.on("get" /* CharacteristicEventTypes.GET */, callback => {
callback(undefined, this.activeIdentifier);
});
this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.Active)
.on("get" /* CharacteristicEventTypes.GET */, callback => {
callback(undefined, this.isActive());
})
.on("set" /* CharacteristicEventTypes.SET */, (value, callback, context, connection) => {
if (!connection) {
debug("Set event handler for Remote.Active cannot be called from plugin. Connection undefined!");
callback(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */);
return;
}
this.handleActiveWrite(value, callback, connection);
});
this.targetControlService.getCharacteristic(Characteristic_1.Characteristic.ButtonEvent)
.on("get" /* CharacteristicEventTypes.GET */, (callback) => {
callback(undefined, this.lastButtonEvent);
});
if (this.audioSupported) {
this.audioStreamManagementService.getCharacteristic(Characteristic_1.Characteristic.SelectedAudioStreamConfiguration)
.on("get" /* CharacteristicEventTypes.GET */, callback => {
callback(null, this.selectedAudioConfigurationString);
})
.on("set" /* CharacteristicEventTypes.SET */, this.handleSelectedAudioConfigurationWrite.bind(this))
.updateValue(this.selectedAudioConfigurationString);
this.dataStreamManagement
.onEventMessage("targetControl" /* Protocols.TARGET_CONTROL */, "whoami" /* Topics.WHOAMI */, this.handleTargetControlWhoAmI.bind(this))
.onServerEvent("connection-closed" /* DataStreamServerEvent.CONNECTION_CLOSED */, this.handleDataStreamConnectionClosed.bind(this));
this.eventHandler = {
["ack" /* Topics.ACK */]: this.handleDataSendAckEvent.bind(this),
["close" /* Topics.CLOSE */]: this.handleDataSendCloseEvent.bind(this),
};
}
}
/**
* @private
*/
handleControllerRemoved() {
this.targetControlManagementService = undefined;
this.targetControlService = undefined;
this.siriService = undefined;
this.audioStreamManagementService = undefined;
this.eventHandler = undefined;
this.requestHandler = undefined;
this.dataStreamManagement?.destroy();
this.dataStreamManagement = undefined;
// the call to dataStreamManagement.destroy will close any open data stream connection
// which will result in a call to this.handleDataStreamConnectionClosed, cleaning up this.dataStreamConnections.
// It will also result in a call to SiriAudioSession.handleDataStreamConnectionClosed (if there are any open session)
// which again results in a call to this.handleSiriAudioSessionClosed,cleaning up this.activeAudioSession and this.nextAudioSession.
}
/**
* @private
*/
handleFactoryReset() {
debug("Running factory reset. Resetting targets...");
this.handleResetTargets(undefined);
this.lastButtonEvent = "";
}
/**
* @private
*/
serialize() {
if (!this.activeIdentifier && Object.keys(this.targetConfigurations).length === 0) {
return undefined;