UNPKG

hap-nodejs

Version:

HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.

1,178 lines • 60.8 kB
"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;
        }
        return {
            activeIdentifier: this.activeIdentifier,
            targetConfigurations: [...this.targetConfigurations].reduce((obj, [key, value]) => {
                obj[key] = value;
                return obj;
            }, {}),
        };
    }
    /**
     * @private
     */
    deserialize(serialized) {
        this.activeIdentifier = serialized.activeIdentifier;
        this.targetConfigurations = Object.entries(serialized.targetConfigurations).reduce((map, [key, value]) => {
            const identifier = parseInt(key, 10);
            map.set(identifier, value);
            return map;
        }, new Map());
        this.updatedTargetConfiguration();
    }
    /**
     * @private
     */
    setupStateChangeDelegate(delegate) {
        this.stateChangeDelegate = delegate;
    }
}
exports.RemoteController = RemoteController;
/**
 * @group Apple TV Remote
 */
var SiriAudioSessionEvents;
(function (SiriAudioSessionEvents) {
    SiriAudioSessionEvents["CLOSE"] = "close";
})(SiriAudioSessionEvents || (exports.SiriAudioSessionEvents = SiriAudioSessionEvents = {}));
/**
 * Represents an ongoing audio transmission
 * @group Apple TV Remote
 */
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
class SiriAudioSession extends events_1.EventEmitter {
    connection;
    selectedAudioConfiguration;
    producer;
    producerRunning = false; // indicates if the producer is running
    producerTimer; // producer has a 3s timeout to produce the first frame, otherwise transmission will be cancelled
    /**
     * @private file private API
     */
    state = 0 /* SiriAudioSessionState.STARTING */;
    streamId; // present when state >= SENDING
    endOfStream = false;
    audioFrameQueue = [];
    maxQueueSize = 1024;
    sequenceNumber = 0;
    closeListener;
    constructor(connection, selectedAudioConfiguration, producerConstructor, 
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any
    producerOptions) {
        super();
        this.connection = connection;
        this.selectedAudioConfiguration = selectedAudioConfiguration;
        this.producer = new producerConstructor(this.handleSiriAudioFrame.bind(this), this.handleProducerError.bind(this), producerOptions);
        this.connection.on("closed" /* DataStreamConnectionEvent.CLOSED */, this.closeListener = this.handleDataStreamConnectionClosed.bind(this));
    }
    /**
     * Called when siri button is pressed
     */
    start() {
        debug("Sending request to start siri audio stream");
        // opening dataSend
        this.connection.sendRequest("dataSend" /* Protocols.DATA_SEND */, "open" /* Topics.OPEN */, {
            target: "controller",
            type: "audio.siri",
        }, (error, status, message) => {
            if (this.state === 3 /* SiriAudioSessionState.CLOSED */) {
                debug("Ignoring dataSend open response as the session is already closed");
                return;
            }
            assert_1.default.strictEqual(this.state, 0 /* SiriAudioSessionState.STARTING */);
            this.state = 1 /* SiriAudioSessionState.SENDING */;
            if (error || status) {
                if (error) { // errors get produced by hap-nodejs
                    debug("Error occurred trying to start siri audio stream: %s", error.message);
                }
                else if (status) { // status codes are those returned by the hds response
                    debug("Controller responded with non-zero status code: %s", datastream_1.HDSStatus[status]);
                }
                this.closed();
            }
            else {
                this.streamId = message.streamId;
                if (!this.producerRunning) { // audio producer errored in the meantime
                    this.sendDataSendCloseEvent(3 /* HDSProtocolSpecificErrorReason.CANCELLED */);
                }
                else {
                    debug("Successfully setup siri audio stream with streamId %d", this.streamId);
                }
            }
        });
        this.startAudioProducer(); // start audio producer and queue frames in the meantime
    }
    /**
     * @returns if the audio session is closing
     */
    isClosing() {
        return this.state >= 2 /* SiriAudioSessionState.CLOSING */;
    }
    /**
     * Called when siri button is released (or active identifier is changed to another device)
     */
    stop() {
        (0, assert_1.default)(this.state <= 1 /* SiriAudioSessionState.SENDING */, "state was higher than SENDING");
        debug("Stopping siri audio stream with streamId %d", this.streamId);
        this.endOfStream = true; // mark as endOfStream
        this.stopAudioProducer();
        if (this.state === 1 /* SiriAudioSessionState.SENDING */) {
            this.handleSiriAudioFrame(undefined); // send out last few audio frames with endOfStream property set
            this.state = 2 /* SiriAudioSessionState.CLOSING */; // we are waiting for an acknowledgment (triggered by endOfStream property)
        }
        else { // if state is not SENDING (aka state is STARTING) the callback for DATA_SEND OPEN did not yet return (or never will)
            this.closed();
        }
    }
    startAudioProducer() {
        this.producer.startAudioProduction(this.selectedAudioConfiguration);
        this.producerRunning = true;
        this.producerTimer = setTimeout(() => {
            debug("Didn't receive any frames from audio producer for stream with streamId %s. Canceling the stream now.", this.streamId);
            this.producerTimer = undefined;
            this.handleProducerError(3 /* HDSProtocolSpecificErrorReason.CANCELLED */);
        }, 3000);
        this.producerTimer.unref();
    }
    stopAudioProducer() {
        this.producer.stopAudioProduction();
        this.producerRunning = false;
        if (this.producerTimer) {
            clearTimeout(this.producerTimer);
            this.producerTimer = undefined;
        }
    }
    handleSiriAudioFrame(frame) {
        if (this.state >= 2 /* SiriAudioSessionState.CLOSING */) {
            return;
        }
        if (this.producerTimer) { // if producerTimer is defined, then this is the first frame we are receiving
            clearTimeout(this.producerTimer);
            this.producerTimer = undefined;
        }
        if (frame && this.audioFrameQueue.length < this.maxQueueSize) { // add frame to queue whilst it is not full
            this.audioFrameQueue.push(frame);
        }
        if (this.state !== 1 /* SiriAudioSessionState.SENDING */) { // dataSend isn't open yet
            return;
        }
        let queued;
        while ((queued = this.popSome()) !== null) { // send packets
            const packets = [];
            queued.forEach(frame => {
                const packetData = {
                    data: frame.data,
                    metadata: {
                        rms: new datastream_1.Float32(frame.rms),
                        sequenceNumber: new datastream_1.Int64(this.sequenceNumber++),
                    },
                };
                packets.push(packetData);
            });
            const message = {
                packets: packets,
                streamId: new datastream_1.Int64(this.streamId),
                endOfStream: this.endOfStream,
            };
            try {
                this.connection.sendEvent("dataSend" /* Protocols.DATA_SEND */, "data" /* Topics.DATA */, message);
            }
            catch (error) {
                debug("Error occurred when trying to send audio frame of hds connection: %s", error.message);
                this.stopAudioProducer();
                this.closed();
            }
            if (this.endOfStream) {
                break; // popSome() returns empty list if endOfStream=true
            }
        }
    }
    handleProducerError(error) {
        if (this.state >= 2 /* SiriAudioSessionState.CLOSING */) {
            return;
        }
        this.stopAudioProducer(); // ensure backend is closed
        if (this.state === 1 /* SiriAudioSessionState.SENDING */) { // if state is less than sending dataSend isn't open (yet)
            this.sendDataSendCloseEvent(error); // cancel submission
        }
    }
    handleDataSendAckEvent(endOfStream) {
        assert_1.default.strictEqual(endOfStream, true);
        debug("Received acknowledgment for siri audio stream with streamId %s, closing it now", this.streamId);
        this.sendDataSendCloseEvent(0 /* HDSProtocolSpecificErrorReason.NORMAL */);
    }
    handleDataSendCloseEvent(reason) {
        // @ts-expect-error: forceConsistentCasingInFileNames compiler option
        debug("Received close event from controller with reason %s for stream with streamId %s", datastream_1.HDSProtocolSpecificErrorReason[reason], this.streamId);
        if (this.state <= 1 /* SiriAudioSessionState.SENDING */) {
            this.stopAudioProducer();
        }
        this.closed();
    }
    sendDataSendCloseEvent(reason) {
        (0, assert_1.default)(this.state >= 1 /* SiriAudioSessionState.SENDING */, "state was less than SENDING");
        (0, assert_1.default)(this.state <= 2 /* SiriAudioSessionState.CLOSING */, "state was higher than CLOSING");
        this.connection.sendEvent("dataSend" /* Protocols.DATA_SEND */, "close" /* Topics.CLOSE */, {
            streamId: new datastream_1.Int64(this.streamId),
            reason: new datastream_1.Int64(reason),
        });
        this.closed();
    }
    handleDataStreamConnectionClosed() {
        debug("Closing audio session with streamId %d", this.streamId);
        if (this.state <= 1 /* SiriAudioSessionState.SENDING */) {
            this.stopAudioProducer();
        }
        this.closed();
    }
    closed() {
        const lastState = this.state;
        this.state = 3 /* SiriAudioSessionState.CLOSED */;
        if (lastState !== 3 /* SiriAudioSessionState.CLOSED */) {
            this.emit("close" /* SiriAudioSessionEvents.CLOSE */);
            this.connection.removeListener("closed" /* DataStreamConnectionEvent.CLOSED */, this.closeListener);
        }
        this.removeAllListeners();
    }
    popSome() {
        if (this.audioFrameQueue.length < 5 && !this.endOfStream) {
            return null;
        }
        const size = Math.min(this.audioFrameQueue.length, 5); // 5 frames per hap packet seems fine
        const result = [];
        for (let i = 0; i < size; i++) {
            const element = this.audioFrameQueue.shift(); // removes first element
            result.push(element);
        }
        return result;
    }
}
exports.SiriAudioSession = SiriAudioSession;
//# sourceMappingURL=RemoteController.js.map