├── .gitattributes ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── appletv ├── dist ├── bin │ ├── index.d.ts │ ├── index.js │ ├── pair.d.ts │ ├── pair.js │ ├── scan.d.ts │ └── scan.js ├── index.d.ts ├── index.js ├── lib │ ├── appletv.d.ts │ ├── appletv.js │ ├── browser.d.ts │ ├── browser.js │ ├── connection.d.ts │ ├── connection.js │ ├── credentials.d.ts │ ├── credentials.js │ ├── message.d.ts │ ├── message.js │ ├── now-playing-info.d.ts │ ├── now-playing-info.js │ ├── pairing.d.ts │ ├── pairing.js │ ├── protos │ │ ├── AudioBuffer.proto │ │ ├── AudioFormatSettings.proto │ │ ├── ClientUpdatesConfigMessage.proto │ │ ├── CommandInfo.proto │ │ ├── CommandOptions.proto │ │ ├── CommandResultMessage.proto │ │ ├── ContentItem.proto │ │ ├── ContentItemMetadata.proto │ │ ├── CryptoPairingMessage.proto │ │ ├── DeviceInfoMessage.proto │ │ ├── DeviceInfoUpdate.proto │ │ ├── GetKeyboardSessionMessage.proto │ │ ├── GetStateMessage.proto │ │ ├── KeyboardMessage.proto │ │ ├── LanguageOption.proto │ │ ├── NotificationMessage.proto │ │ ├── NowPlayingClient.proto │ │ ├── NowPlayingInfo.proto │ │ ├── NowPlayingPlayer.proto │ │ ├── Origin.proto │ │ ├── PlaybackQueue.proto │ │ ├── PlaybackQueueCapabilities.proto │ │ ├── PlaybackQueueContext.proto │ │ ├── PlaybackQueueRequestMessage.proto │ │ ├── PlayerPath.proto │ │ ├── ProtocolMessage.proto │ │ ├── RegisterForGameControllerEventsMessage.proto │ │ ├── RegisterHIDDeviceMessage.proto │ │ ├── RegisterHIDDeviceResultMessage.proto │ │ ├── RegisterVoiceInputDeviceMessage.proto │ │ ├── RegisterVoiceInputDeviceResponseMessage.proto │ │ ├── SendButtonEventMessage.proto │ │ ├── SendCommandMessage.proto │ │ ├── SendHIDEventMessage.proto │ │ ├── SendPackedVirtualTouchEventMessage.proto │ │ ├── SendVirtualTouchEventMessage.proto │ │ ├── SendVoiceInputMessage.proto │ │ ├── SetArtworkMessage.proto │ │ ├── SetConnectionStateMessage.proto │ │ ├── SetHiliteModeMessage.proto │ │ ├── SetRecordingStateMessage.proto │ │ ├── SetStateMessage.proto │ │ ├── SupportedCommands.proto │ │ ├── TextEditingAttributes.proto │ │ ├── TextInputTraits.proto │ │ ├── TransactionKey.proto │ │ ├── TransactionMessage.proto │ │ ├── TransactionPacket.proto │ │ ├── TransactionPackets.proto │ │ ├── VirtualTouchDeviceDescriptor.proto │ │ ├── VirtualTouchEvent.proto │ │ ├── VoiceInputDeviceDescriptor.proto │ │ ├── VolumeControlAvailabilityMessage.proto │ │ └── WakeDeviceMessage.proto │ ├── supported-command.d.ts │ ├── supported-command.js │ ├── util │ │ ├── encryption.d.ts │ │ ├── encryption.js │ │ ├── number.d.ts │ │ ├── number.js │ │ ├── tlv.d.ts │ │ └── tlv.js │ ├── verifier.d.ts │ └── verifier.js └── test │ ├── appletv.spec.d.ts │ ├── appletv.spec.js │ ├── browser.spec.d.ts │ ├── browser.spec.js │ ├── encryption.spec.d.ts │ ├── encryption.spec.js │ ├── helpers │ ├── mock-server.d.ts │ └── mock-server.js │ ├── pairing.spec.d.ts │ └── pairing.spec.js ├── docs ├── .nojekyll ├── assets │ ├── css │ │ └── main.css │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ ├── main.js │ │ └── search.js ├── classes │ ├── _lib_appletv_.appletv.html │ ├── _lib_browser_.browser.html │ ├── _lib_connection_.connection.html │ ├── _lib_credentials_.credentials.html │ ├── _lib_message_.message.html │ ├── _lib_now_playing_info_.nowplayinginfo.html │ ├── _lib_pairing_.pairing.html │ ├── _lib_supported_command_.supportedcommand.html │ ├── _lib_verifier_.verifier.html │ └── _test_helpers_mock_server_.mockserver.html ├── enums │ ├── _lib_appletv_.appletv.key.html │ ├── _lib_message_.message.type.html │ ├── _lib_now_playing_info_.nowplayinginfo.state.html │ └── _lib_supported_command_.supportedcommand.command.html ├── globals.html ├── index.html ├── interfaces │ ├── _lib_appletv_.appletv.events.html │ ├── _lib_appletv_.clientupdatesconfig.html │ ├── _lib_appletv_.playbackqueuerequestoptions.html │ ├── _lib_appletv_.size.html │ ├── _lib_appletv_.staterequestcallback.html │ ├── _lib_connection_.connection.events.html │ └── _lib_connection_.messagecallback.html └── modules │ ├── _bin_index_.html │ ├── _bin_pair_.html │ ├── _bin_scan_.html │ ├── _index_.html │ ├── _lib_appletv_.html │ ├── _lib_browser_.html │ ├── _lib_connection_.html │ ├── _lib_credentials_.html │ ├── _lib_message_.html │ ├── _lib_now_playing_info_.html │ ├── _lib_pairing_.html │ ├── _lib_supported_command_.html │ ├── _lib_util_encryption_.html │ ├── _lib_util_number_.html │ ├── _lib_util_tlv_.html │ ├── _lib_verifier_.html │ ├── _test_appletv_spec_.html │ ├── _test_browser_spec_.html │ ├── _test_encryption_spec_.html │ ├── _test_helpers_mock_server_.html │ └── _test_pairing_spec_.html ├── images ├── pairing.gif └── state.gif ├── package-lock.json ├── package.json ├── scripts └── decode-tlv.js ├── src ├── bin │ ├── index.ts │ ├── pair.ts │ └── scan.ts ├── index.ts ├── lib │ ├── appletv.ts │ ├── browser.ts │ ├── connection.ts │ ├── credentials.ts │ ├── message.ts │ ├── now-playing-info.ts │ ├── pairing.ts │ ├── protos │ │ ├── AudioBuffer.proto │ │ ├── AudioFormatSettings.proto │ │ ├── ClientUpdatesConfigMessage.proto │ │ ├── CommandInfo.proto │ │ ├── CommandOptions.proto │ │ ├── CommandResultMessage.proto │ │ ├── ContentItem.proto │ │ ├── ContentItemMetadata.proto │ │ ├── CryptoPairingMessage.proto │ │ ├── DeviceInfoMessage.proto │ │ ├── DeviceInfoUpdate.proto │ │ ├── GetKeyboardSessionMessage.proto │ │ ├── GetStateMessage.proto │ │ ├── KeyboardMessage.proto │ │ ├── LanguageOption.proto │ │ ├── NotificationMessage.proto │ │ ├── NowPlayingClient.proto │ │ ├── NowPlayingInfo.proto │ │ ├── NowPlayingPlayer.proto │ │ ├── Origin.proto │ │ ├── PlaybackQueue.proto │ │ ├── PlaybackQueueCapabilities.proto │ │ ├── PlaybackQueueContext.proto │ │ ├── PlaybackQueueRequestMessage.proto │ │ ├── PlayerPath.proto │ │ ├── ProtocolMessage.proto │ │ ├── RegisterForGameControllerEventsMessage.proto │ │ ├── RegisterHIDDeviceMessage.proto │ │ ├── RegisterHIDDeviceResultMessage.proto │ │ ├── RegisterVoiceInputDeviceMessage.proto │ │ ├── RegisterVoiceInputDeviceResponseMessage.proto │ │ ├── SendButtonEventMessage.proto │ │ ├── SendCommandMessage.proto │ │ ├── SendHIDEventMessage.proto │ │ ├── SendPackedVirtualTouchEventMessage.proto │ │ ├── SendVirtualTouchEventMessage.proto │ │ ├── SendVoiceInputMessage.proto │ │ ├── SetArtworkMessage.proto │ │ ├── SetConnectionStateMessage.proto │ │ ├── SetHiliteModeMessage.proto │ │ ├── SetRecordingStateMessage.proto │ │ ├── SetStateMessage.proto │ │ ├── SupportedCommands.proto │ │ ├── TextEditingAttributes.proto │ │ ├── TextInputTraits.proto │ │ ├── TransactionKey.proto │ │ ├── TransactionMessage.proto │ │ ├── TransactionPacket.proto │ │ ├── TransactionPackets.proto │ │ ├── VirtualTouchDeviceDescriptor.proto │ │ ├── VirtualTouchEvent.proto │ │ ├── VoiceInputDeviceDescriptor.proto │ │ ├── VolumeControlAvailabilityMessage.proto │ │ └── WakeDeviceMessage.proto │ ├── supported-command.ts │ ├── util │ │ ├── encryption.ts │ │ ├── number.ts │ │ └── tlv.ts │ └── verifier.ts └── test │ ├── appletv.spec.ts │ ├── browser.spec.ts │ ├── encryption.spec.ts │ ├── fixtures │ └── now-playing.json │ ├── helpers │ └── mock-server.ts │ └── pairing.spec.ts └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/* linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.platform }} 8 | strategy: 9 | matrix: 10 | platform: [ubuntu-latest] 11 | node-version: [8.x, 10.x] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: Cache node modules 19 | uses: actions/cache@v1 20 | env: 21 | cache-name: cache-node-modules 22 | with: 23 | path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS 24 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.os }}-build-${{ env.cache-name }}- 27 | ${{ runner.os }}-build- 28 | ${{ runner.os }}- 29 | - run: sudo apt-get install libavahi-compat-libdnssd-dev 30 | - run: npm install 31 | - run: | 32 | if [ ! -f ./coverage/cc-test-reporter ]; then 33 | mkdir -p coverage/ 34 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./coverage/cc-test-reporter 35 | chmod +x ./coverage/cc-test-reporter 36 | fi 37 | name: Install code climate reporter 38 | - run: ./coverage/cc-test-reporter before-build 39 | env: 40 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 41 | - run: npm run build --if-present 42 | - run: npm run coverage 43 | name: Run tests 44 | - run: | 45 | ./coverage/cc-test-reporter format-coverage -t lcov -o ./coverage/coverage.json ./coverage/lcov.info 46 | ./coverage/cc-test-reporter upload-coverage -i ./coverage/coverage.json 47 | name: Upload to code climate 48 | env: 49 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 50 | 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | test.ts 63 | 64 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Next Version 4 | 5 | ## 1.1.0 6 | 7 | #### Added 8 | 9 | * Ability to request now playing artwork https://github.com/evandcoleman/node-appletv/pull/36 by @evandcoleman 10 | * Support for wake, home, and volume commands https://github.com/evandcoleman/node-appletv/pull/38 by @evandcoleman 11 | 12 | [Commits](https://github.com/evandcoleman/node-appletv/compare/1.0.11...1.1.0) 13 | 14 | ## 1.0.11 15 | 16 | #### Internal 17 | - Updated dependencies 18 | - Added tests 19 | 20 | [Commits](https://github.com/evandcoleman/node-appletv/compare/1.0.10...1.0.11) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Evan Coleman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-appletv 2 | 3 | > A node module for interacting with an Apple TV (4th-generation or later) over the Media Remote Protocol. 4 | 5 | [![npm version](https://badge.fury.io/js/node-appletv.svg)](https://badge.fury.io/js/node-appletv) 6 | [![Maintainability](https://img.shields.io/codeclimate/maintainability/evandcoleman/node-appletv)](https://codeclimate.com/github/evandcoleman/node-appletv) 7 | [![Coverage](https://img.shields.io/codeclimate/coverage/evandcoleman/node-appletv)](https://codeclimate.com/github/evandcoleman/node-appletv) 8 | ![Build](https://img.shields.io/github/workflow/status/evandcoleman/node-appletv/Tests/develop) 9 | [![License][license-image]][license-url] 10 | 11 | ![](images/pairing.gif) 12 | 13 | ## Overview 14 | 15 | `node-appletv` is a `node.js` implementation of the Media Remote Protocol which shipped with the 4th-generation Apple TV. This is the protocol that the Apple TV remote app uses, so this should enable the creation of an Apple TV remote app for various platforms. It can also be used in a `homebridge` plugin to connect Apple TV events to HomeKit and vice versa. `node-appletv` can be used as a standalone command line application, or as a module in your own node app. Keep reading for installation and usage instructions. 16 | 17 | ## Documentation 18 | 19 | Developer documentation for `node-appletv` can be found [here](https://evandcoleman.github.io/node-appletv/). 20 | 21 | ## Usage 22 | 23 | ### As a standalone cli 24 | 25 | ```bash 26 | # Install 27 | $ npm install -g node-appletv 28 | 29 | # Display built-in help 30 | $ appletv --help 31 | ``` 32 | 33 | The `appletv` cli supports several commands, such as: 34 | 35 | `pair`: Scans for Apple TVs on the local network and initiates the pairing process 36 | 37 | `command `: Execute a command on an Apple TV (play, pause, menu, volume, wake, suspend, etc.) 38 | 39 | `state`: Logs state changes from an Apple TV (now playing info) 40 | 41 | `queue`: Requests the current playback queue from an Apple TV 42 | 43 | `artwork`: Requests the current now playing artwork from an Apple TV 44 | 45 | `messages`: Logs all raw messages from an Apple TV 46 | 47 | `help `: Get help for a specific command 48 | 49 | 50 | ### As a node module 51 | 52 | ```bash 53 | $ npm install --save node-appletv 54 | ``` 55 | 56 | `node-appletv` makes heavy use of Promises. All functions, except for the observe functions, return Promises. 57 | 58 | ### Examples 59 | 60 | #### Scan for Apple TVs and pair 61 | 62 | ```typescript 63 | import { scan } from 'node-appletv'; 64 | 65 | let devices = await scan(); 66 | // devices is an array of AppleTV objects 67 | let device = devices[0]; 68 | await device.openConnection(); 69 | let callback = await device.pair(); 70 | // the pin is provided onscreen from the Apple TV 71 | await callback(pin); 72 | // you're paired! 73 | let credentials = device.credentials.toString(); 74 | console.log(credentials); 75 | ``` 76 | 77 | #### Connect to a paired Apple TV 78 | 79 | ```typescript 80 | import { scan, parseCredentials, NowPlayingInfo } from 'node-appletv'; 81 | 82 | // see example above for how to get the credentials string 83 | let credentials = parseCredentials(credentialsString); 84 | 85 | let devices = await scan(uniqueIdentifier)[] 86 | let device = devices[0]; 87 | await device.openConnection(credentials); 88 | // you're connected! 89 | // press menu 90 | await device.sendKeyCommand(AppleTV.Key.Menu); 91 | console.log("Sent a menu command!"); 92 | 93 | // monitor now playing info 94 | device.on('nowPlaying', (info: NowPlayingInfo) => { 95 | console.log(info.toString()); 96 | }); 97 | ``` 98 | 99 | The `uniqueIdentifier` is advertised by each Apple TV via Bonjour. Use an app like [Bonjour Browser](http://www.tildesoft.com) to find it. The identifier is also the first value in the string value of the `Credentials` object. 100 | 101 | See [homebridge-theater-mode](https://github.com/evandcoleman/homebridge-theater-mode) for a more practical use of this module. 102 | 103 | ## Development 104 | 105 | `node-appletv` is written in Typescript. Edit files in the `src` directory and then run `npm link` to clean, build, and create the symlinks to use the library and cli. 106 | 107 | ## Acknowledgments 108 | 109 | `node-appletv` would not have been possible without the work of these people: 110 | 111 | * [Jean Regisser](https://github.com/jeanregisser) who reversed the protobuf [spec of the MediaRemoteTV protocol](https://github.com/jeanregisser/mediaremotetv-protocol) 112 | * [Pierre Ståhl](https://github.com/postlund) who [implemented the protocol in Python](https://github.com/postlund/pyatv) 113 | * [Khaos Tian](https://github.com/KhaosT) for [reversing the HomeKit protocol](https://github.com/KhaosT/HAP-NodeJS) which also uses SRP encryption 114 | * [Zach Bean](https://github.com/forty2) for [implementing the HAP client spec](https://github.com/forty2/hap-client) 115 | 116 | ## Meta 117 | 118 | You can find me on Twitter [@evandcoleman](https://twitter.com/evandcoleman) 119 | 120 | Distributed under the MIT license. See ``LICENSE`` for more information. 121 | 122 | [license-image]: https://img.shields.io/badge/License-MIT-blue.svg 123 | [license-url]: LICENSE -------------------------------------------------------------------------------- /bin/appletv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/bin/index.js'); -------------------------------------------------------------------------------- /dist/bin/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/bin/pair.d.ts: -------------------------------------------------------------------------------- 1 | import { AppleTV } from '../lib/appletv'; 2 | export declare function pair(device: AppleTV, logger: Logger): Promise; 3 | -------------------------------------------------------------------------------- /dist/bin/pair.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const inquirer_1 = require("inquirer"); 4 | const ora = require("ora"); 5 | const pairing_1 = require("../lib/pairing"); 6 | function pair(device, logger) { 7 | let spinner = ora("Connecting to " + device.name).start(); 8 | return device 9 | .openConnection() 10 | .then(() => { 11 | spinner.succeed().start('Initiating Pairing'); 12 | let pairing = new pairing_1.Pairing(device); 13 | return pairing.initiatePair() 14 | .then(callback => { 15 | spinner.succeed(); 16 | return inquirer_1.prompt([{ 17 | type: 'input', 18 | name: 'pin', 19 | message: "Enter the 4-digit pin that's currently being displayed on " + device.name, 20 | validate: (input) => { 21 | let isValid = /^\d+$/.test(input); 22 | return isValid ? true : 'Pin must be 4-digits and all numbers.'; 23 | } 24 | }]) 25 | .then(answers => { 26 | spinner.start('Completing Pairing'); 27 | return callback(answers['pin']); 28 | }); 29 | }) 30 | .then(device => { 31 | spinner.succeed(); 32 | return device; 33 | }) 34 | .catch(error => { 35 | spinner.fail(); 36 | throw error; 37 | }); 38 | }); 39 | } 40 | exports.pair = pair; 41 | -------------------------------------------------------------------------------- /dist/bin/scan.d.ts: -------------------------------------------------------------------------------- 1 | import { AppleTV } from '../lib/appletv'; 2 | export declare function scan(logger: Logger, timeout?: number, uniqueIdentifier?: string): Promise; 3 | -------------------------------------------------------------------------------- /dist/bin/scan.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const inquirer_1 = require("inquirer"); 4 | const ora = require("ora"); 5 | const browser_1 = require("../lib/browser"); 6 | function scan(logger, timeout, uniqueIdentifier) { 7 | let browser = new browser_1.Browser(); 8 | let spinner = ora('Scanning for Apple TVs...').start(); 9 | return browser 10 | .scan(uniqueIdentifier, timeout) 11 | .then(devices => { 12 | spinner.stop(); 13 | if (devices.length == 1) { 14 | return devices[0]; 15 | } 16 | if (devices.length == 0) { 17 | throw new Error("No Apple TVs found on the network. Try again."); 18 | } 19 | else { 20 | return inquirer_1.prompt([{ 21 | type: 'list', 22 | name: 'device', 23 | message: 'Which Apple TV would you like to pair with?', 24 | choices: devices.map(device => { 25 | return { 26 | name: device.name + " (" + device.address + ":" + device.port + ")", 27 | value: device.uid 28 | }; 29 | }) 30 | }]) 31 | .then(answers => { 32 | let uid = answers['device']; 33 | return devices.filter(device => { return device.uid == uid; })[0]; 34 | }); 35 | } 36 | }); 37 | } 38 | exports.scan = scan; 39 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Credentials } from './lib/credentials'; 2 | import { AppleTV } from './lib/appletv'; 3 | import { Connection } from './lib/connection'; 4 | import { Browser } from './lib/browser'; 5 | import { NowPlayingInfo } from './lib/now-playing-info'; 6 | import { Message } from './lib/message'; 7 | import { SupportedCommand } from './lib/supported-command'; 8 | /** 9 | * A convenience function to scan for AppleTVs on the local network. 10 | * @param uniqueIdentifier An optional identifier for the AppleTV to scan for. The AppleTV advertises this via Bonjour. 11 | * @param timeout An optional timeout value (in seconds) to give up the search after. 12 | * @returns A promise that resolves to an array of AppleTV objects. If you provide a `uniqueIdentifier` the array is guaranteed to only contain one object. 13 | */ 14 | export declare function scan(uniqueIdentifier?: string, timeout?: number): Promise; 15 | /** 16 | * A convenience function to parse a credentials string into a Credentials object. 17 | * @param text The credentials string. 18 | * @returns A credentials object. 19 | */ 20 | export declare function parseCredentials(text: string): Credentials; 21 | export { AppleTV, Connection, Browser, NowPlayingInfo, Credentials, Message, SupportedCommand }; 22 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const credentials_1 = require("./lib/credentials"); 4 | exports.Credentials = credentials_1.Credentials; 5 | const appletv_1 = require("./lib/appletv"); 6 | exports.AppleTV = appletv_1.AppleTV; 7 | const connection_1 = require("./lib/connection"); 8 | exports.Connection = connection_1.Connection; 9 | const browser_1 = require("./lib/browser"); 10 | exports.Browser = browser_1.Browser; 11 | const now_playing_info_1 = require("./lib/now-playing-info"); 12 | exports.NowPlayingInfo = now_playing_info_1.NowPlayingInfo; 13 | const message_1 = require("./lib/message"); 14 | exports.Message = message_1.Message; 15 | const supported_command_1 = require("./lib/supported-command"); 16 | exports.SupportedCommand = supported_command_1.SupportedCommand; 17 | /** 18 | * A convenience function to scan for AppleTVs on the local network. 19 | * @param uniqueIdentifier An optional identifier for the AppleTV to scan for. The AppleTV advertises this via Bonjour. 20 | * @param timeout An optional timeout value (in seconds) to give up the search after. 21 | * @returns A promise that resolves to an array of AppleTV objects. If you provide a `uniqueIdentifier` the array is guaranteed to only contain one object. 22 | */ 23 | function scan(uniqueIdentifier, timeout) { 24 | let browser = new browser_1.Browser(); 25 | return browser.scan(uniqueIdentifier, timeout); 26 | } 27 | exports.scan = scan; 28 | /** 29 | * A convenience function to parse a credentials string into a Credentials object. 30 | * @param text The credentials string. 31 | * @returns A credentials object. 32 | */ 33 | function parseCredentials(text) { 34 | return credentials_1.Credentials.parse(text); 35 | } 36 | exports.parseCredentials = parseCredentials; 37 | -------------------------------------------------------------------------------- /dist/lib/appletv.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Service } from 'mdns'; 3 | import { EventEmitter } from 'events'; 4 | import { Socket } from 'net'; 5 | import { Connection } from './connection'; 6 | import { Credentials } from './credentials'; 7 | import { NowPlayingInfo } from './now-playing-info'; 8 | import { SupportedCommand } from './supported-command'; 9 | import { Message } from './message'; 10 | export interface Size { 11 | width: number; 12 | height: number; 13 | } 14 | export interface PlaybackQueueRequestOptions { 15 | location: number; 16 | length: number; 17 | includeMetadata?: boolean; 18 | includeLanguageOptions?: boolean; 19 | includeLyrics?: boolean; 20 | artworkSize?: Size; 21 | } 22 | export interface ClientUpdatesConfig { 23 | artworkUpdates: boolean; 24 | nowPlayingUpdates: boolean; 25 | volumeUpdates: boolean; 26 | keyboardUpdates: boolean; 27 | } 28 | export declare class AppleTV extends EventEmitter { 29 | private service; 30 | name: string; 31 | address: string; 32 | port: number; 33 | uid: string; 34 | pairingId: string; 35 | credentials: Credentials; 36 | connection: Connection; 37 | private queuePollTimer?; 38 | constructor(service: Service, socket?: Socket); 39 | /** 40 | * Pair with an already discovered AppleTV. 41 | * @returns A promise that resolves to the AppleTV object. 42 | */ 43 | pair(): Promise<(pin: string) => Promise>; 44 | /** 45 | * Opens a connection to the AppleTV over the MRP protocol. 46 | * @param credentials The credentials object for this AppleTV 47 | * @returns A promise that resolves to the AppleTV object. 48 | */ 49 | openConnection(credentials?: Credentials): Promise; 50 | /** 51 | * Closes the connection to the Apple TV. 52 | */ 53 | closeConnection(): void; 54 | /** 55 | * Send a Protobuf message to the AppleTV. This is for advanced usage only. 56 | * @param definitionFilename The Protobuf filename of the message type. 57 | * @param messageType The name of the message. 58 | * @param body The message body 59 | * @param waitForResponse Whether or not to wait for a response before resolving the Promise. 60 | * @returns A promise that resolves to the response from the AppleTV. 61 | */ 62 | sendMessage(definitionFilename: string, messageType: string, body: {}, waitForResponse: boolean, priority?: number): Promise; 63 | /** 64 | * Wait for a single message of a specified type. 65 | * @param type The type of the message to wait for. 66 | * @param timeout The timeout (in seconds). 67 | * @returns A promise that resolves to the Message. 68 | */ 69 | messageOfType(type: Message.Type, timeout?: number): Promise; 70 | /** 71 | * Requests the current playback queue from the Apple TV. 72 | * @param options Options to send 73 | * @returns A Promise that resolves to a NewPlayingInfo object. 74 | */ 75 | requestPlaybackQueue(options: PlaybackQueueRequestOptions): Promise; 76 | /** 77 | * Requests the current artwork from the Apple TV. 78 | * @param width Image width 79 | * @param height Image height 80 | * @returns A Promise that resolves to a Buffer of data. 81 | */ 82 | requestArtwork(width?: number, height?: number): Promise; 83 | /** 84 | * Send a key command to the AppleTV. 85 | * @param key The key to press. 86 | * @returns A promise that resolves to the AppleTV object after the message has been sent. 87 | */ 88 | sendKeyCommand(key: AppleTV.Key): Promise; 89 | waitForSequence(sequence: number, timeout?: number): Promise; 90 | private sendKeyPressAndRelease; 91 | private sendKeyPress; 92 | private requestPlaybackQueueWithWait; 93 | private sendIntroduction; 94 | private sendConnectionState; 95 | private sendClientUpdatesConfig; 96 | private sendWakeDevice; 97 | private onReceiveMessage; 98 | private onNewListener; 99 | private onRemoveListener; 100 | private setupListeners; 101 | } 102 | export declare module AppleTV { 103 | interface Events { 104 | connect: void; 105 | nowPlaying: NowPlayingInfo; 106 | supportedCommands: SupportedCommand[]; 107 | playbackQueue: any; 108 | message: Message; 109 | close: void; 110 | error: Error; 111 | debug: string; 112 | } 113 | } 114 | export declare module AppleTV { 115 | /** An enumeration of key presses available. 116 | */ 117 | enum Key { 118 | Up = 0, 119 | Down = 1, 120 | Left = 2, 121 | Right = 3, 122 | Menu = 4, 123 | Play = 5, 124 | Pause = 6, 125 | Next = 7, 126 | Previous = 8, 127 | Suspend = 9, 128 | Wake = 10, 129 | Select = 11, 130 | Home = 12, 131 | VolumeUp = 13, 132 | VolumeDown = 14 133 | } 134 | /** Convert a string representation of a key to the correct enum type. 135 | * @param string The string. 136 | * @returns The key enum value. 137 | */ 138 | function key(string: string): AppleTV.Key; 139 | } 140 | -------------------------------------------------------------------------------- /dist/lib/browser.d.ts: -------------------------------------------------------------------------------- 1 | import { AppleTV } from './appletv'; 2 | export declare class Browser { 3 | private browser; 4 | private services; 5 | private uniqueIdentifier; 6 | private onComplete; 7 | private onFailure; 8 | /** 9 | * Creates a new Browser 10 | * @param log An optional function that takes a string to provide verbose logging. 11 | */ 12 | constructor(); 13 | /** 14 | * Scans for AppleTVs on the local network. 15 | * @param uniqueIdentifier An optional identifier for the AppleTV to scan for. The AppleTV advertises this via Bonjour. 16 | * @param timeout An optional timeout value (in seconds) to give up the search after. 17 | * @returns A promise that resolves to an array of AppleTV objects. If you provide a `uniqueIdentifier` the array is guaranteed to only contain one object. 18 | */ 19 | scan(uniqueIdentifier?: string, timeout?: number): Promise; 20 | } 21 | -------------------------------------------------------------------------------- /dist/lib/browser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const mdns = require("mdns"); 4 | const appletv_1 = require("./appletv"); 5 | class Browser { 6 | /** 7 | * Creates a new Browser 8 | * @param log An optional function that takes a string to provide verbose logging. 9 | */ 10 | constructor() { 11 | let sequence = [ 12 | mdns.rst.DNSServiceResolve(), 13 | 'DNSServiceGetAddrInfo' in mdns.dns_sd ? mdns.rst.DNSServiceGetAddrInfo() : mdns.rst.getaddrinfo({ families: [4] }), 14 | mdns.rst.makeAddressesUnique() 15 | ]; 16 | this.browser = mdns.createBrowser(mdns.tcp('mediaremotetv'), { resolverSequence: sequence }); 17 | this.services = []; 18 | let that = this; 19 | this.browser.on('serviceUp', function (service) { 20 | let device = new appletv_1.AppleTV(service); 21 | if (that.uniqueIdentifier && device.uid == that.uniqueIdentifier) { 22 | that.browser.stop(); 23 | that.onComplete([device]); 24 | } 25 | else { 26 | that.services.push(device); 27 | } 28 | }); 29 | } 30 | /** 31 | * Scans for AppleTVs on the local network. 32 | * @param uniqueIdentifier An optional identifier for the AppleTV to scan for. The AppleTV advertises this via Bonjour. 33 | * @param timeout An optional timeout value (in seconds) to give up the search after. 34 | * @returns A promise that resolves to an array of AppleTV objects. If you provide a `uniqueIdentifier` the array is guaranteed to only contain one object. 35 | */ 36 | scan(uniqueIdentifier, timeout) { 37 | this.services = []; 38 | this.uniqueIdentifier = uniqueIdentifier; 39 | this.browser.start(); 40 | let that = this; 41 | let to = timeout == null ? 5 : timeout; 42 | return new Promise((resolve, reject) => { 43 | that.onComplete = resolve; 44 | that.onFailure = reject; 45 | setTimeout(() => { 46 | that.browser.stop(); 47 | if (that.uniqueIdentifier) { 48 | reject(new Error("Failed to locate specified AppleTV on the network")); 49 | } 50 | else { 51 | resolve(that.services 52 | .sort((a, b) => { 53 | return a > b ? 1 : -1; 54 | })); 55 | } 56 | }, to * 1000); 57 | }); 58 | } 59 | } 60 | exports.Browser = Browser; 61 | -------------------------------------------------------------------------------- /dist/lib/connection.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Socket } from 'net'; 3 | import { Message as ProtoMessage } from 'protobufjs'; 4 | import { EventEmitter } from 'events'; 5 | import { Credentials } from './credentials'; 6 | import { AppleTV } from './appletv'; 7 | import { Message } from './message'; 8 | export declare class Connection extends EventEmitter { 9 | device: AppleTV; 10 | isOpen: boolean; 11 | private socket; 12 | private callbacks; 13 | private ProtocolMessage; 14 | private buffer; 15 | constructor(device: AppleTV, socket?: Socket); 16 | private addCallback; 17 | private executeCallbacks; 18 | open(): Promise; 19 | close(): void; 20 | sendBlank(typeName: string, waitForResponse: boolean, credentials?: Credentials): Promise; 21 | send(message: ProtoMessage<{}>, waitForResponse: boolean, priority: number, credentials?: Credentials): Promise; 22 | private sendProtocolMessage; 23 | private decodeMessage; 24 | waitForSequence(sequence: number, timeout?: number): Promise; 25 | handleChunk(data: Buffer): Promise; 26 | private setupListeners; 27 | } 28 | export declare module Connection { 29 | interface Events { 30 | connect: void; 31 | message: Message; 32 | close: void; 33 | error: Error; 34 | debug: string; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /dist/lib/credentials.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export declare class Credentials { 3 | uniqueIdentifier: string; 4 | identifier: Buffer; 5 | pairingId: string; 6 | publicKey: Buffer; 7 | encryptionKey: Buffer; 8 | readKey: Buffer; 9 | writeKey: Buffer; 10 | private encryptCount; 11 | private decryptCount; 12 | constructor(uniqueIdentifier: string, identifier: Buffer, pairingId: string, publicKey: Buffer, encryptionKey: Buffer); 13 | /** 14 | * Parse a credentials string into a Credentials object. 15 | * @param text The credentials string. 16 | * @returns A credentials object. 17 | */ 18 | static parse(text: string): Credentials; 19 | /** 20 | * Returns a string representation of a Credentials object. 21 | * @returns A string representation of a Credentials object. 22 | */ 23 | toString(): string; 24 | encrypt(message: Buffer): Buffer; 25 | decrypt(message: Buffer): Buffer; 26 | } 27 | -------------------------------------------------------------------------------- /dist/lib/credentials.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const encryption_1 = require("./util/encryption"); 4 | const number_1 = require("./util/number"); 5 | class Credentials { 6 | constructor(uniqueIdentifier, identifier, pairingId, publicKey, encryptionKey) { 7 | this.uniqueIdentifier = uniqueIdentifier; 8 | this.identifier = identifier; 9 | this.pairingId = pairingId; 10 | this.publicKey = publicKey; 11 | this.encryptionKey = encryptionKey; 12 | this.encryptCount = 0; 13 | this.decryptCount = 0; 14 | } 15 | /** 16 | * Parse a credentials string into a Credentials object. 17 | * @param text The credentials string. 18 | * @returns A credentials object. 19 | */ 20 | static parse(text) { 21 | let parts = text.split(':'); 22 | return new Credentials(parts[0], Buffer.from(parts[1], 'hex'), Buffer.from(parts[2], 'hex').toString(), Buffer.from(parts[3], 'hex'), Buffer.from(parts[4], 'hex')); 23 | } 24 | /** 25 | * Returns a string representation of a Credentials object. 26 | * @returns A string representation of a Credentials object. 27 | */ 28 | toString() { 29 | return this.uniqueIdentifier 30 | + ":" 31 | + this.identifier.toString('hex') 32 | + ":" 33 | + Buffer.from(this.pairingId).toString('hex') 34 | + ":" 35 | + this.publicKey.toString('hex') 36 | + ":" 37 | + this.encryptionKey.toString('hex'); 38 | } 39 | encrypt(message) { 40 | let nonce = number_1.default.UInt53toBufferLE(this.encryptCount++); 41 | return Buffer.concat(encryption_1.default.encryptAndSeal(message, null, nonce, this.writeKey)); 42 | } 43 | decrypt(message) { 44 | let nonce = number_1.default.UInt53toBufferLE(this.decryptCount++); 45 | let cipherText = message.slice(0, -16); 46 | let hmac = message.slice(-16); 47 | return encryption_1.default.verifyAndDecrypt(cipherText, hmac, null, nonce, this.readKey); 48 | } 49 | } 50 | exports.Credentials = Credentials; 51 | -------------------------------------------------------------------------------- /dist/lib/message.d.ts: -------------------------------------------------------------------------------- 1 | import { Message as ProtoMessage } from 'protobufjs'; 2 | export declare class Message { 3 | private message; 4 | type: Message.Type; 5 | identifier: string; 6 | payload: any; 7 | constructor(message: ProtoMessage<{}>); 8 | toObject(): any; 9 | } 10 | export declare module Message { 11 | enum Type { 12 | SendCommandMessage = 1, 13 | CommandResultMessage = 2, 14 | GetStateMessage = 3, 15 | SetStateMessage = 4, 16 | SetArtworkMessage = 5, 17 | RegisterHidDeviceMessage = 6, 18 | RegisterHidDeviceResultMessage = 7, 19 | SendHidEventMessage = 8, 20 | SendHidReportMessage = 9, 21 | SendVirtualTouchEventMessage = 10, 22 | NotificationMessage = 11, 23 | ContentItemsChangedNotificationMessage = 12, 24 | DeviceInfoMessage = 15, 25 | ClientUpdatesConfigMessage = 16, 26 | VolumeControlAvailabilityMessage = 17, 27 | GameControllerMessage = 18, 28 | RegisterGameControllerMessage = 19, 29 | RegisterGameControllerResponseMessage = 20, 30 | UnregisterGameControllerMessage = 21, 31 | RegisterForGameControllerEventsMessage = 22, 32 | KeyboardMessage = 23, 33 | GetKeyboardSessionMessage = 24, 34 | TextInputMessage = 25, 35 | GetVoiceInputDevicesMessage = 26, 36 | GetVoiceInputDevicesResponseMessage = 27, 37 | RegisterVoiceInputDeviceMessage = 28, 38 | RegisterVoiceInputDeviceResponseMessage = 29, 39 | SetRecordingStateMessage = 30, 40 | SendVoiceInputMessage = 31, 41 | PlaybackQueueRequestMessage = 32, 42 | TransactionMessage = 33, 43 | CryptoPairingMessage = 34, 44 | GameControllerPropertiesMessage = 35, 45 | SetReadyStateMessage = 36, 46 | DeviceInfoUpdate = 37, 47 | SetDisconnectingStateMessage = 38, 48 | SendButtonEvent = 39, 49 | SetHiliteModeMessage = 40, 50 | WakeDeviceMessage = 41, 51 | GenericMessage = 42, 52 | SendPackedVirtualTouchEvent = 43, 53 | SendLyricsEvent = 44, 54 | PlaybackQueueCapabilitiesRequest = 45, 55 | ModifyOutputContextRequest = 46 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /dist/lib/message.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class Message { 4 | constructor(message) { 5 | this.message = message; 6 | this.type = message['type']; 7 | this.identifier = message['identifier']; 8 | let keys = Object.keys(message.toJSON()).filter(key => { return key[0] == "."; }); 9 | if (keys.length > 0) { 10 | this.payload = message[keys[0]]; 11 | } 12 | } 13 | toObject() { 14 | return this.message; 15 | } 16 | } 17 | exports.Message = Message; 18 | (function (Message) { 19 | let Type; 20 | (function (Type) { 21 | Type[Type["SendCommandMessage"] = 1] = "SendCommandMessage"; 22 | Type[Type["CommandResultMessage"] = 2] = "CommandResultMessage"; 23 | Type[Type["GetStateMessage"] = 3] = "GetStateMessage"; 24 | Type[Type["SetStateMessage"] = 4] = "SetStateMessage"; 25 | Type[Type["SetArtworkMessage"] = 5] = "SetArtworkMessage"; 26 | Type[Type["RegisterHidDeviceMessage"] = 6] = "RegisterHidDeviceMessage"; 27 | Type[Type["RegisterHidDeviceResultMessage"] = 7] = "RegisterHidDeviceResultMessage"; 28 | Type[Type["SendHidEventMessage"] = 8] = "SendHidEventMessage"; 29 | Type[Type["SendHidReportMessage"] = 9] = "SendHidReportMessage"; 30 | Type[Type["SendVirtualTouchEventMessage"] = 10] = "SendVirtualTouchEventMessage"; 31 | Type[Type["NotificationMessage"] = 11] = "NotificationMessage"; 32 | Type[Type["ContentItemsChangedNotificationMessage"] = 12] = "ContentItemsChangedNotificationMessage"; 33 | Type[Type["DeviceInfoMessage"] = 15] = "DeviceInfoMessage"; 34 | Type[Type["ClientUpdatesConfigMessage"] = 16] = "ClientUpdatesConfigMessage"; 35 | Type[Type["VolumeControlAvailabilityMessage"] = 17] = "VolumeControlAvailabilityMessage"; 36 | Type[Type["GameControllerMessage"] = 18] = "GameControllerMessage"; 37 | Type[Type["RegisterGameControllerMessage"] = 19] = "RegisterGameControllerMessage"; 38 | Type[Type["RegisterGameControllerResponseMessage"] = 20] = "RegisterGameControllerResponseMessage"; 39 | Type[Type["UnregisterGameControllerMessage"] = 21] = "UnregisterGameControllerMessage"; 40 | Type[Type["RegisterForGameControllerEventsMessage"] = 22] = "RegisterForGameControllerEventsMessage"; 41 | Type[Type["KeyboardMessage"] = 23] = "KeyboardMessage"; 42 | Type[Type["GetKeyboardSessionMessage"] = 24] = "GetKeyboardSessionMessage"; 43 | Type[Type["TextInputMessage"] = 25] = "TextInputMessage"; 44 | Type[Type["GetVoiceInputDevicesMessage"] = 26] = "GetVoiceInputDevicesMessage"; 45 | Type[Type["GetVoiceInputDevicesResponseMessage"] = 27] = "GetVoiceInputDevicesResponseMessage"; 46 | Type[Type["RegisterVoiceInputDeviceMessage"] = 28] = "RegisterVoiceInputDeviceMessage"; 47 | Type[Type["RegisterVoiceInputDeviceResponseMessage"] = 29] = "RegisterVoiceInputDeviceResponseMessage"; 48 | Type[Type["SetRecordingStateMessage"] = 30] = "SetRecordingStateMessage"; 49 | Type[Type["SendVoiceInputMessage"] = 31] = "SendVoiceInputMessage"; 50 | Type[Type["PlaybackQueueRequestMessage"] = 32] = "PlaybackQueueRequestMessage"; 51 | Type[Type["TransactionMessage"] = 33] = "TransactionMessage"; 52 | Type[Type["CryptoPairingMessage"] = 34] = "CryptoPairingMessage"; 53 | Type[Type["GameControllerPropertiesMessage"] = 35] = "GameControllerPropertiesMessage"; 54 | Type[Type["SetReadyStateMessage"] = 36] = "SetReadyStateMessage"; 55 | Type[Type["DeviceInfoUpdate"] = 37] = "DeviceInfoUpdate"; 56 | Type[Type["SetDisconnectingStateMessage"] = 38] = "SetDisconnectingStateMessage"; 57 | Type[Type["SendButtonEvent"] = 39] = "SendButtonEvent"; 58 | Type[Type["SetHiliteModeMessage"] = 40] = "SetHiliteModeMessage"; 59 | Type[Type["WakeDeviceMessage"] = 41] = "WakeDeviceMessage"; 60 | Type[Type["GenericMessage"] = 42] = "GenericMessage"; 61 | Type[Type["SendPackedVirtualTouchEvent"] = 43] = "SendPackedVirtualTouchEvent"; 62 | Type[Type["SendLyricsEvent"] = 44] = "SendLyricsEvent"; 63 | Type[Type["PlaybackQueueCapabilitiesRequest"] = 45] = "PlaybackQueueCapabilitiesRequest"; 64 | Type[Type["ModifyOutputContextRequest"] = 46] = "ModifyOutputContextRequest"; 65 | })(Type = Message.Type || (Message.Type = {})); 66 | })(Message = exports.Message || (exports.Message = {})); 67 | -------------------------------------------------------------------------------- /dist/lib/now-playing-info.d.ts: -------------------------------------------------------------------------------- 1 | export declare class NowPlayingInfo { 2 | message: any; 3 | duration: number; 4 | elapsedTime: number; 5 | title: string; 6 | artist: string; 7 | album: string; 8 | appDisplayName: string; 9 | appBundleIdentifier: string; 10 | playbackState: NowPlayingInfo.State; 11 | timestamp: number; 12 | constructor(message: any); 13 | percentCompleted(): string; 14 | toString(): string; 15 | } 16 | export declare module NowPlayingInfo { 17 | enum State { 18 | Playing = "playing", 19 | Paused = "paused" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dist/lib/now-playing-info.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class NowPlayingInfo { 4 | constructor(message) { 5 | this.message = message; 6 | let nowPlayingInfo = message.nowPlayingInfo; 7 | if (nowPlayingInfo) { 8 | this.duration = nowPlayingInfo.duration; 9 | this.elapsedTime = nowPlayingInfo.elapsedTime; 10 | this.title = nowPlayingInfo.title; 11 | this.artist = nowPlayingInfo.artist; 12 | this.album = nowPlayingInfo.album; 13 | this.timestamp = nowPlayingInfo.timestamp; 14 | } 15 | this.appDisplayName = message.displayName; 16 | this.appBundleIdentifier = message.displayID; 17 | if (message.playbackState == 2) { 18 | this.playbackState = NowPlayingInfo.State.Paused; 19 | } 20 | else if (message.playbackState == 1) { 21 | this.playbackState = NowPlayingInfo.State.Playing; 22 | } 23 | } 24 | percentCompleted() { 25 | if (!this.elapsedTime || !this.duration) { 26 | return "0.00"; 27 | } 28 | return ((this.elapsedTime / this.duration) * 100).toPrecision(3); 29 | } 30 | toString() { 31 | if (this.artist) { 32 | let album = this.album == null ? '' : " -- " + this.album + " "; 33 | return this.title + " by " + this.artist + album + " (" + this.percentCompleted() + "%) | " 34 | + this.appDisplayName + " (" + this.appBundleIdentifier + ") | " 35 | + this.playbackState; 36 | } 37 | else if (this.title) { 38 | return this.title + " (" + this.percentCompleted() + "%) | " 39 | + this.appDisplayName + " (" + this.appBundleIdentifier + ") | " 40 | + this.playbackState; 41 | } 42 | else { 43 | return this.appDisplayName + " (" + this.appBundleIdentifier + ") | " 44 | + this.playbackState; 45 | } 46 | } 47 | } 48 | exports.NowPlayingInfo = NowPlayingInfo; 49 | (function (NowPlayingInfo) { 50 | let State; 51 | (function (State) { 52 | State["Playing"] = "playing"; 53 | State["Paused"] = "paused"; 54 | })(State = NowPlayingInfo.State || (NowPlayingInfo.State = {})); 55 | })(NowPlayingInfo = exports.NowPlayingInfo || (exports.NowPlayingInfo = {})); 56 | -------------------------------------------------------------------------------- /dist/lib/pairing.d.ts: -------------------------------------------------------------------------------- 1 | import { AppleTV } from './appletv'; 2 | export declare class Pairing { 3 | device: AppleTV; 4 | private srp; 5 | private key; 6 | private publicKey; 7 | private proof; 8 | private deviceSalt; 9 | private devicePublicKey; 10 | private deviceProof; 11 | constructor(device: AppleTV); 12 | /** 13 | * Initiates the pairing process 14 | * @returns A promise that resolves to a callback which takes in the pairing pin from the Apple TV. 15 | */ 16 | initiatePair(): Promise<(pin: string) => Promise>; 17 | private completePairing; 18 | private sendThirdSequence; 19 | private sendFifthSequence; 20 | } 21 | -------------------------------------------------------------------------------- /dist/lib/pairing.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const srp = require("fast-srp-hap"); 13 | const crypto = require("crypto"); 14 | const ed25519 = require("ed25519"); 15 | const credentials_1 = require("./credentials"); 16 | const tlv_1 = require("./util/tlv"); 17 | const encryption_1 = require("./util/encryption"); 18 | class Pairing { 19 | constructor(device) { 20 | this.device = device; 21 | this.key = crypto.randomBytes(32); 22 | } 23 | /** 24 | * Initiates the pairing process 25 | * @returns A promise that resolves to a callback which takes in the pairing pin from the Apple TV. 26 | */ 27 | initiatePair() { 28 | return __awaiter(this, void 0, void 0, function* () { 29 | let that = this; 30 | let tlvData = tlv_1.default.encode(tlv_1.default.Tag.PairingMethod, 0x00, tlv_1.default.Tag.Sequence, 0x01); 31 | let requestMessage = { 32 | status: 0, 33 | isUsingSystemPairing: true, 34 | isRetrying: true, 35 | state: 2, 36 | pairingData: tlvData 37 | }; 38 | yield this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', requestMessage, false); 39 | let message = yield this.device.waitForSequence(0x02); 40 | let pairingData = message.payload.pairingData; 41 | let decodedData = tlv_1.default.decode(pairingData); 42 | if (decodedData[tlv_1.default.Tag.BackOff]) { 43 | let backOff = decodedData[tlv_1.default.Tag.BackOff]; 44 | let seconds = backOff.readIntBE(0, backOff.byteLength); 45 | if (seconds > 0) { 46 | throw new Error("You've attempt to pair too recently. Try again in " + seconds + " seconds."); 47 | } 48 | } 49 | if (decodedData[tlv_1.default.Tag.ErrorCode]) { 50 | let buffer = decodedData[tlv_1.default.Tag.ErrorCode]; 51 | throw new Error(this.device.name + " responded with error code " + buffer.readIntBE(0, buffer.byteLength) + ". Try rebooting your Apple TV."); 52 | } 53 | this.deviceSalt = decodedData[tlv_1.default.Tag.Salt]; 54 | this.devicePublicKey = decodedData[tlv_1.default.Tag.PublicKey]; 55 | if (this.deviceSalt.byteLength != 16) { 56 | throw new Error(`salt must be 16 bytes (but was ${this.deviceSalt.byteLength})`); 57 | } 58 | if (this.devicePublicKey.byteLength !== 384) { 59 | throw new Error(`serverPublicKey must be 384 bytes (but was ${this.devicePublicKey.byteLength})`); 60 | } 61 | return (pin) => { 62 | return that.completePairing(pin); 63 | }; 64 | }); 65 | } 66 | completePairing(pin) { 67 | return __awaiter(this, void 0, void 0, function* () { 68 | yield this.sendThirdSequence(pin); 69 | let message = yield this.device.waitForSequence(0x04); 70 | let pairingData = message.payload.pairingData; 71 | this.deviceProof = tlv_1.default.decode(pairingData)[tlv_1.default.Tag.Proof]; 72 | // console.log("DEBUG: Device Proof=" + this.deviceProof.toString('hex')); 73 | this.srp.checkM2(this.deviceProof); 74 | let seed = crypto.randomBytes(32); 75 | let keyPair = ed25519.MakeKeypair(seed); 76 | let privateKey = keyPair.privateKey; 77 | let publicKey = keyPair.publicKey; 78 | let sharedSecret = this.srp.computeK(); 79 | let deviceHash = encryption_1.default.HKDF("sha512", Buffer.from("Pair-Setup-Controller-Sign-Salt"), sharedSecret, Buffer.from("Pair-Setup-Controller-Sign-Info"), 32); 80 | let deviceInfo = Buffer.concat([deviceHash, Buffer.from(this.device.pairingId), publicKey]); 81 | let deviceSignature = ed25519.Sign(deviceInfo, privateKey); 82 | let encryptionKey = encryption_1.default.HKDF("sha512", Buffer.from("Pair-Setup-Encrypt-Salt"), sharedSecret, Buffer.from("Pair-Setup-Encrypt-Info"), 32); 83 | yield this.sendFifthSequence(publicKey, deviceSignature, encryptionKey); 84 | let newMessage = yield this.device.waitForSequence(0x06); 85 | let encryptedData = tlv_1.default.decode(newMessage.payload.pairingData)[tlv_1.default.Tag.EncryptedData]; 86 | let cipherText = encryptedData.slice(0, -16); 87 | let hmac = encryptedData.slice(-16); 88 | let decrpytedData = encryption_1.default.verifyAndDecrypt(cipherText, hmac, null, Buffer.from('PS-Msg06'), encryptionKey); 89 | let tlvData = tlv_1.default.decode(decrpytedData); 90 | this.device.credentials = new credentials_1.Credentials(this.device.uid, tlvData[tlv_1.default.Tag.Username], this.device.pairingId, tlvData[tlv_1.default.Tag.PublicKey], seed); 91 | return this.device; 92 | }); 93 | } 94 | sendThirdSequence(pin) { 95 | return __awaiter(this, void 0, void 0, function* () { 96 | this.srp = srp.Client(srp.params['3072'], this.deviceSalt, Buffer.from('Pair-Setup'), Buffer.from(pin), this.key); 97 | this.srp.setB(this.devicePublicKey); 98 | this.publicKey = this.srp.computeA(); 99 | this.proof = this.srp.computeM1(); 100 | // console.log("DEBUG: Client Public Key=" + this.publicKey.toString('hex') + "\nProof=" + this.proof.toString('hex')); 101 | let tlvData = tlv_1.default.encode(tlv_1.default.Tag.Sequence, 0x03, tlv_1.default.Tag.PublicKey, this.publicKey, tlv_1.default.Tag.Proof, this.proof); 102 | let message = { 103 | status: 0, 104 | pairingData: tlvData 105 | }; 106 | return yield this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', message, false); 107 | }); 108 | } 109 | sendFifthSequence(publicKey, signature, encryptionKey) { 110 | return __awaiter(this, void 0, void 0, function* () { 111 | let tlvData = tlv_1.default.encode(tlv_1.default.Tag.Username, Buffer.from(this.device.pairingId), tlv_1.default.Tag.PublicKey, publicKey, tlv_1.default.Tag.Signature, signature); 112 | let encryptedTLV = Buffer.concat(encryption_1.default.encryptAndSeal(tlvData, null, Buffer.from('PS-Msg05'), encryptionKey)); 113 | // console.log("DEBUG: Encrypted Data=" + encryptedTLV.toString('hex')); 114 | let outerTLV = tlv_1.default.encode(tlv_1.default.Tag.Sequence, 0x05, tlv_1.default.Tag.EncryptedData, encryptedTLV); 115 | let nextMessage = { 116 | status: 0, 117 | pairingData: outerTLV 118 | }; 119 | return yield this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', nextMessage, false); 120 | }); 121 | } 122 | } 123 | exports.Pairing = Pairing; 124 | -------------------------------------------------------------------------------- /dist/lib/protos/AudioBuffer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message AudioPacket { 4 | required int32 startOffset = 1; 5 | required int32 variableFramesInPacket = 2; 6 | required int32 dataByteSize = 3; 7 | } 8 | 9 | message AudioBuffer { 10 | required AudioFormatSettings formatSettings = 1; 11 | optional int32 packetCount = 2; 12 | optional int32 maximumPacketSize = 3; 13 | optional int32 packetCapacity = 4; 14 | required bytes contents = 5; 15 | repeated AudioPacket packetDescriptions = 6; 16 | } 17 | -------------------------------------------------------------------------------- /dist/lib/protos/AudioFormatSettings.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message AudioFormatSettings { 4 | optional bytes formatSettingsPlistData = 1; 5 | } -------------------------------------------------------------------------------- /dist/lib/protos/ClientUpdatesConfigMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional ClientUpdatesConfigMessage clientUpdatesConfigMessage = 21; 7 | } 8 | 9 | message ClientUpdatesConfigMessage { 10 | optional bool artworkUpdates = 1; 11 | optional bool nowPlayingUpdates = 2; 12 | optional bool volumeUpdates = 3; 13 | optional bool keyboardUpdates = 4; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /dist/lib/protos/CommandInfo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | enum Command { 4 | Unknown=0; 5 | Play=1; 6 | Pause=2; 7 | TogglePlayPause=3; 8 | Stop=4; 9 | NextTrack=5; 10 | PreviousTrack=6; 11 | AdvanceShuffleMode=7; 12 | AdvanceRepeatMode=8; 13 | BeginFastForward=9; 14 | EndFastForward=10; 15 | BeginRewind=11; 16 | EndRewind=12; 17 | Rewind15Seconds=13; 18 | FastForward15Seconds=14; 19 | Rewind30Seconds=15; 20 | FastForward30Seconds=16; 21 | 22 | SkipForward=18; 23 | SkipBackward=19; 24 | ChangePlaybackRate=20; 25 | RateTrack=21; 26 | LikeTrack=22; 27 | DislikeTrack=23; 28 | BookmarkTrack=24; 29 | 30 | SeekToPlaybackPosition=45; 31 | ChangeRepeatMode=46; 32 | ChangeShuffleMode=47; 33 | 34 | EnableLanguageOption=53; 35 | DisableLanguageOption=54; 36 | 37 | NextChapter=25; 38 | PreviousChapter=26; 39 | NextAlbum=27; 40 | PreviousAlbum=28; 41 | NextPlaylist=29; 42 | PreviousPlaylist=30; 43 | BanTrack=31; 44 | AddTrackToWishList=32; 45 | RemoveTrackFromWishList=33; 46 | NextInContext=34; 47 | PreviousInContext=35; 48 | 49 | ResetPlaybackTimeout=41; 50 | SetPlaybackQueue=48; 51 | AddNowPlayingItemToLibrary=49; 52 | CreateRadioStation=50; 53 | AddItemToLibrary=51; 54 | InsertIntoPlaybackQueue=52; 55 | 56 | ReorderPlaybackQueue=55; 57 | RemoveFromPlaybackQueue=56; 58 | PlayItemInPlaybackQueue=57; 59 | } 60 | 61 | message CommandInfo { 62 | optional Command command = 1; 63 | optional bool enabled = 2; 64 | optional bool active = 3; 65 | repeated double preferredIntervals = 4; 66 | optional string localizedTitle = 5; 67 | optional float minimumRating = 6; 68 | optional float maximumRating = 7; 69 | repeated float supportedRates = 8; 70 | optional string localizedShortTitle = 9; 71 | optional int32 repeatMode = 10; 72 | optional int32 shuffleMode = 11; 73 | optional int32 presentationStyle = 12; 74 | optional int32 skipInterval = 13; 75 | optional int32 numAvailableSkips = 14; 76 | optional int32 skipFrequency = 15; 77 | optional int32 canScrub = 16; 78 | repeated int32 supportedPlaybackQueueTypes = 17; 79 | repeated string supportedCustomQueueIdentifiers = 18; 80 | repeated int32 supportedInsertionPositions = 19; 81 | optional bool supportsSharedQueue = 20; 82 | } -------------------------------------------------------------------------------- /dist/lib/protos/CommandOptions.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message CommandOptions { 4 | optional string sourceId = 2; 5 | optional string mediaType = 3; 6 | optional bool externalPlayerCommand = 4; 7 | optional float skipInterval = 5; 8 | optional float playbackRate = 6; 9 | optional float rating = 7; 10 | optional bool negative = 8; 11 | optional double playbackPosition = 9; 12 | optional int32 repeatMode = 10; 13 | optional int32 shuffleMode = 11; 14 | optional uint64 trackID = 12; 15 | optional int64 radioStationID = 13; 16 | optional string radioStationHash = 14; 17 | optional bytes systemAppPlaybackQueueData = 15; 18 | optional string destinationAppDisplayID = 16; 19 | optional uint32 sendOptions = 17; 20 | optional bool requestDefermentToPlaybackQueuePosition = 18; 21 | optional string contextID = 19; 22 | optional bool shouldOverrideManuallyCuratedQueue = 20; 23 | optional string stationURL = 21; 24 | optional bool shouldBeginRadioPlayback = 22; 25 | optional int32 playbackQueueInsertionPosition = 23; 26 | optional string contentItemID = 24; 27 | optional int32 playbackQueueOffset = 25; 28 | optional int32 playbackQueueDestinationOffset = 26; 29 | optional bytes languageOption = 27; 30 | optional bytes playbackQueueContext = 28; 31 | optional string insertAfterContentItemID = 29; 32 | optional string nowPlayingContentItemID = 30; 33 | optional int32 replaceIntent = 31; 34 | } -------------------------------------------------------------------------------- /dist/lib/protos/CommandResultMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional CommandResultMessage commandResultMessage = 7; 7 | } 8 | 9 | message CommandResultMessage { 10 | optional uint64 value = 1; 11 | } -------------------------------------------------------------------------------- /dist/lib/protos/ContentItem.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ContentItemMetadata.proto"; 4 | import "LanguageOption.proto"; 5 | 6 | message ContentItem { 7 | optional string identifier = 1; 8 | optional ContentItemMetadata metadata = 2; 9 | optional bytes artworkData = 3; 10 | optional string info = 4; 11 | repeated LanguageOption availableLanguageOptions = 5; 12 | repeated LanguageOption currentLanguageOptions = 6; 13 | // optional Lyrics lyrics = 7; 14 | // repeated Sections sections = 8; 15 | optional string parentIdentifier = 9; 16 | optional string ancestorIdentifier = 10; 17 | optional string queueIdentifier = 11; 18 | optional string requestIdentifier = 12; 19 | optional int32 artworkDataWidth = 13; 20 | optional int32 artworkDataHeight = 14; 21 | } -------------------------------------------------------------------------------- /dist/lib/protos/ContentItemMetadata.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ContentItemMetadata { 4 | enum MediaType { 5 | UnknownMediaType = 0; 6 | Audio = 1; 7 | Video = 2; 8 | } 9 | enum MediaSubType { 10 | UnknownMediaSubType = 0; 11 | Music = 1; 12 | Podcast = 4; 13 | AudioBook = 5; 14 | ITunesU = 6; 15 | } 16 | optional string title = 1; 17 | optional string subtitle = 2; 18 | optional bool isContainer = 3; 19 | optional bool isPlayable = 4; 20 | optional float playbackProgress = 5; 21 | optional string albumName = 6; 22 | optional string trackArtistName = 7; 23 | optional string albumArtistName = 8; 24 | optional string directorName = 9; 25 | optional int32 seasonNumber = 10; 26 | optional int32 episodeNumber = 11; 27 | optional double releaseDate = 12; 28 | optional int32 playCount = 13; 29 | optional double duration = 14; 30 | optional string localizedContentRating = 15; 31 | optional bool isExplicitItem = 16; 32 | optional int32 playlistType = 17; 33 | optional int32 radioStationType = 18; 34 | optional bool artworkAvailable = 19; 35 | 36 | optional bool infoAvailable = 21; 37 | optional bool languageOptionsAvailable = 22; 38 | optional int32 numberOfSections = 23; 39 | optional bool lyricsAvailable = 24; 40 | optional int32 editingStyleFlags = 25; 41 | optional bool isStreamingContent = 26; 42 | optional bool isCurrentlyPlaying = 27; 43 | optional string collectionIdentifier = 28; 44 | optional string profileIdentifier = 29; 45 | optional double startTime = 30; 46 | optional string artworkMIMEType = 31; 47 | optional string assetURLString = 32; 48 | optional string composer = 33; 49 | optional int32 discNumber = 34; 50 | optional double elapsedTime = 35; 51 | optional string genre = 36; 52 | optional bool isAlwaysLive = 37; 53 | 54 | optional float playbackRate = 39; 55 | optional int32 chapterCount = 40; 56 | optional int32 totalDiscCount = 41; 57 | optional int32 totalTrackCount = 42; 58 | optional int32 trackNumber = 43; 59 | optional string contentIdentifier = 44; 60 | 61 | optional bool isSharable = 46; 62 | 63 | optional bool isLiked = 48; 64 | optional bool isInWishList = 49; 65 | optional int64 radioStationIdentifier = 50; 66 | 67 | optional string radioStationName = 52; 68 | optional string radioStationString = 53; 69 | optional int64 iTunesStoreIdentifier = 54; 70 | optional int64 iTunesStoreSubscriptionIdentifier = 55; 71 | optional int64 iTunesStoreArtistIdentifier = 56; 72 | optional int64 iTunesStoreAlbumIdentifier = 57; 73 | optional bytes purchaseInfoData = 58; 74 | optional float defaultPlaybackRate = 59; 75 | optional int32 downloadState = 60; 76 | optional float downloadProgress = 61; 77 | optional bytes appMetricsData = 62; 78 | optional string seriesName = 63; 79 | optional MediaType mediaType = 64; 80 | optional MediaSubType mediaSubType = 65; 81 | 82 | optional bytes nowPlayingInfoData = 67; 83 | optional bytes userInfoData = 68; 84 | optional bool isSteerable = 69; 85 | optional string artworkURL = 70; 86 | optional string lyricsURL = 71; 87 | optional bytes deviceSpecificUserInfoData = 72; 88 | optional bytes collectionInfoData = 73; 89 | optional double elapsedTimeTimestamp = 74; 90 | optional double inferredTimestamp = 75; 91 | optional string serviceIdentifier = 76; 92 | optional int32 artworkDataWidth = 77; 93 | optional int32 artworkDataHeight = 78; 94 | optional bytes currentPlaybackDateData = 79; 95 | optional string artworkIdentifier = 80; 96 | optional bool isLoading = 81; 97 | optional bytes artworkURLTemplatesData = 82; 98 | optional int64 legacyUniqueIdentifier = 83; 99 | optional int32 episodeType = 84; 100 | optional string artworkFileURL = 85; 101 | optional string brandIdentifier = 86; 102 | optional string localizedDurationString = 87; 103 | } -------------------------------------------------------------------------------- /dist/lib/protos/CryptoPairingMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional CryptoPairingMessage cryptoPairingMessage = 39; 7 | } 8 | 9 | message CryptoPairingMessage { 10 | optional bytes pairingData = 1; // Example: <00010006 0101> 11 | required int32 status = 2; // Example: 0 12 | optional bool isRetrying = 3; 13 | optional bool isUsingSystemPairing = 4; 14 | optional int32 state = 5; 15 | } 16 | -------------------------------------------------------------------------------- /dist/lib/protos/DeviceInfoMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional DeviceInfoMessage deviceInfoMessage = 20; 7 | } 8 | 9 | message DeviceInfoMessage { 10 | required string uniqueIdentifier = 1; // Example: B8D8678C-9DA9-4D29-9338-5D6B827B8063 11 | required string name = 2; // Example: Jean's iPhone 12 | optional string localizedModelName = 3; // Example: iPhone 13 | required string systemBuildVersion = 4; // Example: 13F69 14 | required string applicationBundleIdentifier = 5; // Example: com.example.myremote 15 | optional string applicationBundleVersion = 6; // Example: 107 16 | required int32 protocolVersion = 7; // Example: 1 17 | optional int32 lastSupportedMessageType = 8; 18 | optional bool allowsPairing = 9; 19 | optional bool supportsSystemPairing = 10; 20 | } 21 | -------------------------------------------------------------------------------- /dist/lib/protos/DeviceInfoUpdate.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional DeviceInfoUpdate deviceInfoUpdate = 20; 7 | } 8 | 9 | message DeviceInfoUpdate { 10 | required string uniqueIdentifier = 1; // Example: B8D8678C-9DA9-4D29-9338-5D6B827B8063 11 | required string name = 2; // Example: Jean's iPhone 12 | optional string localizedModelName = 3; // Example: iPhone 13 | required string systemBuildVersion = 4; // Example: 13F69 14 | required string applicationBundleIdentifier = 5; // Example: com.example.myremote 15 | optional string applicationBundleVersion = 6; // Example: 107 16 | required int32 protocolVersion = 7; // Example: 1 17 | optional int32 lastSupportedMessageType = 8; 18 | optional bool allowsPairing = 9; 19 | optional bool supportsSystemPairing = 10; 20 | } 21 | -------------------------------------------------------------------------------- /dist/lib/protos/GetKeyboardSessionMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional string getKeyboardSessionMessage = 29; 7 | } 8 | 9 | message GetKeyboardSessionMessage { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /dist/lib/protos/GetStateMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional string getStateMessage = 8; 7 | } 8 | -------------------------------------------------------------------------------- /dist/lib/protos/KeyboardMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "TextEditingAttributes.proto"; 5 | 6 | extend ProtocolMessage { 7 | optional KeyboardMessage keyboardMessage = 28; 8 | } 9 | 10 | message KeyboardMessage { 11 | optional int32 state = 1; 12 | optional TextEditingAttributes attributes = 3; 13 | } 14 | -------------------------------------------------------------------------------- /dist/lib/protos/LanguageOption.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message LanguageOption { 4 | optional int32 type = 1; 5 | optional string languageTag = 2; 6 | repeated string characteristics = 3; 7 | optional string displayName = 4; 8 | optional string identifier = 5; 9 | } -------------------------------------------------------------------------------- /dist/lib/protos/NotificationMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "PlayerPath.proto"; 5 | 6 | extend ProtocolMessage { 7 | optional NotificationMessage notificationMessage = 16; 8 | } 9 | 10 | message NotificationMessage { 11 | repeated string notification = 1; 12 | repeated bytes userInfos = 2; 13 | repeated PlayerPath playerPaths = 3; 14 | } -------------------------------------------------------------------------------- /dist/lib/protos/NowPlayingClient.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message NowPlayingClient { 4 | optional int32 processIdentifier = 1; 5 | optional string bundleIdentifier = 2; 6 | optional string parentApplicationBundleIdentifier = 3; 7 | optional int32 processUserIdentifier = 4; 8 | optional int32 nowPlayingVisibility = 5; 9 | // optional TintColor tintColor = 6; 10 | optional string displayName = 7; 11 | } -------------------------------------------------------------------------------- /dist/lib/protos/NowPlayingInfo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message NowPlayingInfo { 4 | enum RepeatMode { 5 | Unknown = 0; 6 | One = 1; 7 | All = 2; 8 | } 9 | 10 | enum ShuffleMode { 11 | Unkown = 0; 12 | Off = 1; 13 | Albums = 2; 14 | Songs = 3; 15 | } 16 | 17 | optional string album = 1; 18 | optional string artist = 2; 19 | optional double duration = 3; 20 | optional double elapsedTime = 4; 21 | optional float playbackRate = 5; 22 | optional RepeatMode repeatMode = 6; 23 | optional ShuffleMode shuffleMode = 7; 24 | optional double timestamp = 8; 25 | optional string title = 9; 26 | optional uint64 uniqueIdentifier = 10; 27 | optional bool isExplicitTrack = 11; 28 | optional bool isMusicApp = 12; 29 | optional int64 radioStationIdentifier = 13; 30 | optional string radioStationHash = 14; 31 | optional string radioStationName = 15; 32 | optional bytes artworkDataDigest = 16; 33 | optional bool isAlwaysLive = 17; 34 | optional bool isAdvertisement = 18; 35 | } -------------------------------------------------------------------------------- /dist/lib/protos/NowPlayingPlayer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message NowPlayingPlayer { 4 | optional string identifier = 1; 5 | optional string displayName = 2; 6 | optional bool isDefaultPlayer = 3; 7 | } -------------------------------------------------------------------------------- /dist/lib/protos/Origin.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "DeviceInfoMessage.proto"; 4 | 5 | message Origin { 6 | optional int32 type = 1; 7 | optional string displayName = 2; 8 | optional int32 identifier = 3; 9 | optional DeviceInfoMessage deviceInfo = 4; 10 | } -------------------------------------------------------------------------------- /dist/lib/protos/PlaybackQueue.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ContentItem.proto"; 4 | import "PlaybackQueueContext.proto"; 5 | import "PlayerPath.proto"; 6 | 7 | message PlaybackQueue { 8 | optional int32 location = 1; 9 | repeated ContentItem contentItems = 2; 10 | optional PlaybackQueueContext context = 3; 11 | optional string requestId = 4; 12 | optional PlayerPath resolvedPlayerPath = 5; 13 | optional bool sendingPlaybackQueueTransaction = 6; 14 | optional string queueIdentifier = 7; 15 | } -------------------------------------------------------------------------------- /dist/lib/protos/PlaybackQueueCapabilities.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message PlaybackQueueCapabilities { 4 | optional bool requestByRange = 1; 5 | optional bool requestByIdentifiers = 2; 6 | optional bool requestByRequest = 3; 7 | } -------------------------------------------------------------------------------- /dist/lib/protos/PlaybackQueueContext.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message PlaybackQueueContext { 4 | optional string revision = 1; 5 | } -------------------------------------------------------------------------------- /dist/lib/protos/PlaybackQueueRequestMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "PlaybackQueueContext.proto"; 5 | import "PlayerPath.proto"; 6 | 7 | extend ProtocolMessage { 8 | optional PlaybackQueueRequestMessage playbackQueueRequestMessage = 37; 9 | } 10 | 11 | message PlaybackQueueRequestMessage { 12 | optional int32 location = 1; 13 | optional int32 length = 2; 14 | optional bool includeMetadata = 3; 15 | optional double artworkWidth = 4; 16 | optional double artworkHeight = 5; 17 | optional bool includeLyrics = 6; 18 | optional bool includeSections = 7; 19 | optional bool includeInfo = 8; 20 | optional bool includeLanguageOptions = 9; 21 | optional PlaybackQueueContext context = 10; 22 | optional string requestID = 11; 23 | repeated string contentItemIdentifiers = 12; 24 | optional bool returnContentItemAssetsInUserCompletion = 13; 25 | optional PlayerPath playerPath = 14; 26 | optional int32 cachingPolicy = 15; 27 | optional string label = 16; 28 | optional bool isLegacyNowPlayingInfoRequest = 17; 29 | } 30 | -------------------------------------------------------------------------------- /dist/lib/protos/PlayerPath.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "Origin.proto"; 4 | import "NowPlayingClient.proto"; 5 | import "NowPlayingPlayer.proto"; 6 | 7 | message PlayerPath { 8 | optional Origin origin = 1; 9 | optional NowPlayingClient client = 2; 10 | optional NowPlayingPlayer player = 3; 11 | } -------------------------------------------------------------------------------- /dist/lib/protos/ProtocolMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ProtocolMessage { 4 | extensions 6 to max; 5 | 6 | enum Type { 7 | SEND_COMMAND_MESSAGE = 1; 8 | COMMAND_RESULT_MESSAGE = 2; 9 | GET_STATE_MESSAGE = 3; 10 | SET_STATE_MESSAGE = 4; 11 | SET_ARTWORK_MESSAGE = 5; 12 | REGISTER_HID_DEVICE_MESSAGE = 6; 13 | REGISTER_HID_DEVICE_RESULT_MESSAGE = 7; 14 | SEND_HID_EVENT_MESSAGE = 8; 15 | SEND_HID_REPORT_MESSAGE = 9; 16 | SEND_VIRTUAL_TOUCH_EVENT_MESSAGE = 10; 17 | NOTIFICATION_MESSAGE = 11; 18 | CONTENT_ITEMS_CHANGED_NOTIFICATION_MESSAGE = 12; 19 | DEVICE_INFO_MESSAGE = 15; 20 | CLIENT_UPDATES_CONFIG_MESSAGE = 16; 21 | VOLUME_CONTROL_AVAILABILITY_MESSAGE = 17; 22 | GAME_CONTROLLER_MESSAGE = 18; 23 | REGISTER_GAME_CONTROLLER_MESSAGE = 19; 24 | REGISTER_GAME_CONTROLLER_RESPONSE_MESSAGE = 20; 25 | UNREGISTER_GAME_CONTROLLER_MESSAGE = 21; 26 | REGISTER_FOR_GAME_CONTROLLER_EVENTS_MESSAGE = 22; 27 | KEYBOARD_MESSAGE = 23; 28 | GET_KEYBOARD_SESSION_MESSAGE = 24; 29 | TEXT_INPUT_MESSAGE = 25; 30 | GET_VOICE_INPUT_DEVICES_MESSAGE = 26; 31 | GET_VOICE_INPUT_DEVICES_RESPONSE_MESSAGE = 27; 32 | REGISTER_VOICE_INPUT_DEVICE_MESSAGE = 28; 33 | REGISTER_VOICE_INPUT_DEVICE_RESPONSE_MESSAGE = 29; 34 | SET_RECORDING_STATE_MESSAGE = 30; 35 | SEND_VOICE_INPUT_MESSAGE = 31; 36 | PLAYBACK_QUEUE_REQUEST_MESSAGE = 32; 37 | TRANSACTION_MESSAGE = 33; 38 | CRYPTO_PAIRING_MESSAGE = 34; 39 | GAME_CONTROLLER_PROPERTIES_MESSAGE = 35; 40 | SET_READY_STATE_MESSAGE = 36; 41 | DEVICE_INFO_UPDATE = 37; 42 | SET_DISCONNECTING_STATE_MESSAGE = 38; 43 | SEND_BUTTON_EVENT = 39; 44 | SET_HILITE_MODE_MESSAGE = 40; 45 | WAKE_DEVICE_MESSAGE = 41; 46 | GENERIC_MESSAGE = 42; 47 | SEND_PACKED_VIRTUAL_TOUCH_EVENT = 43; 48 | SEND_LYRICS_EVENT = 44; 49 | PLAYBACK_QUEUE_CAPABILITIES_REQUEST = 45; 50 | MODIFY_OUTPUT_CONTEXT_REQUEST = 46; 51 | } 52 | 53 | required Type type = 1; // Identifies which underlying message is filled in. 54 | optional string identifier = 2; 55 | optional int32 priority = 4; 56 | 57 | // One of the following will be filled in. 58 | // optional SendCommandMessage sendCommandMessage = 6; 59 | // optional SendCommandResultMessage sendCommandResultMessage = 7; 60 | // optional SetStateMessage setStateMessage = 9; 61 | // optional SetArtworkMessage setArtworkMessage = 10; 62 | // optional RegisterHIDDeviceMessage registerHIDDeviceMessage = 11; 63 | // optional RegisterHIDDeviceResultMessage registerHIDDeviceResultMessage = 12; 64 | // optional SendHIDEventMessage sendHIDEventMessage = 13; 65 | // optional SendVirtualTouchEventMessage sendVirtualTouchEventMessage = 15; 66 | // optional NotificationMessage notificationMessage = 16; 67 | // optional ContentItemsChangedNotificationMessage contentItemsChangedNotificationMessage = 17; 68 | // optional DeviceInfoMessage deviceInfoMessage = 20; 69 | // optional ClientUpdatesConfigMessage clientUpdatesConfigMessage = 21; 70 | // optional VolumeControlAvailabilityMessage volumeControlAvailabilityMessage = 22; 71 | // optional GameControllerMessage gameControllerMessage = 23; 72 | // optional RegisterGameControllerMessage registerGameControllerMessage = 24; 73 | // optional RegisterGameControllerResponseMessage registerGameControllerResponseMessage = 25; 74 | // optional UnregisterGameControllerMessage unregisterGameControllerMessage = 26; 75 | // optional RegisterForGameControllerEventsMessage registerForGameControllerEventsMessage = 27; 76 | // optional KeyboardMessage keyboardMessage = 28; 77 | // optional GetKeyboardSessionMessage getKeyboardSessionMessage = 29; 78 | // optional TextInputMessage textInputMessage = 30; 79 | // optional GetVoiceInputDevicesMessage getVoiceInputDevicesMessage = 31; 80 | // optional GetVoiceInputDevicesResponseMessage getVoiceInputDevicesResponseMessage = 32; 81 | // optional RegisterVoiceInputDeviceMessage registerVoiceInputDeviceMessage = 33; 82 | // optional RegisterVoiceInputDeviceResponseMessage registerVoiceInputDeviceResponseMessage = 34; 83 | // optional SetRecordingStateMessage setRecordingStateMessage = 35; 84 | // optional SendVoiceInputMessage sendVoiceInputMessage = 36; 85 | // optional GetPlaybackQueueMessage getPlaybackQueueMessage = 37; 86 | // optional TransactionMessage transactionMessage = 38; 87 | // optional CryptoPairingMessage cryptoPairingMessage = 39; 88 | // optional GameControllerPropertiesMessage gameControllerPropertiesMessage = 40; 89 | // optional SetReadyStateMessage setReadyStateMessage = 41; 90 | // optional SendButtonEventMessage sendButtonEventMessage = 43; 91 | // optional SetHiliteModeMessage setHiliteModeMessage = 44; 92 | } 93 | -------------------------------------------------------------------------------- /dist/lib/protos/RegisterForGameControllerEventsMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional RegisterForGameControllerEventsMessage registerForGameControllerEventsMessage = 27; 7 | } 8 | 9 | message RegisterForGameControllerEventsMessage { 10 | optional int32 inputModeFlags = 1; 11 | } 12 | -------------------------------------------------------------------------------- /dist/lib/protos/RegisterHIDDeviceMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "VirtualTouchDeviceDescriptor.proto"; 5 | 6 | extend ProtocolMessage { 7 | optional RegisterHIDDeviceMessage registerHIDDeviceMessage = 11; 8 | } 9 | 10 | message RegisterHIDDeviceMessage { 11 | optional VirtualTouchDeviceDescriptor deviceDescriptor = 1; 12 | } -------------------------------------------------------------------------------- /dist/lib/protos/RegisterHIDDeviceResultMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional RegisterHIDDeviceResultMessage registerHIDDeviceResultMessage = 12; 7 | } 8 | 9 | message RegisterHIDDeviceResultMessage { 10 | optional int32 errorCode = 1; 11 | optional int32 deviceIdentifier = 2; 12 | } -------------------------------------------------------------------------------- /dist/lib/protos/RegisterVoiceInputDeviceMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "VoiceInputDeviceDescriptor.proto"; 5 | 6 | extend ProtocolMessage { 7 | optional RegisterVoiceInputDeviceMessage registerVoiceInputDeviceMessage = 33; 8 | } 9 | 10 | message RegisterVoiceInputDeviceMessage { 11 | optional VoiceInputDeviceDescriptor deviceDescriptor = 1; 12 | } -------------------------------------------------------------------------------- /dist/lib/protos/RegisterVoiceInputDeviceResponseMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional RegisterVoiceInputDeviceResponseMessage registerVoiceInputDeviceResponseMessage = 34; 7 | } 8 | 9 | message RegisterVoiceInputDeviceResponseMessage { 10 | optional int32 deviceID = 1; 11 | optional int32 errorCode = 2; 12 | } -------------------------------------------------------------------------------- /dist/lib/protos/SendButtonEventMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SendButtonEventMessage sendButtonEventMessage = 43; 7 | } 8 | 9 | message SendButtonEventMessage { 10 | optional uint32 usagePage = 1; 11 | optional uint32 usage = 2; 12 | optional bool buttonDown = 3; 13 | } -------------------------------------------------------------------------------- /dist/lib/protos/SendCommandMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "CommandInfo.proto"; 5 | import "CommandOptions.proto"; 6 | import "PlayerPath.proto"; 7 | 8 | extend ProtocolMessage { 9 | optional SendCommandMessage sendCommandMessage = 6; 10 | } 11 | 12 | message SendCommandMessage { 13 | optional Command command = 1; 14 | optional CommandOptions options = 2; 15 | optional PlayerPath playerPath = 3; 16 | } -------------------------------------------------------------------------------- /dist/lib/protos/SendHIDEventMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SendHIDEventMessage sendHIDEventMessage = 13; 7 | } 8 | 9 | message SendHIDEventMessage { 10 | // This data corresponds to a "keyboardEvent" in IOHIDEvent.h encoded as raw 11 | // data. Here is one source: 12 | // 13 | // https://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-308/IOHIDFamily/IOHIDEvent.h.auto.html 14 | // 15 | // The interesting parts are: 16 | // - usagePage (UInt32) 17 | // - usage (Uint32) 18 | // - down (bool) 19 | // 20 | // The parameters usagePage and usage corresponds to the key being pressed. 21 | // It is mapped to the USB HID values, which can be found here: 22 | // 23 | // https://github.com/Daij-Djan/DDHidLib/blob/master/usb_hid_usages.txt 24 | // 25 | // Pressing left key would for instance map to usagePage=0x01, usage=0x8B. In 26 | // the hid data, these values are stored as big endian uint16 values in the 27 | // mentioned order. So the same example would be: 0x0001008B0001, assuming 28 | // down = true (key being pressed). For each key press, the same usagePage 29 | // and usage are sent with down=true and down=false (key down + key up). 30 | // 31 | // There is a bit of magic in the raw data that's just not decoded yet, but 32 | // that doesn't matter. Just use this and it will work: 33 | // 34 | // 438922cf0802000000000000000000000100000000000000020000002000000003000000010000000000000000000000000001000000 35 | // 36 | // corresponds to the values above, e.g. 0001008B0001. The first 8 37 | // bytes is a timestamp (mach AbsoluteTime). It's a bit tricky to derive but 38 | // tvOS seems to accept old timestamps here. So it's probably fine to send 39 | // anything. 40 | optional bytes hidEventData = 1; 41 | } -------------------------------------------------------------------------------- /dist/lib/protos/SendPackedVirtualTouchEventMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SendPackedVirtualTouchEventMessage sendPackedVirtualTouchEventMessage = 47; 7 | } 8 | 9 | message SendPackedVirtualTouchEventMessage { 10 | 11 | // Corresponds to "phase" in data 12 | enum Phase { 13 | Began = 1; 14 | Moved = 2; 15 | Stationary = 3; 16 | Ended = 4; 17 | Cancelled = 5; 18 | } 19 | 20 | // The packed version of VirtualTouchEvent contains X, Y, phase, deviceID 21 | // and finger stored as a byte array. Each value is written as 16bit little 22 | // endian integers. 23 | optional bytes data = 1; 24 | } -------------------------------------------------------------------------------- /dist/lib/protos/SendVirtualTouchEventMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "VirtualTouchEvent.proto"; 5 | 6 | extend ProtocolMessage { 7 | optional SendVirtualTouchEventMessage sendVirtualTouchEventMessage = 15; 8 | } 9 | 10 | message SendVirtualTouchEventMessage { 11 | optional int32 deviceIdentifier = 1; 12 | optional VirtualTouchEvent event = 2; 13 | } -------------------------------------------------------------------------------- /dist/lib/protos/SendVoiceInputMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "AudioFormatSettings.proto"; 5 | import "AudioBuffer.proto"; 6 | 7 | extend ProtocolMessage { 8 | optional SendVoiceInputMessage sendVoiceInputMessage = 36; 9 | } 10 | 11 | message VoiceInputTime { 12 | required float timestamp = 1; 13 | required float sampleRate = 2; 14 | } 15 | 16 | message VoiceInputDataBlock { 17 | required AudioBuffer buffer = 1; 18 | optional VoiceInputTime time = 2; 19 | optional float gain = 3; 20 | } 21 | 22 | message SendVoiceInputMessage { 23 | required VoiceInputDataBlock dataBlock = 1; 24 | } 25 | -------------------------------------------------------------------------------- /dist/lib/protos/SetArtworkMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SetArtworkMessage setArtworkMessage = 10; 7 | } 8 | 9 | message SetArtworkMessage { 10 | optional bytes jpegData = 1; 11 | } 12 | -------------------------------------------------------------------------------- /dist/lib/protos/SetConnectionStateMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SetConnectionStateMessage setConnectionStateMessage = 42; 7 | } 8 | 9 | message SetConnectionStateMessage { 10 | enum ConnectionState { 11 | Connected = 2; 12 | } 13 | 14 | optional ConnectionState state = 1; 15 | } 16 | -------------------------------------------------------------------------------- /dist/lib/protos/SetHiliteModeMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SetHiliteModeMessage setHiliteModeMessage = 44; 7 | } 8 | 9 | message SetHiliteModeMessage { 10 | optional bool hiliteMode = 1; 11 | } -------------------------------------------------------------------------------- /dist/lib/protos/SetRecordingStateMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SetRecordingStateMessage setRecordingStateMessage = 35; 7 | } 8 | 9 | message SetRecordingStateMessage { 10 | enum RecordingState { 11 | Recording = 1; 12 | NotRecording = 2; 13 | } 14 | 15 | required RecordingState state = 1; 16 | } -------------------------------------------------------------------------------- /dist/lib/protos/SetStateMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "NowPlayingInfo.proto"; 5 | import "SupportedCommands.proto"; 6 | import "PlayerPath.proto"; 7 | import "PlaybackQueue.proto"; 8 | import "PlaybackQueueRequestMessage.proto"; 9 | import "PlaybackQueueCapabilities.proto"; 10 | 11 | extend ProtocolMessage { 12 | optional SetStateMessage setStateMessage = 9; 13 | } 14 | 15 | message SetStateMessage { 16 | enum PlaybackState { 17 | Unknown = 0; 18 | Playing = 1; 19 | Paused = 2; 20 | Stopped = 3; 21 | Interrupted = 4; 22 | Seeking = 5; 23 | } 24 | 25 | optional NowPlayingInfo nowPlayingInfo = 1; 26 | optional SupportedCommands supportedCommands = 2; 27 | optional PlaybackQueue playbackQueue = 3; 28 | optional string displayID = 4; 29 | optional string displayName = 5; 30 | optional PlaybackState playbackState = 6; 31 | optional PlaybackQueueCapabilities playbackQueueCapabilities = 8; 32 | optional PlayerPath playerPath = 9; 33 | optional PlaybackQueueRequestMessage request = 10; 34 | optional double playbackStateTimestamp = 11; 35 | } -------------------------------------------------------------------------------- /dist/lib/protos/SupportedCommands.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "CommandInfo.proto"; 4 | 5 | message SupportedCommands { 6 | repeated CommandInfo supportedCommands = 1; 7 | } -------------------------------------------------------------------------------- /dist/lib/protos/TextEditingAttributes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "TextInputTraits.proto"; 4 | 5 | message TextEditingAttributes { 6 | optional string title = 1; 7 | optional string prompt = 2; 8 | optional TextInputTraits inputTraits = 3; 9 | } -------------------------------------------------------------------------------- /dist/lib/protos/TextInputTraits.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message TextInputTraits { 4 | enum AutocapitalizationType { 5 | NONE = 0; 6 | WORDS = 1; 7 | SENTENCES = 2; 8 | CHARACTERS = 3; 9 | } 10 | 11 | enum KeyboardType { 12 | KEYBOARD_TYPE_DEFAULT = 0; 13 | ASCII_CAPABLE = 1; 14 | NUMBERS_AND_PUNCTUATION = 2; 15 | URL = 3; 16 | NUMBER_PAD = 4; 17 | PHONE_PAD = 5; 18 | NAME_PHONE_PAD = 6; 19 | EMAIL_ADDRESS = 7; 20 | DECIMAL_PAD = 8; 21 | TWITTER = 9; 22 | WEB_SEARCH = 10; 23 | // ALPHABET = 1; 24 | } 25 | 26 | enum ReturnKeyType { 27 | RETURN_KEY_DEFAULT = 0; 28 | GO = 1; 29 | GOOGLE = 2; 30 | JOIN = 3; 31 | NEXT = 4; 32 | ROUTE = 5; 33 | SEARCH = 6; 34 | SEND = 7; 35 | YAHOO = 8; 36 | DONE = 9; 37 | EMERGENCY_CALL = 10; 38 | CONTINUE = 11; 39 | } 40 | 41 | // optional AutocapitalizationType autocapitalizationType = ?; 42 | // optional bool autocorrection = ?; 43 | // repeated int64 PINEntrySeparatorIndexes = ?; 44 | // optional bool enablesReturnKeyAutomatically = ?; 45 | // optional KeyboardType keyboardType = ?; 46 | // optional ReturnKeyType returnKeyType = ?; 47 | // optional bool secureTextEntry = ?; 48 | // optional bool spellchecking = ?; 49 | // optional int32 validTextRangeLength = ?; 50 | // optional int32 validTextRangeLocation = ?; 51 | } -------------------------------------------------------------------------------- /dist/lib/protos/TransactionKey.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message TransactionKey { 4 | optional string identifier = 1; 5 | optional bytes userData = 2; 6 | } -------------------------------------------------------------------------------- /dist/lib/protos/TransactionMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "TransactionPackets.proto"; 4 | import "ProtocolMessage.proto"; 5 | import "PlayerPath.proto"; 6 | 7 | extend ProtocolMessage { 8 | optional TransactionMessage transactionMessage = 38; 9 | } 10 | 11 | message TransactionMessage { 12 | optional uint64 name = 1; 13 | optional TransactionPackets packets = 2; 14 | optional PlayerPath playerPath = 3; 15 | } -------------------------------------------------------------------------------- /dist/lib/protos/TransactionPacket.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "TransactionKey.proto"; 4 | 5 | message TransactionPacket { 6 | optional TransactionKey key = 1; 7 | optional bytes packetData = 2; 8 | optional string identifier = 3; 9 | optional uint64 totalLength = 4; 10 | optional uint64 totalWritePosition = 5; 11 | } -------------------------------------------------------------------------------- /dist/lib/protos/TransactionPackets.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "TransactionPacket.proto"; 4 | 5 | message TransactionPackets { 6 | repeated TransactionPacket packets = 1; 7 | } -------------------------------------------------------------------------------- /dist/lib/protos/VirtualTouchDeviceDescriptor.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message VirtualTouchDeviceDescriptor { 4 | optional bool absolute = 1; 5 | optional bool integratedDisplay = 2; 6 | optional float screenSizeWidth = 3; 7 | optional float screenSizeHeight = 4; 8 | } -------------------------------------------------------------------------------- /dist/lib/protos/VirtualTouchEvent.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message VirtualTouchEvent { 4 | optional double x = 1; 5 | optional double y = 2; 6 | optional int32 phase = 3; 7 | optional int32 finger = 4; 8 | } -------------------------------------------------------------------------------- /dist/lib/protos/VoiceInputDeviceDescriptor.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "AudioFormatSettings.proto"; 4 | 5 | message VoiceInputDeviceDescriptor { 6 | optional AudioFormatSettings defaultFormat = 1; 7 | repeated AudioFormatSettings supportedFormats = 2; 8 | } -------------------------------------------------------------------------------- /dist/lib/protos/VolumeControlAvailabilityMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional VolumeControlAvailabilityMessage volumeControlAvailabilityMessage = 22; 7 | } 8 | 9 | message VolumeControlAvailabilityMessage { 10 | optional bool volumeControlAvailable = 1; 11 | } -------------------------------------------------------------------------------- /dist/lib/protos/WakeDeviceMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional WakeDeviceMessage wakeDeviceMessage = 45; 7 | } 8 | 9 | message WakeDeviceMessage { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /dist/lib/supported-command.d.ts: -------------------------------------------------------------------------------- 1 | export declare class SupportedCommand { 2 | command: keyof typeof SupportedCommand.Command; 3 | enabled: boolean; 4 | canScrub: boolean; 5 | constructor(command: keyof typeof SupportedCommand.Command, enabled: boolean, canScrub: boolean); 6 | } 7 | export declare module SupportedCommand { 8 | enum Command { 9 | Play = "Play", 10 | Pause = "Pause", 11 | TogglePlayPause = "TogglePlayPause", 12 | EnableLanguageOption = "EnableLanguageOption", 13 | DisableLanguageOption = "DisableLanguageOption", 14 | Stop = "Stop", 15 | SkipForward = "SkipForward", 16 | SkipBackward = "SkipBackward", 17 | BeginFastForward = "BeginFastForward", 18 | BeginRewind = "BeginRewind", 19 | ChangePlaybackRate = "ChangePlaybackRate", 20 | SeekToPlaybackPosition = "SeekToPlaybackPosition", 21 | NextInContext = "NextInContext", 22 | PreviousInContext = "PreviousInContext" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /dist/lib/supported-command.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class SupportedCommand { 4 | constructor(command, enabled, canScrub) { 5 | this.command = command; 6 | this.enabled = enabled; 7 | this.canScrub = canScrub; 8 | } 9 | } 10 | exports.SupportedCommand = SupportedCommand; 11 | (function (SupportedCommand) { 12 | let Command; 13 | (function (Command) { 14 | Command["Play"] = "Play"; 15 | Command["Pause"] = "Pause"; 16 | Command["TogglePlayPause"] = "TogglePlayPause"; 17 | Command["EnableLanguageOption"] = "EnableLanguageOption"; 18 | Command["DisableLanguageOption"] = "DisableLanguageOption"; 19 | Command["Stop"] = "Stop"; 20 | Command["SkipForward"] = "SkipForward"; 21 | Command["SkipBackward"] = "SkipBackward"; 22 | Command["BeginFastForward"] = "BeginFastForward"; 23 | Command["BeginRewind"] = "BeginRewind"; 24 | Command["ChangePlaybackRate"] = "ChangePlaybackRate"; 25 | Command["SeekToPlaybackPosition"] = "SeekToPlaybackPosition"; 26 | Command["NextInContext"] = "NextInContext"; 27 | Command["PreviousInContext"] = "PreviousInContext"; 28 | })(Command = SupportedCommand.Command || (SupportedCommand.Command = {})); 29 | })(SupportedCommand = exports.SupportedCommand || (exports.SupportedCommand = {})); 30 | -------------------------------------------------------------------------------- /dist/lib/util/encryption.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare function verifyAndDecrypt(cipherText: Buffer, mac: Buffer, AAD: Buffer, nonce: Buffer, key: Buffer): Buffer; 3 | declare function encryptAndSeal(plainText: Buffer, AAD: Buffer, nonce: Buffer, key: Buffer): Buffer[]; 4 | declare function HKDF(hashAlg: string, salt: Buffer, ikm: Buffer, info: Buffer, size: number): Buffer; 5 | declare const _default: { 6 | encryptAndSeal: typeof encryptAndSeal; 7 | verifyAndDecrypt: typeof verifyAndDecrypt; 8 | HKDF: typeof HKDF; 9 | }; 10 | export default _default; 11 | -------------------------------------------------------------------------------- /dist/lib/util/encryption.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const sodium_1 = require("sodium"); 4 | const crypto = require("crypto"); 5 | const number_1 = require("./number"); 6 | function computePoly1305(cipherText, AAD, nonce, key) { 7 | if (AAD == null) { 8 | AAD = Buffer.alloc(0); 9 | } 10 | const msg = Buffer.concat([ 11 | AAD, 12 | getPadding(AAD, 16), 13 | cipherText, 14 | getPadding(cipherText, 16), 15 | number_1.default.UInt53toBufferLE(AAD.length), 16 | number_1.default.UInt53toBufferLE(cipherText.length) 17 | ]); 18 | const polyKey = sodium_1.api.crypto_stream_chacha20(32, nonce, key); 19 | const computed_hmac = sodium_1.api.crypto_onetimeauth(msg, polyKey); 20 | polyKey.fill(0); 21 | return computed_hmac; 22 | } 23 | // i'd really prefer for this to be a direct call to 24 | // Sodium.crypto_aead_chacha20poly1305_decrypt() 25 | // but unfortunately the way it constructs the message to 26 | // calculate the HMAC is not compatible with homekit 27 | // (long story short, it uses [ AAD, AAD.length, CipherText, CipherText.length ] 28 | // whereas homekit expects [ AAD, CipherText, AAD.length, CipherText.length ] 29 | function verifyAndDecrypt(cipherText, mac, AAD, nonce, key) { 30 | const matches = sodium_1.api.crypto_verify_16(mac, computePoly1305(cipherText, AAD, nonce, key)); 31 | if (matches === 0) { 32 | return sodium_1.api 33 | .crypto_stream_chacha20_xor_ic(cipherText, nonce, 1, key); 34 | } 35 | return null; 36 | } 37 | // See above about calling directly into libsodium. 38 | function encryptAndSeal(plainText, AAD, nonce, key) { 39 | const cipherText = sodium_1.api 40 | .crypto_stream_chacha20_xor_ic(plainText, nonce, 1, key); 41 | const hmac = computePoly1305(cipherText, AAD, nonce, key); 42 | return [cipherText, hmac]; 43 | } 44 | function getPadding(buffer, blockSize) { 45 | return buffer.length % blockSize === 0 46 | ? Buffer.alloc(0) 47 | : Buffer.alloc(blockSize - (buffer.length % blockSize)); 48 | } 49 | function HKDF(hashAlg, salt, ikm, info, size) { 50 | // create the hash alg to see if it exists and get its length 51 | var hash = crypto.createHash(hashAlg); 52 | var hashLength = hash.digest().length; 53 | // now we compute the PRK 54 | var hmac = crypto.createHmac(hashAlg, salt); 55 | hmac.update(ikm); 56 | var prk = hmac.digest(); 57 | var prev = Buffer.alloc(0); 58 | var output; 59 | var buffers = []; 60 | var num_blocks = Math.ceil(size / hashLength); 61 | info = Buffer.from(info); 62 | for (var i = 0; i < num_blocks; i++) { 63 | var hmac = crypto.createHmac(hashAlg, prk); 64 | var input = Buffer.concat([ 65 | prev, 66 | info, 67 | Buffer.from(String.fromCharCode(i + 1)) 68 | ]); 69 | hmac.update(input); 70 | prev = hmac.digest(); 71 | buffers.push(prev); 72 | } 73 | output = Buffer.concat(buffers, size); 74 | return output.slice(0, size); 75 | } 76 | exports.default = { 77 | encryptAndSeal, 78 | verifyAndDecrypt, 79 | HKDF 80 | }; 81 | -------------------------------------------------------------------------------- /dist/lib/util/number.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare function UInt53toBufferLE(number: number): Buffer; 3 | declare function UInt16toBufferBE(number: number): Buffer; 4 | declare const _default: { 5 | UInt53toBufferLE: typeof UInt53toBufferLE; 6 | UInt16toBufferBE: typeof UInt16toBufferBE; 7 | }; 8 | export default _default; 9 | -------------------------------------------------------------------------------- /dist/lib/util/number.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const assert = require("assert"); 4 | /* 5 | * Originally based on code from github:KhaosT/HAP-NodeJS@0c8fd88 used 6 | * used per the terms of the Apache Software License v2. 7 | * 8 | * Original code copyright Khaos Tian 9 | * 10 | * Modifications copyright Zach Bean 11 | * * Reformatted for ES6-style module 12 | * * renamed *UInt64* to *UInt53* to be more clear about range 13 | * * renamed uintHighLow to be more clear about what it does 14 | * * Refactored to return a buffer rather write into a passed-in buffer 15 | */ 16 | function splitUInt53(number) { 17 | const MAX_UINT32 = 0x00000000FFFFFFFF; 18 | const MAX_INT53 = 0x001FFFFFFFFFFFFF; 19 | assert(number > -1 && number <= MAX_INT53, "number out of range"); 20 | assert(Math.floor(number) === number, "number must be an integer"); 21 | var high = 0; 22 | var signbit = number & 0xFFFFFFFF; 23 | var low = signbit < 0 ? (number & 0x7FFFFFFF) + 0x80000000 : signbit; 24 | if (number > MAX_UINT32) { 25 | high = (number - low) / (MAX_UINT32 + 1); 26 | } 27 | return [high, low]; 28 | } 29 | function UInt53toBufferLE(number) { 30 | const [high, low] = splitUInt53(number); 31 | const buf = Buffer.alloc(8); 32 | buf.writeUInt32LE(low, 0); 33 | buf.writeUInt32LE(high, 4); 34 | return buf; 35 | } 36 | function UInt16toBufferBE(number) { 37 | const buf = Buffer.alloc(2); 38 | buf.writeUInt16BE(number, 0); 39 | return buf; 40 | } 41 | exports.default = { 42 | UInt53toBufferLE, 43 | UInt16toBufferBE 44 | }; 45 | -------------------------------------------------------------------------------- /dist/lib/util/tlv.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type Length Value encoding/decoding, used by HAP as a wire format. 3 | * https://en.wikipedia.org/wiki/Type-length-value 4 | * 5 | * Originally based on code from github:KhaosT/HAP-NodeJS@0c8fd88 used 6 | * used per the terms of the Apache Software License v2. 7 | * 8 | * Original code copyright Khaos Tian 9 | * 10 | * Modifications copyright Zach Bean 11 | * * Reformatted for ES6-style module 12 | * * Rewrote encode() to be non-recursive; also simplified the logic 13 | * * Rewrote decode() 14 | */ 15 | /// 16 | declare function encode(type: any, data: any, ...args: any[]): Buffer; 17 | declare function decode(data: any): {}; 18 | declare const _default: { 19 | Tag: { 20 | PairingMethod: number; 21 | Username: number; 22 | Salt: number; 23 | PublicKey: number; 24 | Proof: number; 25 | EncryptedData: number; 26 | Sequence: number; 27 | ErrorCode: number; 28 | BackOff: number; 29 | Signature: number; 30 | MFiCertificate: number; 31 | MFiSignature: number; 32 | }; 33 | encode: typeof encode; 34 | decode: typeof decode; 35 | }; 36 | export default _default; 37 | -------------------------------------------------------------------------------- /dist/lib/util/tlv.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Type Length Value encoding/decoding, used by HAP as a wire format. 4 | * https://en.wikipedia.org/wiki/Type-length-value 5 | * 6 | * Originally based on code from github:KhaosT/HAP-NodeJS@0c8fd88 used 7 | * used per the terms of the Apache Software License v2. 8 | * 9 | * Original code copyright Khaos Tian 10 | * 11 | * Modifications copyright Zach Bean 12 | * * Reformatted for ES6-style module 13 | * * Rewrote encode() to be non-recursive; also simplified the logic 14 | * * Rewrote decode() 15 | */ 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | const Tag = { 18 | PairingMethod: 0x00, 19 | Username: 0x01, 20 | Salt: 0x02, 21 | // could be either the SRP client public key (384 bytes) or the ED25519 public key (32 bytes), depending on context 22 | PublicKey: 0x03, 23 | Proof: 0x04, 24 | EncryptedData: 0x05, 25 | Sequence: 0x06, 26 | ErrorCode: 0x07, 27 | BackOff: 0x08, 28 | Signature: 0x0A, 29 | MFiCertificate: 0x09, 30 | MFiSignature: 0x0A 31 | }; 32 | function encode(type, data, ...args) { 33 | var encodedTLVBuffer = Buffer.alloc(0); 34 | // coerce data to Buffer if needed 35 | if (typeof data === 'number') 36 | data = Buffer.from([data]); 37 | else if (typeof data === 'string') 38 | data = Buffer.from(data); 39 | if (data.length <= 255) { 40 | encodedTLVBuffer = Buffer.concat([Buffer.from([type, data.length]), data]); 41 | } 42 | else { 43 | var leftLength = data.length; 44 | var tempBuffer = Buffer.alloc(0); 45 | var currentStart = 0; 46 | for (; leftLength > 0;) { 47 | if (leftLength >= 255) { 48 | tempBuffer = Buffer.concat([tempBuffer, Buffer.from([type, 0xFF]), data.slice(currentStart, currentStart + 255)]); 49 | leftLength -= 255; 50 | currentStart = currentStart + 255; 51 | } 52 | else { 53 | tempBuffer = Buffer.concat([tempBuffer, Buffer.from([type, leftLength]), data.slice(currentStart, currentStart + leftLength)]); 54 | leftLength -= leftLength; 55 | } 56 | } 57 | encodedTLVBuffer = tempBuffer; 58 | } 59 | // do we have more to encode? 60 | if (arguments.length > 2) { 61 | // chop off the first two arguments which we already processed, and process the rest recursively 62 | var remainingArguments = Array.prototype.slice.call(arguments, 2); 63 | var remainingTLVBuffer = encode.apply(this, remainingArguments); 64 | // append the remaining encoded arguments directly to the buffer 65 | encodedTLVBuffer = Buffer.concat([encodedTLVBuffer, remainingTLVBuffer]); 66 | } 67 | return encodedTLVBuffer; 68 | } 69 | function decode(data) { 70 | var objects = {}; 71 | var leftLength = data.length; 72 | var currentIndex = 0; 73 | for (; leftLength > 0;) { 74 | var type = data[currentIndex]; 75 | var length = data[currentIndex + 1]; 76 | currentIndex += 2; 77 | leftLength -= 2; 78 | var newData = data.slice(currentIndex, currentIndex + length); 79 | if (objects[type]) { 80 | objects[type] = Buffer.concat([objects[type], newData]); 81 | } 82 | else { 83 | objects[type] = newData; 84 | } 85 | currentIndex += length; 86 | leftLength -= length; 87 | } 88 | return objects; 89 | } 90 | exports.default = { 91 | Tag, 92 | encode, 93 | decode 94 | }; 95 | -------------------------------------------------------------------------------- /dist/lib/verifier.d.ts: -------------------------------------------------------------------------------- 1 | import { AppleTV } from './appletv'; 2 | export declare class Verifier { 3 | device: AppleTV; 4 | constructor(device: AppleTV); 5 | verify(): Promise<{}>; 6 | private requestPairingData; 7 | private completeVerification; 8 | } 9 | -------------------------------------------------------------------------------- /dist/lib/verifier.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const ed25519 = require("ed25519"); 13 | const curve25519 = require("curve25519-n2"); 14 | const tlv_1 = require("./util/tlv"); 15 | const encryption_1 = require("./util/encryption"); 16 | class Verifier { 17 | constructor(device) { 18 | this.device = device; 19 | } 20 | verify() { 21 | return __awaiter(this, void 0, void 0, function* () { 22 | var verifyPrivate = Buffer.alloc(32); 23 | curve25519.makeSecretKey(verifyPrivate); 24 | let verifyPublic = curve25519.derivePublicKey(verifyPrivate); 25 | let { sessionPublicKey, encryptionKey, sharedSecret, pairingData } = yield this.requestPairingData(verifyPublic, verifyPrivate); 26 | let tlvData = tlv_1.default.decode(pairingData); 27 | let identifier = tlvData[tlv_1.default.Tag.Username]; 28 | let signature = tlvData[tlv_1.default.Tag.Signature]; 29 | if (!identifier.equals(this.device.credentials.identifier)) { 30 | throw new Error("Identifier mismatch"); 31 | } 32 | let deviceInfo = Buffer.concat([sessionPublicKey, Buffer.from(identifier), verifyPublic]); 33 | if (!ed25519.Verify(deviceInfo, signature, this.device.credentials.publicKey)) { 34 | throw new Error("Signature verification failed"); 35 | } 36 | return yield this.completeVerification(verifyPublic, sessionPublicKey, encryptionKey, sharedSecret); 37 | }); 38 | } 39 | requestPairingData(verifyPublic, verifyPrivate) { 40 | return __awaiter(this, void 0, void 0, function* () { 41 | let encodedData = tlv_1.default.encode(tlv_1.default.Tag.Sequence, 0x01, tlv_1.default.Tag.PublicKey, verifyPublic); 42 | let message = { 43 | status: 0, 44 | state: 3, 45 | isRetrying: true, 46 | isUsingSystemPairing: true, 47 | pairingData: encodedData 48 | }; 49 | yield this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', message, false); 50 | let pairingDataResponse = yield this.device.waitForSequence(0x02); 51 | let pairingData = pairingDataResponse.payload.pairingData; 52 | let decodedData = tlv_1.default.decode(pairingData); 53 | let sessionPublicKey = decodedData[tlv_1.default.Tag.PublicKey]; 54 | let encryptedData = decodedData[tlv_1.default.Tag.EncryptedData]; 55 | if (sessionPublicKey.length != 32) { 56 | throw new Error(`sessionPublicKey must be 32 bytes (but was ${sessionPublicKey.length})`); 57 | } 58 | let cipherText = encryptedData.slice(0, -16); 59 | let hmac = encryptedData.slice(-16); 60 | let sharedSecret = curve25519.deriveSharedSecret(verifyPrivate, sessionPublicKey); 61 | let encryptionKey = encryption_1.default.HKDF("sha512", Buffer.from("Pair-Verify-Encrypt-Salt"), sharedSecret, Buffer.from("Pair-Verify-Encrypt-Info"), 32); 62 | let decryptedData = encryption_1.default.verifyAndDecrypt(cipherText, hmac, null, Buffer.from('PV-Msg02'), encryptionKey); 63 | return { 64 | sessionPublicKey: sessionPublicKey, 65 | sharedSecret: sharedSecret, 66 | encryptionKey: encryptionKey, 67 | pairingData: decryptedData 68 | }; 69 | }); 70 | } 71 | completeVerification(verifyPublic, sessionPublicKey, encryptionKey, sharedSecret) { 72 | return __awaiter(this, void 0, void 0, function* () { 73 | let material = Buffer.concat([verifyPublic, Buffer.from(this.device.credentials.pairingId), sessionPublicKey]); 74 | let keyPair = ed25519.MakeKeypair(this.device.credentials.encryptionKey); 75 | let signed = ed25519.Sign(material, keyPair); 76 | let plainTLV = tlv_1.default.encode(tlv_1.default.Tag.Username, Buffer.from(this.device.credentials.pairingId), tlv_1.default.Tag.Signature, signed); 77 | let encryptedTLV = Buffer.concat(encryption_1.default.encryptAndSeal(plainTLV, null, Buffer.from('PV-Msg03'), encryptionKey)); 78 | let tlvData = tlv_1.default.encode(tlv_1.default.Tag.Sequence, 0x03, tlv_1.default.Tag.EncryptedData, encryptedTLV); 79 | let message = { 80 | status: 0, 81 | state: 3, 82 | isRetrying: false, 83 | isUsingSystemPairing: true, 84 | pairingData: tlvData 85 | }; 86 | yield this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', message, false); 87 | yield this.device.waitForSequence(0x04); 88 | let readKey = encryption_1.default.HKDF("sha512", Buffer.from("MediaRemote-Salt"), sharedSecret, Buffer.from("MediaRemote-Read-Encryption-Key"), 32); 89 | let writeKey = encryption_1.default.HKDF("sha512", Buffer.from("MediaRemote-Salt"), sharedSecret, Buffer.from("MediaRemote-Write-Encryption-Key"), 32); 90 | return { 91 | readKey: readKey, 92 | writeKey: writeKey 93 | }; 94 | }); 95 | } 96 | } 97 | exports.Verifier = Verifier; 98 | -------------------------------------------------------------------------------- /dist/test/appletv.spec.d.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | -------------------------------------------------------------------------------- /dist/test/appletv.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const appletv_1 = require("../lib/appletv"); 13 | const message_1 = require("../lib/message"); 14 | const chai_1 = require("chai"); 15 | const net_1 = require("net"); 16 | const sinon = require("sinon"); 17 | require("mocha"); 18 | describe('apple tv tests', function () { 19 | beforeEach(function () { 20 | let socket = new net_1.Socket({}); 21 | sinon.stub(socket, 'write'); 22 | sinon.stub(socket, 'connect').callsFake(function (port, host, callback) { 23 | callback(); 24 | }); 25 | this.device = new appletv_1.AppleTV({ 26 | addresses: ['127.0.0.1'], 27 | port: 12345, 28 | flags: 0, 29 | fullname: '', 30 | host: '', 31 | interfaceIndex: 0, 32 | networkInterface: '', 33 | replyDomain: '', 34 | type: null, 35 | txtRecord: { 36 | Name: "Mock Apple TV", 37 | UniqueIdentifier: "MockAppleTVUUID" 38 | } 39 | }, socket); 40 | this.fake = sinon.stub(this.device.connection, 'sendProtocolMessage'); 41 | this.device.connection.isOpen = true; 42 | this.sentMessages = function () { 43 | var messages = []; 44 | for (var i = 0; i < this.fake.callCount; i++) { 45 | messages.push(new message_1.Message(this.fake.getCall(i).args[0])); 46 | } 47 | return messages; 48 | }; 49 | }); 50 | it('should send introduction', function () { 51 | return __awaiter(this, void 0, void 0, function* () { 52 | yield this.device.openConnection(); 53 | let messages = this.sentMessages(); 54 | chai_1.expect(messages.length).to.equal(1); 55 | chai_1.expect(messages[0].type).to.equal(message_1.Message.Type.DeviceInfoMessage); 56 | }); 57 | }); 58 | it('should request artwork', function () { 59 | return __awaiter(this, void 0, void 0, function* () { 60 | let width = 640; 61 | let height = 480; 62 | yield this.device.openConnection(); 63 | try { 64 | yield this.device.requestArtwork(width, height); 65 | } 66 | catch (error) { } 67 | let messages = this.sentMessages(); 68 | chai_1.expect(messages.length).to.equal(2); 69 | chai_1.expect(messages[1].type).to.equal(message_1.Message.Type.PlaybackQueueRequestMessage); 70 | chai_1.expect(messages[1].payload.artworkWidth).to.equal(width); 71 | chai_1.expect(messages[1].payload.artworkHeight).to.equal(height); 72 | chai_1.expect(messages[1].payload.length).to.equal(1); 73 | chai_1.expect(messages[1].payload.location).to.equal(0); 74 | }); 75 | }); 76 | it('should press and release menu', function () { 77 | return __awaiter(this, void 0, void 0, function* () { 78 | yield this.device.openConnection(); 79 | yield this.device.sendKeyCommand(appletv_1.AppleTV.Key.Menu); 80 | let messages = this.sentMessages(); 81 | chai_1.expect(messages.length).to.equal(3); 82 | chai_1.expect(messages[1].type).to.equal(message_1.Message.Type.SendHidEventMessage); 83 | chai_1.expect(messages[2].type).to.equal(message_1.Message.Type.SendHidEventMessage); 84 | }); 85 | }); 86 | it('should read now playing', function () { 87 | return __awaiter(this, void 0, void 0, function* () { 88 | yield this.device.openConnection(); 89 | var spy = sinon.spy(); 90 | this.device.on('nowPlaying', spy); 91 | this.device.connection.emit('message', require('./fixtures/now-playing.json')); 92 | let messages = this.sentMessages(); 93 | chai_1.expect(messages.length).to.equal(1); 94 | chai_1.expect(spy.lastCall.lastArg.title).to.equal('Seinfeld'); 95 | chai_1.expect(spy.lastCall.lastArg.appDisplayName).to.equal('Hulu'); 96 | chai_1.expect(spy.lastCall.lastArg.appBundleIdentifier).to.equal('com.hulu.plus'); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /dist/test/browser.spec.d.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | -------------------------------------------------------------------------------- /dist/test/browser.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const browser_1 = require("../lib/browser"); 13 | const chai_1 = require("chai"); 14 | const mdns = require("mdns"); 15 | require("mocha"); 16 | const AppleTVName = "Test Apple TV"; 17 | const AppleTVIdentifier = "TestAppleTVIdentifier"; 18 | describe('apple tv discovery', function () { 19 | it('should discover apple tv', function () { 20 | return __awaiter(this, void 0, void 0, function* () { 21 | this.timeout(5000); 22 | let ad = mdns.createAdvertisement(mdns.tcp('mediaremotetv'), 54321, { 23 | name: AppleTVName, 24 | txtRecord: { 25 | Name: AppleTVName, 26 | UniqueIdentifier: AppleTVIdentifier 27 | } 28 | }); 29 | ad.start(); 30 | let browser = new browser_1.Browser(); 31 | let devices = yield browser.scan(AppleTVIdentifier); 32 | chai_1.expect(devices.length).to.be.greaterThan(0); 33 | let device = devices[0]; 34 | chai_1.expect(device.uid).to.equal(AppleTVIdentifier); 35 | chai_1.expect(device.name).to.equal(AppleTVName); 36 | ad.stop(); 37 | }); 38 | }); 39 | }); 40 | // describe('apple tv pairing', function() { 41 | // beforeEach(function() { this.mitm = Mitm(); }); 42 | // afterEach(function() { this.mitm.disable(); }); 43 | // it('should pair with apple tv', async function() { 44 | // this.mitm.on("connection", function(socket) { console.log("Hello back!") }); 45 | // this.timeout(10000); 46 | // let server = new MockServer(); 47 | // let device = await server.device; 48 | // await device.openConnection(); 49 | // let callback = await device.pair(); 50 | // }); 51 | // }); 52 | -------------------------------------------------------------------------------- /dist/test/encryption.spec.d.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | -------------------------------------------------------------------------------- /dist/test/encryption.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const encryption_1 = require("../lib/util/encryption"); 4 | const crypto = require("crypto"); 5 | const chai_1 = require("chai"); 6 | require("mocha"); 7 | describe('test encryption', function () { 8 | it('should encrypt and decrypt string', function () { 9 | let value = Buffer.from("some string"); 10 | let nonce = Buffer.from('PS-Msg06'); 11 | let key = encryption_1.default.HKDF("sha512", Buffer.from("Pair-Setup-Encrypt-Salt"), crypto.randomBytes(32), Buffer.from("Pair-Setup-Encrypt-Info"), 32); 12 | let encrypted = encryption_1.default.encryptAndSeal(value, null, nonce, key); 13 | let decrypted = encryption_1.default.verifyAndDecrypt(encrypted[0], encrypted[1], null, nonce, key); 14 | chai_1.expect(decrypted.toString()).to.equal(value.toString()); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /dist/test/helpers/mock-server.d.ts: -------------------------------------------------------------------------------- 1 | import { AppleTV } from '../../lib/appletv'; 2 | import { Message } from '../../lib/message'; 3 | export declare class MockServer { 4 | device: AppleTV; 5 | message: Promise; 6 | private server; 7 | constructor(); 8 | close(): void; 9 | } 10 | -------------------------------------------------------------------------------- /dist/test/helpers/mock-server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const net_1 = require("net"); 4 | const appletv_1 = require("../../lib/appletv"); 5 | const connection_1 = require("../../lib/connection"); 6 | class MockServer { 7 | constructor() { 8 | let port = 65416; 9 | this.device = new appletv_1.AppleTV({ 10 | addresses: ['127.0.0.1'], 11 | port: port, 12 | txtRecord: { 13 | Name: "Mock Apple TV", 14 | UniqueIdentifier: "MockAppleTVUUID" 15 | } 16 | }); 17 | let d = this.device; 18 | let that = this; 19 | this.message = new Promise(function (resolve, reject) { 20 | that.server = net_1.createServer(function (socket) { 21 | let connection = new connection_1.Connection(d, socket); 22 | d.connection = connection; 23 | d.on('message', function (message) { 24 | resolve(message); 25 | }); 26 | }); 27 | that.server.listen(port); 28 | }); 29 | } 30 | close() { 31 | this.server.close(); 32 | this.device.connection.close(); 33 | } 34 | } 35 | exports.MockServer = MockServer; 36 | -------------------------------------------------------------------------------- /dist/test/pairing.spec.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evandcoleman/node-appletv/12dd26dbb427c72be9c66cd884dd7d83c5b48da7/dist/test/pairing.spec.d.ts -------------------------------------------------------------------------------- /dist/test/pairing.spec.js: -------------------------------------------------------------------------------- 1 | // import { Pairing } from '../lib/pairing'; 2 | // import { AppleTV } from '../lib/appletv'; 3 | // import { Message } from '../lib/message'; 4 | // import { MockServer } from './helpers/mock-server'; 5 | // import { expect } from 'chai'; 6 | // import 'mocha'; 7 | // describe('apple tv pairing', function() { 8 | // beforeEach(function() { 9 | // this.server = new MockServer(); 10 | // this.device = this.server.device; 11 | // }); 12 | // afterEach(function() { 13 | // this.server.close(); 14 | // }); 15 | // it('should send introduction', async function() { 16 | // this.device.openConnection(); 17 | // let message = await this.server.message; 18 | // expect(message.type).to.equal(Message.Type.DeviceInfoMessage); 19 | // }); 20 | // }); 21 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evandcoleman/node-appletv/12dd26dbb427c72be9c66cd884dd7d83c5b48da7/docs/.nojekyll -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evandcoleman/node-appletv/12dd26dbb427c72be9c66cd884dd7d83c5b48da7/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evandcoleman/node-appletv/12dd26dbb427c72be9c66cd884dd7d83c5b48da7/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evandcoleman/node-appletv/12dd26dbb427c72be9c66cd884dd7d83c5b48da7/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evandcoleman/node-appletv/12dd26dbb427c72be9c66cd884dd7d83c5b48da7/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /images/pairing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evandcoleman/node-appletv/12dd26dbb427c72be9c66cd884dd7d83c5b48da7/images/pairing.gif -------------------------------------------------------------------------------- /images/state.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evandcoleman/node-appletv/12dd26dbb427c72be9c66cd884dd7d83c5b48da7/images/state.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-appletv", 3 | "version": "1.1.0", 4 | "description": "A Node.js library for communicating with an Apple TV", 5 | "homepage": "https://github.com/evandcoleman/node-appletv", 6 | "bugs": "https://github.com/evandcoleman/node-appletv/issues", 7 | "keywords": [ 8 | "apple tv", 9 | "appletv", 10 | "mrp", 11 | "media remote" 12 | ], 13 | "main": "dist/index.js", 14 | "types": "dist/index.d.ts", 15 | "scripts": { 16 | "prepare": "npm run build", 17 | "build": "npm run clean && tsc && cp -R ./src/lib/protos ./dist/lib/protos", 18 | "test": "mocha -r ts-node/register ./src/test/**/*.spec.ts", 19 | "coverage": "nyc --reporter=lcov mocha -r ts-node/register ./src/test/**/*.spec.ts", 20 | "clean": "rimraf dist", 21 | "docs": "rimraf docs && typedoc --mode modules --excludePrivate --excludeExternals --out ./docs --exclude ./src/test/**/* ./src" 22 | }, 23 | "bin": { 24 | "appletv": "bin/appletv" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/evandcoleman/node-appletv.git" 29 | }, 30 | "author": "Evan Coleman (https://edc.me)", 31 | "license": "MIT", 32 | "dependencies": { 33 | "camelcase": "^4.1.0", 34 | "caporal": "^1.3.0", 35 | "curve25519-n2": "^1.1.3", 36 | "ed25519": "0.0.4", 37 | "fast-srp-hap": "^1.0.1", 38 | "inquirer": "^7.0.4", 39 | "mdns": "^2.5.1", 40 | "ora": "^2.1.0", 41 | "protobufjs": "^6.8.8", 42 | "snake-case": "^2.1.0", 43 | "sodium": "^3.0.2", 44 | "uuid": "^3.4.0", 45 | "varint": "^5.0.0" 46 | }, 47 | "devDependencies": { 48 | "@types/chai": "^4.2.7", 49 | "@types/inquirer": "0.0.37", 50 | "@types/mdns": "0.0.32", 51 | "@types/mocha": "^5.2.7", 52 | "@types/node": "^13.5.0", 53 | "@types/ora": "^1.3.5", 54 | "@types/sinon": "^7.5.1", 55 | "@types/uuid": "^3.4.6", 56 | "chai": "^4.2.0", 57 | "mitm": "^1.7.0", 58 | "mocha": "^7.0.0", 59 | "nyc": "^15.0.0", 60 | "rimraf": "^3.0.0", 61 | "sinon": "^8.1.1", 62 | "ts-node": "^8.6.2", 63 | "typedoc": "^0.16.9", 64 | "typescript": "^3.7.5" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /scripts/decode-tlv.js: -------------------------------------------------------------------------------- 1 | var tlv = require('../dist/lib/util/tlv').default; 2 | 3 | let buffer = Buffer.from("06010103 20a629a8 86bd2028 0f4ef051 62efd291 22bc5cc6 610b26cd a3d704f8 d816c9e2 50".replace(/\s/g, ''), 'hex'); 4 | console.log(tlv.decode(buffer)); -------------------------------------------------------------------------------- /src/bin/index.ts: -------------------------------------------------------------------------------- 1 | import * as caporal from 'caporal'; 2 | let cli = caporal as any; 3 | 4 | import { AppleTV } from '../lib/appletv'; 5 | import { Credentials } from '../lib/credentials'; 6 | import { NowPlayingInfo } from '../lib/now-playing-info'; 7 | import { Message } from '../lib/message'; 8 | import { scan } from './scan'; 9 | import { pair } from './pair'; 10 | import { writeFile } from 'fs'; 11 | import { promisify } from 'util'; 12 | 13 | const project = require('../../package.json') 14 | 15 | async function openDevice(credentials: Credentials, logger: any): Promise { 16 | let device = await scan(logger, null, credentials.uniqueIdentifier); 17 | device.on('debug', (message: string) => { 18 | logger.debug(message); 19 | }); 20 | device.on('error', (error: Error) => { 21 | logger.error(error.message); 22 | logger.debug(error.stack); 23 | }); 24 | return await device.openConnection(credentials); 25 | } 26 | 27 | cli 28 | .version(project.version) 29 | .command('pair', 'Pair with an Apple TV') 30 | .option('--timeout ', 'The amount of time (in seconds) to scan for Apple TVs', cli.INTEGER) 31 | .action(async (args, options, logger) => { 32 | try { 33 | let device = await scan(logger, options.timeout); 34 | device.on('debug', (message: string) => { 35 | logger.debug(message); 36 | }); 37 | device.on('error', (error: Error) => { 38 | logger.error(error.message); 39 | logger.debug(error.stack); 40 | }); 41 | let keys = await pair(device, logger); 42 | logger.info("Credentials: " + device.credentials.toString()); 43 | process.exit(); 44 | } catch (error) { 45 | logger.error(error.message); 46 | logger.debug(error.stack); 47 | process.exit(); 48 | } 49 | }); 50 | 51 | cli 52 | .command('command', 'Send a command to an Apple TV') 53 | .argument('', 'The command to send', /^up|down|left|right|menu|play|pause|next|previous|suspend|wake|home|volumeup|volumedown$/) 54 | .option('--credentials ', 'The device credentials from pairing', cli.STRING) 55 | .action(async (args, options, logger) => { 56 | if (!options.credentials) { 57 | logger.error("Credentials are required. Pair first."); 58 | process.exit(); 59 | } 60 | let credentials = Credentials.parse(options.credentials); 61 | try { 62 | let device = await openDevice(credentials, logger); 63 | await device.sendKeyCommand(AppleTV.key(args["command"])) 64 | logger.info("Success!"); 65 | process.exit(); 66 | } catch (error) { 67 | logger.error(error.message); 68 | logger.debug(error.stack); 69 | process.exit(); 70 | } 71 | }); 72 | 73 | cli 74 | .command('artwork', 'Retreive the artwork for the currently playing item') 75 | .option('--output ', 'Output path for the artwork image', cli.STRING) 76 | .option('--credentials ', 'The device credentials from pairing', cli.STRING) 77 | .action(async (args, options, logger) => { 78 | if (!options.credentials) { 79 | logger.error("Credentials are required. Pair first."); 80 | process.exit(); 81 | } 82 | let credentials = Credentials.parse(options.credentials); 83 | try { 84 | let device = await openDevice(credentials, logger); 85 | let data = await device.requestArtwork(); 86 | if (options.output) { 87 | await promisify(writeFile)(options.output, data); 88 | } else { 89 | logger.info(data.toString('hex')); 90 | } 91 | process.exit(); 92 | } catch (error) { 93 | logger.error(error.message); 94 | logger.debug(error.stack); 95 | process.exit(); 96 | } 97 | }); 98 | 99 | cli 100 | .command('state', 'Logs the playback state from the Apple TV') 101 | .option('--credentials ', 'The device credentials from pairing', cli.STRING) 102 | .action(async (args, options, logger) => { 103 | if (!options.credentials) { 104 | logger.error("Credentials are required. Pair first."); 105 | process.exit(); 106 | } 107 | let credentials = Credentials.parse(options.credentials); 108 | try { 109 | let device = await openDevice(credentials, logger); 110 | 111 | device.on('nowPlaying', (info: NowPlayingInfo) => { 112 | logger.info(info.toString()); 113 | }); 114 | } catch (error) { 115 | logger.error(error.message); 116 | logger.debug(error.stack); 117 | process.exit(); 118 | } 119 | }); 120 | 121 | cli 122 | .command('queue', 'Request the playback state from the Apple TV') 123 | .option('--credentials ', 'The device credentials from pairing', cli.STRING) 124 | .option('--location ', 'The location in the queue', cli.INTEGER) 125 | .option('--length ', 'The length of the queue', cli.INTEGER) 126 | .option('--metadata', 'Include metadata', cli.BOOLEAN) 127 | .option('--lyrics', 'Include lyrics', cli.BOOLEAN) 128 | .option('--languages', 'Include language options', cli.BOOLEAN) 129 | .action(async (args, options, logger) => { 130 | if (!options.credentials) { 131 | logger.error("Credentials are required. Pair first."); 132 | process.exit(); 133 | } 134 | let credentials = Credentials.parse(options.credentials); 135 | try { 136 | let device = await openDevice(credentials, logger); 137 | let message = await device 138 | .requestPlaybackQueue({ 139 | location: options.location || 0, 140 | length: options.length || 1, 141 | includeMetadata: options.metadata, 142 | includeLyrics: options.lyrics, 143 | includeLanguageOptions: options.languages 144 | }); 145 | logger.info(message); 146 | } catch (error) { 147 | logger.error(error.message); 148 | logger.debug(error.stack); 149 | process.exit(); 150 | } 151 | }); 152 | 153 | cli 154 | .command('messages', 'Log all messages sent from the Apple TV') 155 | .option('--credentials ', 'The device credentials from pairing', cli.STRING) 156 | .action(async (args, options, logger) => { 157 | if (!options.credentials) { 158 | logger.error("Credentials are required. Pair first."); 159 | process.exit(); 160 | } 161 | let credentials = Credentials.parse(options.credentials); 162 | try { 163 | let device = await openDevice(credentials, logger); 164 | device.on('message', (message: Message) => { 165 | logger.info(JSON.stringify(message.toObject(), null, 2)); 166 | }); 167 | } catch (error) { 168 | logger.error(error.message); 169 | logger.debug(error.stack); 170 | process.exit(); 171 | } 172 | }); 173 | 174 | cli.parse(process.argv); -------------------------------------------------------------------------------- /src/bin/pair.ts: -------------------------------------------------------------------------------- 1 | import { prompt } from 'inquirer'; 2 | import * as caporal from 'caporal'; 3 | import * as ora from 'ora'; 4 | 5 | import { AppleTV } from '../lib/appletv'; 6 | import { Pairing } from '../lib/pairing'; 7 | import { Verifier } from '../lib/verifier'; 8 | 9 | export function pair(device: AppleTV, logger: Logger): Promise { 10 | let spinner = ora("Connecting to " + device.name).start() 11 | return device 12 | .openConnection() 13 | .then(() => { 14 | spinner.succeed().start('Initiating Pairing') 15 | let pairing = new Pairing(device); 16 | 17 | return pairing.initiatePair() 18 | .then(callback => { 19 | spinner.succeed(); 20 | return prompt([{ 21 | type: 'input', 22 | name: 'pin', 23 | message: "Enter the 4-digit pin that's currently being displayed on " + device.name, 24 | validate: (input) => { 25 | let isValid = /^\d+$/.test(input); 26 | 27 | return isValid ? true : 'Pin must be 4-digits and all numbers.'; 28 | } 29 | }]) 30 | .then(answers => { 31 | spinner.start('Completing Pairing'); 32 | return callback(answers['pin']); 33 | }); 34 | }) 35 | .then(device => { 36 | spinner.succeed(); 37 | return device; 38 | }) 39 | .catch(error => { 40 | spinner.fail(); 41 | throw error; 42 | }); 43 | }); 44 | } -------------------------------------------------------------------------------- /src/bin/scan.ts: -------------------------------------------------------------------------------- 1 | import { prompt } from 'inquirer'; 2 | import * as caporal from 'caporal'; 3 | import * as ora from 'ora'; 4 | 5 | import { AppleTV } from '../lib/appletv'; 6 | import { Browser } from '../lib/browser'; 7 | 8 | export function scan(logger: Logger, timeout?: number, uniqueIdentifier?: string): Promise { 9 | let browser = new Browser(); 10 | let spinner = ora('Scanning for Apple TVs...').start(); 11 | return browser 12 | .scan(uniqueIdentifier, timeout) 13 | .then(devices => { 14 | spinner.stop(); 15 | if (devices.length == 1) { 16 | return devices[0]; 17 | } 18 | 19 | if (devices.length == 0) { 20 | throw new Error("No Apple TVs found on the network. Try again."); 21 | } else { 22 | return prompt<{}>([{ 23 | type: 'list', 24 | name: 'device', 25 | message: 'Which Apple TV would you like to pair with?', 26 | choices: devices.map(device => { 27 | return { 28 | name: device.name + " (" + device.address + ":" + device.port + ")", 29 | value: device.uid 30 | }; 31 | }) 32 | }]) 33 | .then(answers => { 34 | let uid = answers['device']; 35 | return devices.filter(device => { return device.uid == uid; })[0]; 36 | }); 37 | } 38 | }); 39 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Credentials } from './lib/credentials'; 2 | import { AppleTV } from './lib/appletv'; 3 | import { Connection } from './lib/connection'; 4 | import { Browser } from './lib/browser'; 5 | import { NowPlayingInfo } from './lib/now-playing-info'; 6 | import { Message } from './lib/message'; 7 | import { SupportedCommand } from './lib/supported-command'; 8 | 9 | /** 10 | * A convenience function to scan for AppleTVs on the local network. 11 | * @param uniqueIdentifier An optional identifier for the AppleTV to scan for. The AppleTV advertises this via Bonjour. 12 | * @param timeout An optional timeout value (in seconds) to give up the search after. 13 | * @returns A promise that resolves to an array of AppleTV objects. If you provide a `uniqueIdentifier` the array is guaranteed to only contain one object. 14 | */ 15 | export function scan(uniqueIdentifier?: string, timeout?: number): Promise { 16 | let browser = new Browser(); 17 | return browser.scan(uniqueIdentifier, timeout); 18 | } 19 | 20 | /** 21 | * A convenience function to parse a credentials string into a Credentials object. 22 | * @param text The credentials string. 23 | * @returns A credentials object. 24 | */ 25 | export function parseCredentials(text: string): Credentials { 26 | return Credentials.parse(text); 27 | } 28 | 29 | export { 30 | AppleTV, 31 | Connection, 32 | Browser, 33 | NowPlayingInfo, 34 | Credentials, 35 | Message, 36 | SupportedCommand 37 | }; -------------------------------------------------------------------------------- /src/lib/browser.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import * as mdns from 'mdns'; 3 | 4 | import { AppleTV } from './appletv'; 5 | 6 | export class Browser { 7 | private browser: mdns.Browser; 8 | private services: AppleTV[]; 9 | private uniqueIdentifier: string; 10 | private onComplete: (device: AppleTV[]) => void; 11 | private onFailure: (error: Error) => void; 12 | 13 | /** 14 | * Creates a new Browser 15 | * @param log An optional function that takes a string to provide verbose logging. 16 | */ 17 | constructor() { 18 | let sequence = [ 19 | mdns.rst.DNSServiceResolve(), 20 | 'DNSServiceGetAddrInfo' in (mdns).dns_sd ? mdns.rst.DNSServiceGetAddrInfo() : mdns.rst.getaddrinfo({ families: [4] }), 21 | mdns.rst.makeAddressesUnique() 22 | ]; 23 | this.browser = mdns.createBrowser(mdns.tcp('mediaremotetv'), { resolverSequence: sequence }); 24 | this.services = []; 25 | 26 | let that = this; 27 | this.browser.on('serviceUp', function(service) { 28 | let device = new AppleTV(service); 29 | if (that.uniqueIdentifier && device.uid == that.uniqueIdentifier) { 30 | that.browser.stop(); 31 | that.onComplete([device]); 32 | } else { 33 | that.services.push(device); 34 | } 35 | }); 36 | } 37 | 38 | /** 39 | * Scans for AppleTVs on the local network. 40 | * @param uniqueIdentifier An optional identifier for the AppleTV to scan for. The AppleTV advertises this via Bonjour. 41 | * @param timeout An optional timeout value (in seconds) to give up the search after. 42 | * @returns A promise that resolves to an array of AppleTV objects. If you provide a `uniqueIdentifier` the array is guaranteed to only contain one object. 43 | */ 44 | scan(uniqueIdentifier?: string, timeout?: number): Promise { 45 | this.services = []; 46 | this.uniqueIdentifier = uniqueIdentifier; 47 | this.browser.start(); 48 | let that = this; 49 | let to = timeout == null ? 5 : timeout; 50 | 51 | return new Promise((resolve, reject) => { 52 | that.onComplete = resolve; 53 | that.onFailure = reject; 54 | setTimeout(() => { 55 | that.browser.stop(); 56 | if (that.uniqueIdentifier) { 57 | reject(new Error("Failed to locate specified AppleTV on the network")); 58 | } else { 59 | resolve(that.services 60 | .sort((a, b) => { 61 | return a > b ? 1 : -1; 62 | })); 63 | } 64 | }, to * 1000); 65 | }); 66 | } 67 | } -------------------------------------------------------------------------------- /src/lib/credentials.ts: -------------------------------------------------------------------------------- 1 | import encryption from './util/encryption'; 2 | import number from './util/number'; 3 | 4 | export class Credentials { 5 | public readKey: Buffer; 6 | public writeKey: Buffer; 7 | 8 | private encryptCount: number = 0; 9 | private decryptCount: number = 0; 10 | 11 | constructor(public uniqueIdentifier: string, public identifier: Buffer, public pairingId: string, public publicKey: Buffer, public encryptionKey: Buffer) { 12 | 13 | } 14 | 15 | /** 16 | * Parse a credentials string into a Credentials object. 17 | * @param text The credentials string. 18 | * @returns A credentials object. 19 | */ 20 | static parse(text: string): Credentials { 21 | let parts = text.split(':'); 22 | return new Credentials( 23 | parts[0], 24 | Buffer.from(parts[1], 'hex'), 25 | Buffer.from(parts[2], 'hex').toString(), 26 | Buffer.from(parts[3], 'hex'), 27 | Buffer.from(parts[4], 'hex') 28 | ); 29 | } 30 | 31 | /** 32 | * Returns a string representation of a Credentials object. 33 | * @returns A string representation of a Credentials object. 34 | */ 35 | toString(): string { 36 | return this.uniqueIdentifier 37 | + ":" 38 | + this.identifier.toString('hex') 39 | + ":" 40 | + Buffer.from(this.pairingId).toString('hex') 41 | + ":" 42 | + this.publicKey.toString('hex') 43 | + ":" 44 | + this.encryptionKey.toString('hex'); 45 | } 46 | 47 | encrypt(message: Buffer): Buffer { 48 | let nonce = number.UInt53toBufferLE(this.encryptCount++) 49 | 50 | return Buffer.concat(encryption.encryptAndSeal(message, null, nonce, this.writeKey)); 51 | } 52 | 53 | decrypt(message: Buffer): Buffer { 54 | let nonce = number.UInt53toBufferLE(this.decryptCount++); 55 | let cipherText = message.slice(0, -16); 56 | let hmac = message.slice(-16); 57 | 58 | return encryption.verifyAndDecrypt(cipherText, hmac, null, nonce, this.readKey); 59 | } 60 | } -------------------------------------------------------------------------------- /src/lib/message.ts: -------------------------------------------------------------------------------- 1 | import { Message as ProtoMessage} from 'protobufjs'; 2 | 3 | export class Message { 4 | public type: Message.Type; 5 | public identifier: string; 6 | public payload: any; 7 | 8 | constructor(private message: ProtoMessage<{}>) { 9 | this.type = message['type']; 10 | this.identifier = message['identifier']; 11 | let keys = Object.keys(message.toJSON()).filter(key => { return key[0] == "."; }); 12 | if (keys.length > 0) { 13 | this.payload = message[keys[0]]; 14 | } 15 | } 16 | 17 | toObject(): any { 18 | return this.message; 19 | } 20 | } 21 | 22 | export module Message { 23 | export enum Type { 24 | SendCommandMessage = 1, 25 | CommandResultMessage = 2, 26 | GetStateMessage = 3, 27 | SetStateMessage = 4, 28 | SetArtworkMessage = 5, 29 | RegisterHidDeviceMessage = 6, 30 | RegisterHidDeviceResultMessage = 7, 31 | SendHidEventMessage = 8, 32 | SendHidReportMessage = 9, 33 | SendVirtualTouchEventMessage = 10, 34 | NotificationMessage = 11, 35 | ContentItemsChangedNotificationMessage = 12, 36 | DeviceInfoMessage = 15, 37 | ClientUpdatesConfigMessage = 16, 38 | VolumeControlAvailabilityMessage = 17, 39 | GameControllerMessage = 18, 40 | RegisterGameControllerMessage = 19, 41 | RegisterGameControllerResponseMessage = 20, 42 | UnregisterGameControllerMessage = 21, 43 | RegisterForGameControllerEventsMessage = 22, 44 | KeyboardMessage = 23, 45 | GetKeyboardSessionMessage = 24, 46 | TextInputMessage = 25, 47 | GetVoiceInputDevicesMessage = 26, 48 | GetVoiceInputDevicesResponseMessage = 27, 49 | RegisterVoiceInputDeviceMessage = 28, 50 | RegisterVoiceInputDeviceResponseMessage = 29, 51 | SetRecordingStateMessage = 30, 52 | SendVoiceInputMessage = 31, 53 | PlaybackQueueRequestMessage = 32, 54 | TransactionMessage = 33, 55 | CryptoPairingMessage = 34, 56 | GameControllerPropertiesMessage = 35, 57 | SetReadyStateMessage = 36, 58 | DeviceInfoUpdate = 37, 59 | SetDisconnectingStateMessage = 38, 60 | SendButtonEvent = 39, 61 | SetHiliteModeMessage = 40, 62 | WakeDeviceMessage = 41, 63 | GenericMessage = 42, 64 | SendPackedVirtualTouchEvent = 43, 65 | SendLyricsEvent = 44, 66 | PlaybackQueueCapabilitiesRequest = 45, 67 | ModifyOutputContextRequest = 46 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/now-playing-info.ts: -------------------------------------------------------------------------------- 1 | export class NowPlayingInfo { 2 | public duration: number; 3 | public elapsedTime: number; 4 | public title: string; 5 | public artist: string; 6 | public album: string; 7 | public appDisplayName: string; 8 | public appBundleIdentifier: string; 9 | public playbackState: NowPlayingInfo.State; 10 | public timestamp: number; 11 | 12 | constructor(public message: any) { 13 | let nowPlayingInfo = message.nowPlayingInfo; 14 | if (nowPlayingInfo) { 15 | this.duration = nowPlayingInfo.duration; 16 | this.elapsedTime = nowPlayingInfo.elapsedTime; 17 | this.title = nowPlayingInfo.title; 18 | this.artist = nowPlayingInfo.artist; 19 | this.album = nowPlayingInfo.album; 20 | this.timestamp = nowPlayingInfo.timestamp; 21 | } 22 | this.appDisplayName = message.displayName; 23 | this.appBundleIdentifier = message.displayID; 24 | if (message.playbackState == 2) { 25 | this.playbackState = NowPlayingInfo.State.Paused; 26 | } else if (message.playbackState == 1) { 27 | this.playbackState = NowPlayingInfo.State.Playing; 28 | } 29 | } 30 | 31 | public percentCompleted(): string { 32 | if (!this.elapsedTime || !this.duration) { return "0.00"; } 33 | 34 | return ((this.elapsedTime / this.duration) * 100).toPrecision(3); 35 | } 36 | 37 | public toString(): string { 38 | if (this.artist) { 39 | let album = this.album == null ? '' : " -- " + this.album + " "; 40 | return this.title + " by " + this.artist + album + " (" + this.percentCompleted() + "%) | " 41 | + this.appDisplayName + " (" + this.appBundleIdentifier + ") | " 42 | + this.playbackState; 43 | } else if (this.title) { 44 | return this.title + " (" + this.percentCompleted() + "%) | " 45 | + this.appDisplayName + " (" + this.appBundleIdentifier + ") | " 46 | + this.playbackState; 47 | } else { 48 | return this.appDisplayName + " (" + this.appBundleIdentifier + ") | " 49 | + this.playbackState; 50 | } 51 | } 52 | } 53 | 54 | export module NowPlayingInfo { 55 | export enum State { 56 | Playing = 'playing', 57 | Paused = 'paused' 58 | } 59 | } -------------------------------------------------------------------------------- /src/lib/pairing.ts: -------------------------------------------------------------------------------- 1 | import * as srp from 'fast-srp-hap'; 2 | import { v4 as uuid } from 'uuid'; 3 | import { load } from 'protobufjs'; 4 | import * as path from 'path'; 5 | import * as crypto from 'crypto'; 6 | import * as ed25519 from 'ed25519'; 7 | 8 | import { AppleTV } from './appletv'; 9 | import { Credentials } from './credentials'; 10 | import { Message } from './message'; 11 | import tlv from './util/tlv'; 12 | import enc from './util/encryption'; 13 | 14 | export class Pairing { 15 | private srp: srp.Client; 16 | 17 | private key: Buffer = crypto.randomBytes(32); 18 | private publicKey: Buffer; 19 | private proof: Buffer; 20 | 21 | private deviceSalt: Buffer; 22 | private devicePublicKey: Buffer; 23 | private deviceProof: Buffer; 24 | 25 | constructor(public device: AppleTV) { 26 | 27 | } 28 | 29 | /** 30 | * Initiates the pairing process 31 | * @returns A promise that resolves to a callback which takes in the pairing pin from the Apple TV. 32 | */ 33 | async initiatePair(): Promise<(pin: string) => Promise> { 34 | let that = this; 35 | let tlvData = tlv.encode( 36 | tlv.Tag.PairingMethod, 0x00, 37 | tlv.Tag.Sequence, 0x01, 38 | ); 39 | let requestMessage = { 40 | status: 0, 41 | isUsingSystemPairing: true, 42 | isRetrying: true, 43 | state: 2, 44 | pairingData: tlvData 45 | }; 46 | await this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', requestMessage, false); 47 | let message = await this.device.waitForSequence(0x02); 48 | let pairingData = message.payload.pairingData; 49 | let decodedData = tlv.decode(pairingData); 50 | 51 | if (decodedData[tlv.Tag.BackOff]) { 52 | let backOff: Buffer = decodedData[tlv.Tag.BackOff]; 53 | let seconds = backOff.readIntBE(0, backOff.byteLength); 54 | if (seconds > 0) { 55 | throw new Error("You've attempt to pair too recently. Try again in " + seconds + " seconds."); 56 | } 57 | } 58 | if (decodedData[tlv.Tag.ErrorCode]) { 59 | let buffer: Buffer = decodedData[tlv.Tag.ErrorCode]; 60 | throw new Error(this.device.name + " responded with error code " + buffer.readIntBE(0, buffer.byteLength) + ". Try rebooting your Apple TV."); 61 | } 62 | 63 | this.deviceSalt = decodedData[tlv.Tag.Salt]; 64 | this.devicePublicKey = decodedData[tlv.Tag.PublicKey]; 65 | 66 | if (this.deviceSalt.byteLength != 16) { 67 | throw new Error(`salt must be 16 bytes (but was ${this.deviceSalt.byteLength})`); 68 | } 69 | if (this.devicePublicKey.byteLength !== 384) { 70 | throw new Error(`serverPublicKey must be 384 bytes (but was ${this.devicePublicKey.byteLength})`); 71 | } 72 | 73 | return (pin: string) => { 74 | return that.completePairing(pin); 75 | }; 76 | } 77 | 78 | private async completePairing(pin: string): Promise { 79 | await this.sendThirdSequence(pin); 80 | let message = await this.device.waitForSequence(0x04); 81 | let pairingData = message.payload.pairingData; 82 | this.deviceProof = tlv.decode(pairingData)[tlv.Tag.Proof]; 83 | // console.log("DEBUG: Device Proof=" + this.deviceProof.toString('hex')); 84 | 85 | this.srp.checkM2(this.deviceProof); 86 | 87 | let seed = crypto.randomBytes(32); 88 | let keyPair = ed25519.MakeKeypair(seed); 89 | let privateKey = keyPair.privateKey; 90 | let publicKey = keyPair.publicKey; 91 | let sharedSecret = this.srp.computeK(); 92 | 93 | let deviceHash = enc.HKDF( 94 | "sha512", 95 | Buffer.from("Pair-Setup-Controller-Sign-Salt"), 96 | sharedSecret, 97 | Buffer.from("Pair-Setup-Controller-Sign-Info"), 98 | 32 99 | ); 100 | let deviceInfo = Buffer.concat([deviceHash, Buffer.from(this.device.pairingId), publicKey]); 101 | let deviceSignature = ed25519.Sign(deviceInfo, privateKey); 102 | let encryptionKey = enc.HKDF( 103 | "sha512", 104 | Buffer.from("Pair-Setup-Encrypt-Salt"), 105 | sharedSecret, 106 | Buffer.from("Pair-Setup-Encrypt-Info"), 107 | 32 108 | ); 109 | 110 | await this.sendFifthSequence(publicKey, deviceSignature, encryptionKey); 111 | let newMessage = await this.device.waitForSequence(0x06); 112 | let encryptedData = tlv.decode(newMessage.payload.pairingData)[tlv.Tag.EncryptedData]; 113 | let cipherText = encryptedData.slice(0, -16); 114 | let hmac = encryptedData.slice(-16); 115 | let decrpytedData = enc.verifyAndDecrypt(cipherText, hmac, null, Buffer.from('PS-Msg06'), encryptionKey); 116 | let tlvData = tlv.decode(decrpytedData); 117 | this.device.credentials = new Credentials( 118 | this.device.uid, 119 | tlvData[tlv.Tag.Username], 120 | this.device.pairingId, 121 | tlvData[tlv.Tag.PublicKey], 122 | seed 123 | ); 124 | 125 | return this.device; 126 | } 127 | 128 | private async sendThirdSequence(pin: string): Promise { 129 | this.srp = srp.Client( 130 | srp.params['3072'], 131 | this.deviceSalt, 132 | Buffer.from('Pair-Setup'), 133 | Buffer.from(pin), 134 | this.key 135 | ); 136 | this.srp.setB(this.devicePublicKey); 137 | this.publicKey = this.srp.computeA(); 138 | this.proof = this.srp.computeM1(); 139 | 140 | // console.log("DEBUG: Client Public Key=" + this.publicKey.toString('hex') + "\nProof=" + this.proof.toString('hex')); 141 | 142 | let tlvData = tlv.encode( 143 | tlv.Tag.Sequence, 0x03, 144 | tlv.Tag.PublicKey, this.publicKey, 145 | tlv.Tag.Proof, this.proof 146 | ); 147 | let message = { 148 | status: 0, 149 | pairingData: tlvData 150 | }; 151 | 152 | return await this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', message, false); 153 | } 154 | 155 | private async sendFifthSequence(publicKey: Buffer, signature: Buffer, encryptionKey: Buffer): Promise { 156 | let tlvData = tlv.encode( 157 | tlv.Tag.Username, Buffer.from(this.device.pairingId), 158 | tlv.Tag.PublicKey, publicKey, 159 | tlv.Tag.Signature, signature 160 | ); 161 | let encryptedTLV = Buffer.concat(enc.encryptAndSeal(tlvData, null, Buffer.from('PS-Msg05'), encryptionKey)); 162 | // console.log("DEBUG: Encrypted Data=" + encryptedTLV.toString('hex')); 163 | let outerTLV = tlv.encode( 164 | tlv.Tag.Sequence, 0x05, 165 | tlv.Tag.EncryptedData, encryptedTLV 166 | ); 167 | let nextMessage = { 168 | status: 0, 169 | pairingData: outerTLV 170 | }; 171 | 172 | return await this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', nextMessage, false); 173 | } 174 | } -------------------------------------------------------------------------------- /src/lib/protos/AudioBuffer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message AudioPacket { 4 | required int32 startOffset = 1; 5 | required int32 variableFramesInPacket = 2; 6 | required int32 dataByteSize = 3; 7 | } 8 | 9 | message AudioBuffer { 10 | required AudioFormatSettings formatSettings = 1; 11 | optional int32 packetCount = 2; 12 | optional int32 maximumPacketSize = 3; 13 | optional int32 packetCapacity = 4; 14 | required bytes contents = 5; 15 | repeated AudioPacket packetDescriptions = 6; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/protos/AudioFormatSettings.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message AudioFormatSettings { 4 | optional bytes formatSettingsPlistData = 1; 5 | } -------------------------------------------------------------------------------- /src/lib/protos/ClientUpdatesConfigMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional ClientUpdatesConfigMessage clientUpdatesConfigMessage = 21; 7 | } 8 | 9 | message ClientUpdatesConfigMessage { 10 | optional bool artworkUpdates = 1; 11 | optional bool nowPlayingUpdates = 2; 12 | optional bool volumeUpdates = 3; 13 | optional bool keyboardUpdates = 4; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/lib/protos/CommandInfo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | enum Command { 4 | Unknown=0; 5 | Play=1; 6 | Pause=2; 7 | TogglePlayPause=3; 8 | Stop=4; 9 | NextTrack=5; 10 | PreviousTrack=6; 11 | AdvanceShuffleMode=7; 12 | AdvanceRepeatMode=8; 13 | BeginFastForward=9; 14 | EndFastForward=10; 15 | BeginRewind=11; 16 | EndRewind=12; 17 | Rewind15Seconds=13; 18 | FastForward15Seconds=14; 19 | Rewind30Seconds=15; 20 | FastForward30Seconds=16; 21 | 22 | SkipForward=18; 23 | SkipBackward=19; 24 | ChangePlaybackRate=20; 25 | RateTrack=21; 26 | LikeTrack=22; 27 | DislikeTrack=23; 28 | BookmarkTrack=24; 29 | 30 | SeekToPlaybackPosition=45; 31 | ChangeRepeatMode=46; 32 | ChangeShuffleMode=47; 33 | 34 | EnableLanguageOption=53; 35 | DisableLanguageOption=54; 36 | 37 | NextChapter=25; 38 | PreviousChapter=26; 39 | NextAlbum=27; 40 | PreviousAlbum=28; 41 | NextPlaylist=29; 42 | PreviousPlaylist=30; 43 | BanTrack=31; 44 | AddTrackToWishList=32; 45 | RemoveTrackFromWishList=33; 46 | NextInContext=34; 47 | PreviousInContext=35; 48 | 49 | ResetPlaybackTimeout=41; 50 | SetPlaybackQueue=48; 51 | AddNowPlayingItemToLibrary=49; 52 | CreateRadioStation=50; 53 | AddItemToLibrary=51; 54 | InsertIntoPlaybackQueue=52; 55 | 56 | ReorderPlaybackQueue=55; 57 | RemoveFromPlaybackQueue=56; 58 | PlayItemInPlaybackQueue=57; 59 | } 60 | 61 | message CommandInfo { 62 | optional Command command = 1; 63 | optional bool enabled = 2; 64 | optional bool active = 3; 65 | repeated double preferredIntervals = 4; 66 | optional string localizedTitle = 5; 67 | optional float minimumRating = 6; 68 | optional float maximumRating = 7; 69 | repeated float supportedRates = 8; 70 | optional string localizedShortTitle = 9; 71 | optional int32 repeatMode = 10; 72 | optional int32 shuffleMode = 11; 73 | optional int32 presentationStyle = 12; 74 | optional int32 skipInterval = 13; 75 | optional int32 numAvailableSkips = 14; 76 | optional int32 skipFrequency = 15; 77 | optional int32 canScrub = 16; 78 | repeated int32 supportedPlaybackQueueTypes = 17; 79 | repeated string supportedCustomQueueIdentifiers = 18; 80 | repeated int32 supportedInsertionPositions = 19; 81 | optional bool supportsSharedQueue = 20; 82 | } -------------------------------------------------------------------------------- /src/lib/protos/CommandOptions.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message CommandOptions { 4 | optional string sourceId = 2; 5 | optional string mediaType = 3; 6 | optional bool externalPlayerCommand = 4; 7 | optional float skipInterval = 5; 8 | optional float playbackRate = 6; 9 | optional float rating = 7; 10 | optional bool negative = 8; 11 | optional double playbackPosition = 9; 12 | optional int32 repeatMode = 10; 13 | optional int32 shuffleMode = 11; 14 | optional uint64 trackID = 12; 15 | optional int64 radioStationID = 13; 16 | optional string radioStationHash = 14; 17 | optional bytes systemAppPlaybackQueueData = 15; 18 | optional string destinationAppDisplayID = 16; 19 | optional uint32 sendOptions = 17; 20 | optional bool requestDefermentToPlaybackQueuePosition = 18; 21 | optional string contextID = 19; 22 | optional bool shouldOverrideManuallyCuratedQueue = 20; 23 | optional string stationURL = 21; 24 | optional bool shouldBeginRadioPlayback = 22; 25 | optional int32 playbackQueueInsertionPosition = 23; 26 | optional string contentItemID = 24; 27 | optional int32 playbackQueueOffset = 25; 28 | optional int32 playbackQueueDestinationOffset = 26; 29 | optional bytes languageOption = 27; 30 | optional bytes playbackQueueContext = 28; 31 | optional string insertAfterContentItemID = 29; 32 | optional string nowPlayingContentItemID = 30; 33 | optional int32 replaceIntent = 31; 34 | } -------------------------------------------------------------------------------- /src/lib/protos/CommandResultMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional CommandResultMessage commandResultMessage = 7; 7 | } 8 | 9 | message CommandResultMessage { 10 | optional uint64 value = 1; 11 | } -------------------------------------------------------------------------------- /src/lib/protos/ContentItem.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ContentItemMetadata.proto"; 4 | import "LanguageOption.proto"; 5 | 6 | message ContentItem { 7 | optional string identifier = 1; 8 | optional ContentItemMetadata metadata = 2; 9 | optional bytes artworkData = 3; 10 | optional string info = 4; 11 | repeated LanguageOption availableLanguageOptions = 5; 12 | repeated LanguageOption currentLanguageOptions = 6; 13 | // optional Lyrics lyrics = 7; 14 | // repeated Sections sections = 8; 15 | optional string parentIdentifier = 9; 16 | optional string ancestorIdentifier = 10; 17 | optional string queueIdentifier = 11; 18 | optional string requestIdentifier = 12; 19 | optional int32 artworkDataWidth = 13; 20 | optional int32 artworkDataHeight = 14; 21 | } -------------------------------------------------------------------------------- /src/lib/protos/ContentItemMetadata.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ContentItemMetadata { 4 | enum MediaType { 5 | UnknownMediaType = 0; 6 | Audio = 1; 7 | Video = 2; 8 | } 9 | enum MediaSubType { 10 | UnknownMediaSubType = 0; 11 | Music = 1; 12 | Podcast = 4; 13 | AudioBook = 5; 14 | ITunesU = 6; 15 | } 16 | optional string title = 1; 17 | optional string subtitle = 2; 18 | optional bool isContainer = 3; 19 | optional bool isPlayable = 4; 20 | optional float playbackProgress = 5; 21 | optional string albumName = 6; 22 | optional string trackArtistName = 7; 23 | optional string albumArtistName = 8; 24 | optional string directorName = 9; 25 | optional int32 seasonNumber = 10; 26 | optional int32 episodeNumber = 11; 27 | optional double releaseDate = 12; 28 | optional int32 playCount = 13; 29 | optional double duration = 14; 30 | optional string localizedContentRating = 15; 31 | optional bool isExplicitItem = 16; 32 | optional int32 playlistType = 17; 33 | optional int32 radioStationType = 18; 34 | optional bool artworkAvailable = 19; 35 | 36 | optional bool infoAvailable = 21; 37 | optional bool languageOptionsAvailable = 22; 38 | optional int32 numberOfSections = 23; 39 | optional bool lyricsAvailable = 24; 40 | optional int32 editingStyleFlags = 25; 41 | optional bool isStreamingContent = 26; 42 | optional bool isCurrentlyPlaying = 27; 43 | optional string collectionIdentifier = 28; 44 | optional string profileIdentifier = 29; 45 | optional double startTime = 30; 46 | optional string artworkMIMEType = 31; 47 | optional string assetURLString = 32; 48 | optional string composer = 33; 49 | optional int32 discNumber = 34; 50 | optional double elapsedTime = 35; 51 | optional string genre = 36; 52 | optional bool isAlwaysLive = 37; 53 | 54 | optional float playbackRate = 39; 55 | optional int32 chapterCount = 40; 56 | optional int32 totalDiscCount = 41; 57 | optional int32 totalTrackCount = 42; 58 | optional int32 trackNumber = 43; 59 | optional string contentIdentifier = 44; 60 | 61 | optional bool isSharable = 46; 62 | 63 | optional bool isLiked = 48; 64 | optional bool isInWishList = 49; 65 | optional int64 radioStationIdentifier = 50; 66 | 67 | optional string radioStationName = 52; 68 | optional string radioStationString = 53; 69 | optional int64 iTunesStoreIdentifier = 54; 70 | optional int64 iTunesStoreSubscriptionIdentifier = 55; 71 | optional int64 iTunesStoreArtistIdentifier = 56; 72 | optional int64 iTunesStoreAlbumIdentifier = 57; 73 | optional bytes purchaseInfoData = 58; 74 | optional float defaultPlaybackRate = 59; 75 | optional int32 downloadState = 60; 76 | optional float downloadProgress = 61; 77 | optional bytes appMetricsData = 62; 78 | optional string seriesName = 63; 79 | optional MediaType mediaType = 64; 80 | optional MediaSubType mediaSubType = 65; 81 | 82 | optional bytes nowPlayingInfoData = 67; 83 | optional bytes userInfoData = 68; 84 | optional bool isSteerable = 69; 85 | optional string artworkURL = 70; 86 | optional string lyricsURL = 71; 87 | optional bytes deviceSpecificUserInfoData = 72; 88 | optional bytes collectionInfoData = 73; 89 | optional double elapsedTimeTimestamp = 74; 90 | optional double inferredTimestamp = 75; 91 | optional string serviceIdentifier = 76; 92 | optional int32 artworkDataWidth = 77; 93 | optional int32 artworkDataHeight = 78; 94 | optional bytes currentPlaybackDateData = 79; 95 | optional string artworkIdentifier = 80; 96 | optional bool isLoading = 81; 97 | optional bytes artworkURLTemplatesData = 82; 98 | optional int64 legacyUniqueIdentifier = 83; 99 | optional int32 episodeType = 84; 100 | optional string artworkFileURL = 85; 101 | optional string brandIdentifier = 86; 102 | optional string localizedDurationString = 87; 103 | } -------------------------------------------------------------------------------- /src/lib/protos/CryptoPairingMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional CryptoPairingMessage cryptoPairingMessage = 39; 7 | } 8 | 9 | message CryptoPairingMessage { 10 | optional bytes pairingData = 1; // Example: <00010006 0101> 11 | required int32 status = 2; // Example: 0 12 | optional bool isRetrying = 3; 13 | optional bool isUsingSystemPairing = 4; 14 | optional int32 state = 5; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/protos/DeviceInfoMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional DeviceInfoMessage deviceInfoMessage = 20; 7 | } 8 | 9 | message DeviceInfoMessage { 10 | required string uniqueIdentifier = 1; // Example: B8D8678C-9DA9-4D29-9338-5D6B827B8063 11 | required string name = 2; // Example: Jean's iPhone 12 | optional string localizedModelName = 3; // Example: iPhone 13 | required string systemBuildVersion = 4; // Example: 13F69 14 | required string applicationBundleIdentifier = 5; // Example: com.example.myremote 15 | optional string applicationBundleVersion = 6; // Example: 107 16 | required int32 protocolVersion = 7; // Example: 1 17 | optional int32 lastSupportedMessageType = 8; 18 | optional bool allowsPairing = 9; 19 | optional bool supportsSystemPairing = 10; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/protos/DeviceInfoUpdate.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional DeviceInfoUpdate deviceInfoUpdate = 20; 7 | } 8 | 9 | message DeviceInfoUpdate { 10 | required string uniqueIdentifier = 1; // Example: B8D8678C-9DA9-4D29-9338-5D6B827B8063 11 | required string name = 2; // Example: Jean's iPhone 12 | optional string localizedModelName = 3; // Example: iPhone 13 | required string systemBuildVersion = 4; // Example: 13F69 14 | required string applicationBundleIdentifier = 5; // Example: com.example.myremote 15 | optional string applicationBundleVersion = 6; // Example: 107 16 | required int32 protocolVersion = 7; // Example: 1 17 | optional int32 lastSupportedMessageType = 8; 18 | optional bool allowsPairing = 9; 19 | optional bool supportsSystemPairing = 10; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/protos/GetKeyboardSessionMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional string getKeyboardSessionMessage = 29; 7 | } 8 | 9 | message GetKeyboardSessionMessage { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/protos/GetStateMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional string getStateMessage = 8; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/protos/KeyboardMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "TextEditingAttributes.proto"; 5 | 6 | extend ProtocolMessage { 7 | optional KeyboardMessage keyboardMessage = 28; 8 | } 9 | 10 | message KeyboardMessage { 11 | optional int32 state = 1; 12 | optional TextEditingAttributes attributes = 3; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/protos/LanguageOption.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message LanguageOption { 4 | optional int32 type = 1; 5 | optional string languageTag = 2; 6 | repeated string characteristics = 3; 7 | optional string displayName = 4; 8 | optional string identifier = 5; 9 | } -------------------------------------------------------------------------------- /src/lib/protos/NotificationMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "PlayerPath.proto"; 5 | 6 | extend ProtocolMessage { 7 | optional NotificationMessage notificationMessage = 16; 8 | } 9 | 10 | message NotificationMessage { 11 | repeated string notification = 1; 12 | repeated bytes userInfos = 2; 13 | repeated PlayerPath playerPaths = 3; 14 | } -------------------------------------------------------------------------------- /src/lib/protos/NowPlayingClient.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message NowPlayingClient { 4 | optional int32 processIdentifier = 1; 5 | optional string bundleIdentifier = 2; 6 | optional string parentApplicationBundleIdentifier = 3; 7 | optional int32 processUserIdentifier = 4; 8 | optional int32 nowPlayingVisibility = 5; 9 | // optional TintColor tintColor = 6; 10 | optional string displayName = 7; 11 | } -------------------------------------------------------------------------------- /src/lib/protos/NowPlayingInfo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message NowPlayingInfo { 4 | enum RepeatMode { 5 | Unknown = 0; 6 | One = 1; 7 | All = 2; 8 | } 9 | 10 | enum ShuffleMode { 11 | Unkown = 0; 12 | Off = 1; 13 | Albums = 2; 14 | Songs = 3; 15 | } 16 | 17 | optional string album = 1; 18 | optional string artist = 2; 19 | optional double duration = 3; 20 | optional double elapsedTime = 4; 21 | optional float playbackRate = 5; 22 | optional RepeatMode repeatMode = 6; 23 | optional ShuffleMode shuffleMode = 7; 24 | optional double timestamp = 8; 25 | optional string title = 9; 26 | optional uint64 uniqueIdentifier = 10; 27 | optional bool isExplicitTrack = 11; 28 | optional bool isMusicApp = 12; 29 | optional int64 radioStationIdentifier = 13; 30 | optional string radioStationHash = 14; 31 | optional string radioStationName = 15; 32 | optional bytes artworkDataDigest = 16; 33 | optional bool isAlwaysLive = 17; 34 | optional bool isAdvertisement = 18; 35 | } -------------------------------------------------------------------------------- /src/lib/protos/NowPlayingPlayer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message NowPlayingPlayer { 4 | optional string identifier = 1; 5 | optional string displayName = 2; 6 | optional bool isDefaultPlayer = 3; 7 | } -------------------------------------------------------------------------------- /src/lib/protos/Origin.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "DeviceInfoMessage.proto"; 4 | 5 | message Origin { 6 | optional int32 type = 1; 7 | optional string displayName = 2; 8 | optional int32 identifier = 3; 9 | optional DeviceInfoMessage deviceInfo = 4; 10 | } -------------------------------------------------------------------------------- /src/lib/protos/PlaybackQueue.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ContentItem.proto"; 4 | import "PlaybackQueueContext.proto"; 5 | import "PlayerPath.proto"; 6 | 7 | message PlaybackQueue { 8 | optional int32 location = 1; 9 | repeated ContentItem contentItems = 2; 10 | optional PlaybackQueueContext context = 3; 11 | optional string requestId = 4; 12 | optional PlayerPath resolvedPlayerPath = 5; 13 | optional bool sendingPlaybackQueueTransaction = 6; 14 | optional string queueIdentifier = 7; 15 | } -------------------------------------------------------------------------------- /src/lib/protos/PlaybackQueueCapabilities.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message PlaybackQueueCapabilities { 4 | optional bool requestByRange = 1; 5 | optional bool requestByIdentifiers = 2; 6 | optional bool requestByRequest = 3; 7 | } -------------------------------------------------------------------------------- /src/lib/protos/PlaybackQueueContext.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message PlaybackQueueContext { 4 | optional string revision = 1; 5 | } -------------------------------------------------------------------------------- /src/lib/protos/PlaybackQueueRequestMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "PlaybackQueueContext.proto"; 5 | import "PlayerPath.proto"; 6 | 7 | extend ProtocolMessage { 8 | optional PlaybackQueueRequestMessage playbackQueueRequestMessage = 37; 9 | } 10 | 11 | message PlaybackQueueRequestMessage { 12 | optional int32 location = 1; 13 | optional int32 length = 2; 14 | optional bool includeMetadata = 3; 15 | optional double artworkWidth = 4; 16 | optional double artworkHeight = 5; 17 | optional bool includeLyrics = 6; 18 | optional bool includeSections = 7; 19 | optional bool includeInfo = 8; 20 | optional bool includeLanguageOptions = 9; 21 | optional PlaybackQueueContext context = 10; 22 | optional string requestID = 11; 23 | repeated string contentItemIdentifiers = 12; 24 | optional bool returnContentItemAssetsInUserCompletion = 13; 25 | optional PlayerPath playerPath = 14; 26 | optional int32 cachingPolicy = 15; 27 | optional string label = 16; 28 | optional bool isLegacyNowPlayingInfoRequest = 17; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/protos/PlayerPath.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "Origin.proto"; 4 | import "NowPlayingClient.proto"; 5 | import "NowPlayingPlayer.proto"; 6 | 7 | message PlayerPath { 8 | optional Origin origin = 1; 9 | optional NowPlayingClient client = 2; 10 | optional NowPlayingPlayer player = 3; 11 | } -------------------------------------------------------------------------------- /src/lib/protos/ProtocolMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message ProtocolMessage { 4 | extensions 6 to max; 5 | 6 | enum Type { 7 | SEND_COMMAND_MESSAGE = 1; 8 | COMMAND_RESULT_MESSAGE = 2; 9 | GET_STATE_MESSAGE = 3; 10 | SET_STATE_MESSAGE = 4; 11 | SET_ARTWORK_MESSAGE = 5; 12 | REGISTER_HID_DEVICE_MESSAGE = 6; 13 | REGISTER_HID_DEVICE_RESULT_MESSAGE = 7; 14 | SEND_HID_EVENT_MESSAGE = 8; 15 | SEND_HID_REPORT_MESSAGE = 9; 16 | SEND_VIRTUAL_TOUCH_EVENT_MESSAGE = 10; 17 | NOTIFICATION_MESSAGE = 11; 18 | CONTENT_ITEMS_CHANGED_NOTIFICATION_MESSAGE = 12; 19 | DEVICE_INFO_MESSAGE = 15; 20 | CLIENT_UPDATES_CONFIG_MESSAGE = 16; 21 | VOLUME_CONTROL_AVAILABILITY_MESSAGE = 17; 22 | GAME_CONTROLLER_MESSAGE = 18; 23 | REGISTER_GAME_CONTROLLER_MESSAGE = 19; 24 | REGISTER_GAME_CONTROLLER_RESPONSE_MESSAGE = 20; 25 | UNREGISTER_GAME_CONTROLLER_MESSAGE = 21; 26 | REGISTER_FOR_GAME_CONTROLLER_EVENTS_MESSAGE = 22; 27 | KEYBOARD_MESSAGE = 23; 28 | GET_KEYBOARD_SESSION_MESSAGE = 24; 29 | TEXT_INPUT_MESSAGE = 25; 30 | GET_VOICE_INPUT_DEVICES_MESSAGE = 26; 31 | GET_VOICE_INPUT_DEVICES_RESPONSE_MESSAGE = 27; 32 | REGISTER_VOICE_INPUT_DEVICE_MESSAGE = 28; 33 | REGISTER_VOICE_INPUT_DEVICE_RESPONSE_MESSAGE = 29; 34 | SET_RECORDING_STATE_MESSAGE = 30; 35 | SEND_VOICE_INPUT_MESSAGE = 31; 36 | PLAYBACK_QUEUE_REQUEST_MESSAGE = 32; 37 | TRANSACTION_MESSAGE = 33; 38 | CRYPTO_PAIRING_MESSAGE = 34; 39 | GAME_CONTROLLER_PROPERTIES_MESSAGE = 35; 40 | SET_READY_STATE_MESSAGE = 36; 41 | DEVICE_INFO_UPDATE = 37; 42 | SET_DISCONNECTING_STATE_MESSAGE = 38; 43 | SEND_BUTTON_EVENT = 39; 44 | SET_HILITE_MODE_MESSAGE = 40; 45 | WAKE_DEVICE_MESSAGE = 41; 46 | GENERIC_MESSAGE = 42; 47 | SEND_PACKED_VIRTUAL_TOUCH_EVENT = 43; 48 | SEND_LYRICS_EVENT = 44; 49 | PLAYBACK_QUEUE_CAPABILITIES_REQUEST = 45; 50 | MODIFY_OUTPUT_CONTEXT_REQUEST = 46; 51 | } 52 | 53 | required Type type = 1; // Identifies which underlying message is filled in. 54 | optional string identifier = 2; 55 | optional int32 priority = 4; 56 | 57 | // One of the following will be filled in. 58 | // optional SendCommandMessage sendCommandMessage = 6; 59 | // optional SendCommandResultMessage sendCommandResultMessage = 7; 60 | // optional SetStateMessage setStateMessage = 9; 61 | // optional SetArtworkMessage setArtworkMessage = 10; 62 | // optional RegisterHIDDeviceMessage registerHIDDeviceMessage = 11; 63 | // optional RegisterHIDDeviceResultMessage registerHIDDeviceResultMessage = 12; 64 | // optional SendHIDEventMessage sendHIDEventMessage = 13; 65 | // optional SendVirtualTouchEventMessage sendVirtualTouchEventMessage = 15; 66 | // optional NotificationMessage notificationMessage = 16; 67 | // optional ContentItemsChangedNotificationMessage contentItemsChangedNotificationMessage = 17; 68 | // optional DeviceInfoMessage deviceInfoMessage = 20; 69 | // optional ClientUpdatesConfigMessage clientUpdatesConfigMessage = 21; 70 | // optional VolumeControlAvailabilityMessage volumeControlAvailabilityMessage = 22; 71 | // optional GameControllerMessage gameControllerMessage = 23; 72 | // optional RegisterGameControllerMessage registerGameControllerMessage = 24; 73 | // optional RegisterGameControllerResponseMessage registerGameControllerResponseMessage = 25; 74 | // optional UnregisterGameControllerMessage unregisterGameControllerMessage = 26; 75 | // optional RegisterForGameControllerEventsMessage registerForGameControllerEventsMessage = 27; 76 | // optional KeyboardMessage keyboardMessage = 28; 77 | // optional GetKeyboardSessionMessage getKeyboardSessionMessage = 29; 78 | // optional TextInputMessage textInputMessage = 30; 79 | // optional GetVoiceInputDevicesMessage getVoiceInputDevicesMessage = 31; 80 | // optional GetVoiceInputDevicesResponseMessage getVoiceInputDevicesResponseMessage = 32; 81 | // optional RegisterVoiceInputDeviceMessage registerVoiceInputDeviceMessage = 33; 82 | // optional RegisterVoiceInputDeviceResponseMessage registerVoiceInputDeviceResponseMessage = 34; 83 | // optional SetRecordingStateMessage setRecordingStateMessage = 35; 84 | // optional SendVoiceInputMessage sendVoiceInputMessage = 36; 85 | // optional GetPlaybackQueueMessage getPlaybackQueueMessage = 37; 86 | // optional TransactionMessage transactionMessage = 38; 87 | // optional CryptoPairingMessage cryptoPairingMessage = 39; 88 | // optional GameControllerPropertiesMessage gameControllerPropertiesMessage = 40; 89 | // optional SetReadyStateMessage setReadyStateMessage = 41; 90 | // optional SendButtonEventMessage sendButtonEventMessage = 43; 91 | // optional SetHiliteModeMessage setHiliteModeMessage = 44; 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/protos/RegisterForGameControllerEventsMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional RegisterForGameControllerEventsMessage registerForGameControllerEventsMessage = 27; 7 | } 8 | 9 | message RegisterForGameControllerEventsMessage { 10 | optional int32 inputModeFlags = 1; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/protos/RegisterHIDDeviceMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "VirtualTouchDeviceDescriptor.proto"; 5 | 6 | extend ProtocolMessage { 7 | optional RegisterHIDDeviceMessage registerHIDDeviceMessage = 11; 8 | } 9 | 10 | message RegisterHIDDeviceMessage { 11 | optional VirtualTouchDeviceDescriptor deviceDescriptor = 1; 12 | } -------------------------------------------------------------------------------- /src/lib/protos/RegisterHIDDeviceResultMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional RegisterHIDDeviceResultMessage registerHIDDeviceResultMessage = 12; 7 | } 8 | 9 | message RegisterHIDDeviceResultMessage { 10 | optional int32 errorCode = 1; 11 | optional int32 deviceIdentifier = 2; 12 | } -------------------------------------------------------------------------------- /src/lib/protos/RegisterVoiceInputDeviceMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "VoiceInputDeviceDescriptor.proto"; 5 | 6 | extend ProtocolMessage { 7 | optional RegisterVoiceInputDeviceMessage registerVoiceInputDeviceMessage = 33; 8 | } 9 | 10 | message RegisterVoiceInputDeviceMessage { 11 | optional VoiceInputDeviceDescriptor deviceDescriptor = 1; 12 | } -------------------------------------------------------------------------------- /src/lib/protos/RegisterVoiceInputDeviceResponseMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional RegisterVoiceInputDeviceResponseMessage registerVoiceInputDeviceResponseMessage = 34; 7 | } 8 | 9 | message RegisterVoiceInputDeviceResponseMessage { 10 | optional int32 deviceID = 1; 11 | optional int32 errorCode = 2; 12 | } -------------------------------------------------------------------------------- /src/lib/protos/SendButtonEventMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SendButtonEventMessage sendButtonEventMessage = 43; 7 | } 8 | 9 | message SendButtonEventMessage { 10 | optional uint32 usagePage = 1; 11 | optional uint32 usage = 2; 12 | optional bool buttonDown = 3; 13 | } -------------------------------------------------------------------------------- /src/lib/protos/SendCommandMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "CommandInfo.proto"; 5 | import "CommandOptions.proto"; 6 | import "PlayerPath.proto"; 7 | 8 | extend ProtocolMessage { 9 | optional SendCommandMessage sendCommandMessage = 6; 10 | } 11 | 12 | message SendCommandMessage { 13 | optional Command command = 1; 14 | optional CommandOptions options = 2; 15 | optional PlayerPath playerPath = 3; 16 | } -------------------------------------------------------------------------------- /src/lib/protos/SendHIDEventMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SendHIDEventMessage sendHIDEventMessage = 13; 7 | } 8 | 9 | message SendHIDEventMessage { 10 | // This data corresponds to a "keyboardEvent" in IOHIDEvent.h encoded as raw 11 | // data. Here is one source: 12 | // 13 | // https://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-308/IOHIDFamily/IOHIDEvent.h.auto.html 14 | // 15 | // The interesting parts are: 16 | // - usagePage (UInt32) 17 | // - usage (Uint32) 18 | // - down (bool) 19 | // 20 | // The parameters usagePage and usage corresponds to the key being pressed. 21 | // It is mapped to the USB HID values, which can be found here: 22 | // 23 | // https://github.com/Daij-Djan/DDHidLib/blob/master/usb_hid_usages.txt 24 | // 25 | // Pressing left key would for instance map to usagePage=0x01, usage=0x8B. In 26 | // the hid data, these values are stored as big endian uint16 values in the 27 | // mentioned order. So the same example would be: 0x0001008B0001, assuming 28 | // down = true (key being pressed). For each key press, the same usagePage 29 | // and usage are sent with down=true and down=false (key down + key up). 30 | // 31 | // There is a bit of magic in the raw data that's just not decoded yet, but 32 | // that doesn't matter. Just use this and it will work: 33 | // 34 | // 438922cf0802000000000000000000000100000000000000020000002000000003000000010000000000000000000000000001000000 35 | // 36 | // corresponds to the values above, e.g. 0001008B0001. The first 8 37 | // bytes is a timestamp (mach AbsoluteTime). It's a bit tricky to derive but 38 | // tvOS seems to accept old timestamps here. So it's probably fine to send 39 | // anything. 40 | optional bytes hidEventData = 1; 41 | } -------------------------------------------------------------------------------- /src/lib/protos/SendPackedVirtualTouchEventMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SendPackedVirtualTouchEventMessage sendPackedVirtualTouchEventMessage = 47; 7 | } 8 | 9 | message SendPackedVirtualTouchEventMessage { 10 | 11 | // Corresponds to "phase" in data 12 | enum Phase { 13 | Began = 1; 14 | Moved = 2; 15 | Stationary = 3; 16 | Ended = 4; 17 | Cancelled = 5; 18 | } 19 | 20 | // The packed version of VirtualTouchEvent contains X, Y, phase, deviceID 21 | // and finger stored as a byte array. Each value is written as 16bit little 22 | // endian integers. 23 | optional bytes data = 1; 24 | } -------------------------------------------------------------------------------- /src/lib/protos/SendVirtualTouchEventMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "VirtualTouchEvent.proto"; 5 | 6 | extend ProtocolMessage { 7 | optional SendVirtualTouchEventMessage sendVirtualTouchEventMessage = 15; 8 | } 9 | 10 | message SendVirtualTouchEventMessage { 11 | optional int32 deviceIdentifier = 1; 12 | optional VirtualTouchEvent event = 2; 13 | } -------------------------------------------------------------------------------- /src/lib/protos/SendVoiceInputMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "AudioFormatSettings.proto"; 5 | import "AudioBuffer.proto"; 6 | 7 | extend ProtocolMessage { 8 | optional SendVoiceInputMessage sendVoiceInputMessage = 36; 9 | } 10 | 11 | message VoiceInputTime { 12 | required float timestamp = 1; 13 | required float sampleRate = 2; 14 | } 15 | 16 | message VoiceInputDataBlock { 17 | required AudioBuffer buffer = 1; 18 | optional VoiceInputTime time = 2; 19 | optional float gain = 3; 20 | } 21 | 22 | message SendVoiceInputMessage { 23 | required VoiceInputDataBlock dataBlock = 1; 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/protos/SetArtworkMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SetArtworkMessage setArtworkMessage = 10; 7 | } 8 | 9 | message SetArtworkMessage { 10 | optional bytes jpegData = 1; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/protos/SetConnectionStateMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SetConnectionStateMessage setConnectionStateMessage = 42; 7 | } 8 | 9 | message SetConnectionStateMessage { 10 | enum ConnectionState { 11 | Connected = 2; 12 | } 13 | 14 | optional ConnectionState state = 1; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/protos/SetHiliteModeMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SetHiliteModeMessage setHiliteModeMessage = 44; 7 | } 8 | 9 | message SetHiliteModeMessage { 10 | optional bool hiliteMode = 1; 11 | } -------------------------------------------------------------------------------- /src/lib/protos/SetRecordingStateMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional SetRecordingStateMessage setRecordingStateMessage = 35; 7 | } 8 | 9 | message SetRecordingStateMessage { 10 | enum RecordingState { 11 | Recording = 1; 12 | NotRecording = 2; 13 | } 14 | 15 | required RecordingState state = 1; 16 | } -------------------------------------------------------------------------------- /src/lib/protos/SetStateMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | import "NowPlayingInfo.proto"; 5 | import "SupportedCommands.proto"; 6 | import "PlayerPath.proto"; 7 | import "PlaybackQueue.proto"; 8 | import "PlaybackQueueRequestMessage.proto"; 9 | import "PlaybackQueueCapabilities.proto"; 10 | 11 | extend ProtocolMessage { 12 | optional SetStateMessage setStateMessage = 9; 13 | } 14 | 15 | message SetStateMessage { 16 | enum PlaybackState { 17 | Unknown = 0; 18 | Playing = 1; 19 | Paused = 2; 20 | Stopped = 3; 21 | Interrupted = 4; 22 | Seeking = 5; 23 | } 24 | 25 | optional NowPlayingInfo nowPlayingInfo = 1; 26 | optional SupportedCommands supportedCommands = 2; 27 | optional PlaybackQueue playbackQueue = 3; 28 | optional string displayID = 4; 29 | optional string displayName = 5; 30 | optional PlaybackState playbackState = 6; 31 | optional PlaybackQueueCapabilities playbackQueueCapabilities = 8; 32 | optional PlayerPath playerPath = 9; 33 | optional PlaybackQueueRequestMessage request = 10; 34 | optional double playbackStateTimestamp = 11; 35 | } -------------------------------------------------------------------------------- /src/lib/protos/SupportedCommands.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "CommandInfo.proto"; 4 | 5 | message SupportedCommands { 6 | repeated CommandInfo supportedCommands = 1; 7 | } -------------------------------------------------------------------------------- /src/lib/protos/TextEditingAttributes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "TextInputTraits.proto"; 4 | 5 | message TextEditingAttributes { 6 | optional string title = 1; 7 | optional string prompt = 2; 8 | optional TextInputTraits inputTraits = 3; 9 | } -------------------------------------------------------------------------------- /src/lib/protos/TextInputTraits.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message TextInputTraits { 4 | enum AutocapitalizationType { 5 | NONE = 0; 6 | WORDS = 1; 7 | SENTENCES = 2; 8 | CHARACTERS = 3; 9 | } 10 | 11 | enum KeyboardType { 12 | KEYBOARD_TYPE_DEFAULT = 0; 13 | ASCII_CAPABLE = 1; 14 | NUMBERS_AND_PUNCTUATION = 2; 15 | URL = 3; 16 | NUMBER_PAD = 4; 17 | PHONE_PAD = 5; 18 | NAME_PHONE_PAD = 6; 19 | EMAIL_ADDRESS = 7; 20 | DECIMAL_PAD = 8; 21 | TWITTER = 9; 22 | WEB_SEARCH = 10; 23 | // ALPHABET = 1; 24 | } 25 | 26 | enum ReturnKeyType { 27 | RETURN_KEY_DEFAULT = 0; 28 | GO = 1; 29 | GOOGLE = 2; 30 | JOIN = 3; 31 | NEXT = 4; 32 | ROUTE = 5; 33 | SEARCH = 6; 34 | SEND = 7; 35 | YAHOO = 8; 36 | DONE = 9; 37 | EMERGENCY_CALL = 10; 38 | CONTINUE = 11; 39 | } 40 | 41 | // optional AutocapitalizationType autocapitalizationType = ?; 42 | // optional bool autocorrection = ?; 43 | // repeated int64 PINEntrySeparatorIndexes = ?; 44 | // optional bool enablesReturnKeyAutomatically = ?; 45 | // optional KeyboardType keyboardType = ?; 46 | // optional ReturnKeyType returnKeyType = ?; 47 | // optional bool secureTextEntry = ?; 48 | // optional bool spellchecking = ?; 49 | // optional int32 validTextRangeLength = ?; 50 | // optional int32 validTextRangeLocation = ?; 51 | } -------------------------------------------------------------------------------- /src/lib/protos/TransactionKey.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message TransactionKey { 4 | optional string identifier = 1; 5 | optional bytes userData = 2; 6 | } -------------------------------------------------------------------------------- /src/lib/protos/TransactionMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "TransactionPackets.proto"; 4 | import "ProtocolMessage.proto"; 5 | import "PlayerPath.proto"; 6 | 7 | extend ProtocolMessage { 8 | optional TransactionMessage transactionMessage = 38; 9 | } 10 | 11 | message TransactionMessage { 12 | optional uint64 name = 1; 13 | optional TransactionPackets packets = 2; 14 | optional PlayerPath playerPath = 3; 15 | } -------------------------------------------------------------------------------- /src/lib/protos/TransactionPacket.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "TransactionKey.proto"; 4 | 5 | message TransactionPacket { 6 | optional TransactionKey key = 1; 7 | optional bytes packetData = 2; 8 | optional string identifier = 3; 9 | optional uint64 totalLength = 4; 10 | optional uint64 totalWritePosition = 5; 11 | } -------------------------------------------------------------------------------- /src/lib/protos/TransactionPackets.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "TransactionPacket.proto"; 4 | 5 | message TransactionPackets { 6 | repeated TransactionPacket packets = 1; 7 | } -------------------------------------------------------------------------------- /src/lib/protos/VirtualTouchDeviceDescriptor.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message VirtualTouchDeviceDescriptor { 4 | optional bool absolute = 1; 5 | optional bool integratedDisplay = 2; 6 | optional float screenSizeWidth = 3; 7 | optional float screenSizeHeight = 4; 8 | } -------------------------------------------------------------------------------- /src/lib/protos/VirtualTouchEvent.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message VirtualTouchEvent { 4 | optional double x = 1; 5 | optional double y = 2; 6 | optional int32 phase = 3; 7 | optional int32 finger = 4; 8 | } -------------------------------------------------------------------------------- /src/lib/protos/VoiceInputDeviceDescriptor.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "AudioFormatSettings.proto"; 4 | 5 | message VoiceInputDeviceDescriptor { 6 | optional AudioFormatSettings defaultFormat = 1; 7 | repeated AudioFormatSettings supportedFormats = 2; 8 | } -------------------------------------------------------------------------------- /src/lib/protos/VolumeControlAvailabilityMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional VolumeControlAvailabilityMessage volumeControlAvailabilityMessage = 22; 7 | } 8 | 9 | message VolumeControlAvailabilityMessage { 10 | optional bool volumeControlAvailable = 1; 11 | } -------------------------------------------------------------------------------- /src/lib/protos/WakeDeviceMessage.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | import "ProtocolMessage.proto"; 4 | 5 | extend ProtocolMessage { 6 | optional WakeDeviceMessage wakeDeviceMessage = 45; 7 | } 8 | 9 | message WakeDeviceMessage { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/supported-command.ts: -------------------------------------------------------------------------------- 1 | export class SupportedCommand { 2 | constructor(public command: keyof typeof SupportedCommand.Command, public enabled: boolean, public canScrub: boolean) {} 3 | } 4 | 5 | export module SupportedCommand { 6 | export enum Command { 7 | Play = 'Play', 8 | Pause = 'Pause', 9 | TogglePlayPause = 'TogglePlayPause', 10 | EnableLanguageOption = 'EnableLanguageOption', 11 | DisableLanguageOption = 'DisableLanguageOption', 12 | Stop = 'Stop', 13 | SkipForward = 'SkipForward', 14 | SkipBackward = 'SkipBackward', 15 | BeginFastForward = 'BeginFastForward', 16 | BeginRewind = 'BeginRewind', 17 | ChangePlaybackRate = 'ChangePlaybackRate', 18 | SeekToPlaybackPosition = 'SeekToPlaybackPosition', 19 | NextInContext = 'NextInContext', 20 | PreviousInContext = 'PreviousInContext' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/util/encryption.ts: -------------------------------------------------------------------------------- 1 | import { api as Sodium } from 'sodium'; 2 | import * as crypto from 'crypto'; 3 | 4 | import number from './number'; 5 | 6 | function computePoly1305(cipherText: Buffer, AAD: Buffer, nonce: Buffer, key: Buffer): Buffer { 7 | if (AAD == null) { 8 | AAD = Buffer.alloc(0); 9 | } 10 | 11 | const msg = 12 | Buffer.concat([ 13 | AAD, 14 | getPadding(AAD, 16), 15 | cipherText, 16 | getPadding(cipherText, 16), 17 | number.UInt53toBufferLE(AAD.length), 18 | number.UInt53toBufferLE(cipherText.length) 19 | ]) 20 | 21 | const polyKey = Sodium.crypto_stream_chacha20(32, nonce, key); 22 | const computed_hmac = Sodium.crypto_onetimeauth(msg, polyKey); 23 | polyKey.fill(0); 24 | 25 | return computed_hmac; 26 | } 27 | 28 | // i'd really prefer for this to be a direct call to 29 | // Sodium.crypto_aead_chacha20poly1305_decrypt() 30 | // but unfortunately the way it constructs the message to 31 | // calculate the HMAC is not compatible with homekit 32 | // (long story short, it uses [ AAD, AAD.length, CipherText, CipherText.length ] 33 | // whereas homekit expects [ AAD, CipherText, AAD.length, CipherText.length ] 34 | function verifyAndDecrypt(cipherText: Buffer, mac: Buffer, AAD: Buffer, nonce: Buffer, key: Buffer): Buffer { 35 | const matches = 36 | Sodium.crypto_verify_16( 37 | mac, 38 | computePoly1305(cipherText, AAD, nonce, key) 39 | ); 40 | 41 | if (matches === 0) { 42 | return Sodium 43 | .crypto_stream_chacha20_xor_ic(cipherText, nonce, 1, key); 44 | } 45 | 46 | return null; 47 | } 48 | 49 | // See above about calling directly into libsodium. 50 | function encryptAndSeal(plainText: Buffer, AAD: Buffer, nonce: Buffer, key: Buffer): Buffer[] { 51 | const cipherText = 52 | Sodium 53 | .crypto_stream_chacha20_xor_ic(plainText, nonce, 1, key); 54 | 55 | const hmac = 56 | computePoly1305(cipherText, AAD, nonce, key); 57 | 58 | return [ cipherText, hmac ]; 59 | } 60 | 61 | function getPadding(buffer, blockSize) { 62 | return buffer.length % blockSize === 0 63 | ? Buffer.alloc(0) 64 | : Buffer.alloc(blockSize - (buffer.length % blockSize)) 65 | } 66 | 67 | function HKDF(hashAlg: string, salt: Buffer, ikm: Buffer, info: Buffer, size: number): Buffer { 68 | // create the hash alg to see if it exists and get its length 69 | var hash = crypto.createHash(hashAlg); 70 | var hashLength = hash.digest().length; 71 | 72 | // now we compute the PRK 73 | var hmac = crypto.createHmac(hashAlg, salt); 74 | hmac.update(ikm); 75 | var prk = hmac.digest(); 76 | 77 | var prev = Buffer.alloc(0); 78 | var output; 79 | var buffers = []; 80 | var num_blocks = Math.ceil(size / hashLength); 81 | info = Buffer.from(info); 82 | 83 | for (var i=0; i 8 | * 9 | * Modifications copyright Zach Bean 10 | * * Reformatted for ES6-style module 11 | * * renamed *UInt64* to *UInt53* to be more clear about range 12 | * * renamed uintHighLow to be more clear about what it does 13 | * * Refactored to return a buffer rather write into a passed-in buffer 14 | */ 15 | 16 | function splitUInt53(number) { 17 | const MAX_UINT32 = 0x00000000FFFFFFFF 18 | const MAX_INT53 = 0x001FFFFFFFFFFFFF 19 | 20 | assert(number > -1 && number <= MAX_INT53, "number out of range") 21 | assert(Math.floor(number) === number, "number must be an integer") 22 | 23 | var high = 0 24 | var signbit = number & 0xFFFFFFFF 25 | var low = signbit < 0 ? (number & 0x7FFFFFFF) + 0x80000000 : signbit 26 | 27 | if (number > MAX_UINT32) { 28 | high = (number - low) / (MAX_UINT32 + 1) 29 | } 30 | return [ high, low ] 31 | } 32 | 33 | function UInt53toBufferLE(number: number): Buffer { 34 | const [ high, low ] = splitUInt53(number) 35 | 36 | const buf = Buffer.alloc(8); 37 | buf.writeUInt32LE(low, 0); 38 | buf.writeUInt32LE(high, 4); 39 | 40 | return buf; 41 | } 42 | 43 | function UInt16toBufferBE(number: number): Buffer { 44 | const buf = Buffer.alloc(2); 45 | buf.writeUInt16BE(number, 0) 46 | 47 | return buf; 48 | } 49 | 50 | export default { 51 | UInt53toBufferLE, 52 | UInt16toBufferBE 53 | } -------------------------------------------------------------------------------- /src/lib/util/tlv.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type Length Value encoding/decoding, used by HAP as a wire format. 3 | * https://en.wikipedia.org/wiki/Type-length-value 4 | * 5 | * Originally based on code from github:KhaosT/HAP-NodeJS@0c8fd88 used 6 | * used per the terms of the Apache Software License v2. 7 | * 8 | * Original code copyright Khaos Tian 9 | * 10 | * Modifications copyright Zach Bean 11 | * * Reformatted for ES6-style module 12 | * * Rewrote encode() to be non-recursive; also simplified the logic 13 | * * Rewrote decode() 14 | */ 15 | 16 | const Tag = { 17 | PairingMethod: 0x00, 18 | Username: 0x01, 19 | Salt: 0x02, // salt is 16 bytes long 20 | 21 | // could be either the SRP client public key (384 bytes) or the ED25519 public key (32 bytes), depending on context 22 | PublicKey: 0x03, 23 | Proof: 0x04, // 64 bytes 24 | EncryptedData: 0x05, 25 | Sequence: 0x06, 26 | ErrorCode: 0x07, 27 | BackOff: 0x08, 28 | Signature: 0x0A, // 64 bytes 29 | 30 | MFiCertificate: 0x09, 31 | MFiSignature: 0x0A 32 | }; 33 | 34 | function encode(type, data, ...args: any[]): Buffer { 35 | 36 | var encodedTLVBuffer = Buffer.alloc(0); 37 | 38 | // coerce data to Buffer if needed 39 | if (typeof data === 'number') 40 | data = Buffer.from([data]); 41 | else if (typeof data === 'string') 42 | data = Buffer.from(data); 43 | 44 | if (data.length <= 255) { 45 | encodedTLVBuffer = Buffer.concat([Buffer.from([type,data.length]),data]); 46 | } else { 47 | var leftLength = data.length; 48 | var tempBuffer = Buffer.alloc(0); 49 | var currentStart = 0; 50 | 51 | for (; leftLength > 0;) { 52 | if (leftLength >= 255) { 53 | tempBuffer = Buffer.concat([tempBuffer,Buffer.from([type,0xFF]),data.slice(currentStart, currentStart + 255)]); 54 | leftLength -= 255; 55 | currentStart = currentStart + 255; 56 | } else { 57 | tempBuffer = Buffer.concat([tempBuffer,Buffer.from([type,leftLength]),data.slice(currentStart, currentStart + leftLength)]); 58 | leftLength -= leftLength; 59 | } 60 | } 61 | 62 | encodedTLVBuffer = tempBuffer; 63 | } 64 | 65 | // do we have more to encode? 66 | if (arguments.length > 2) { 67 | 68 | // chop off the first two arguments which we already processed, and process the rest recursively 69 | var remainingArguments = Array.prototype.slice.call(arguments, 2); 70 | var remainingTLVBuffer = encode.apply(this, remainingArguments); 71 | 72 | // append the remaining encoded arguments directly to the buffer 73 | encodedTLVBuffer = Buffer.concat([encodedTLVBuffer, remainingTLVBuffer]); 74 | } 75 | 76 | return encodedTLVBuffer; 77 | } 78 | 79 | function decode(data): {} { 80 | 81 | var objects = {}; 82 | 83 | var leftLength = data.length; 84 | var currentIndex = 0; 85 | 86 | for (; leftLength > 0;) { 87 | var type = data[currentIndex]; 88 | var length = data[currentIndex+1]; 89 | currentIndex += 2; 90 | leftLength -= 2; 91 | 92 | var newData = data.slice(currentIndex, currentIndex+length); 93 | 94 | if (objects[type]) { 95 | objects[type] = Buffer.concat([objects[type],newData]); 96 | } else { 97 | objects[type] = newData; 98 | } 99 | 100 | currentIndex += length; 101 | leftLength -= length; 102 | } 103 | 104 | return objects; 105 | } 106 | 107 | export default { 108 | Tag, 109 | encode, 110 | decode 111 | } -------------------------------------------------------------------------------- /src/lib/verifier.ts: -------------------------------------------------------------------------------- 1 | import { load } from 'protobufjs'; 2 | import * as path from 'path'; 3 | import * as ed25519 from 'ed25519'; 4 | import * as crypto from 'crypto'; 5 | import * as curve25519 from 'curve25519-n2'; 6 | 7 | import { AppleTV } from './appletv'; 8 | import { Credentials } from './credentials'; 9 | import { Message } from './message'; 10 | import tlv from './util/tlv'; 11 | import enc from './util/encryption'; 12 | 13 | type PairingData = { 14 | sessionPublicKey: Buffer; 15 | sharedSecret: Buffer; 16 | encryptionKey: Buffer; 17 | pairingData: Buffer; 18 | } 19 | 20 | export class Verifier { 21 | constructor(public device: AppleTV) { 22 | 23 | } 24 | 25 | async verify(): Promise<{}> { 26 | var verifyPrivate = Buffer.alloc(32); 27 | curve25519.makeSecretKey(verifyPrivate); 28 | let verifyPublic = curve25519.derivePublicKey(verifyPrivate); 29 | let { sessionPublicKey, encryptionKey, sharedSecret, pairingData } = await this.requestPairingData(verifyPublic, verifyPrivate); 30 | 31 | let tlvData = tlv.decode(pairingData); 32 | let identifier = tlvData[tlv.Tag.Username]; 33 | let signature = tlvData[tlv.Tag.Signature]; 34 | 35 | if (!identifier.equals(this.device.credentials.identifier)) { 36 | throw new Error("Identifier mismatch"); 37 | } 38 | 39 | let deviceInfo = Buffer.concat([sessionPublicKey, Buffer.from(identifier), verifyPublic]); 40 | if (!ed25519.Verify(deviceInfo, signature, this.device.credentials.publicKey)) { 41 | throw new Error("Signature verification failed"); 42 | } 43 | 44 | return await this.completeVerification(verifyPublic, sessionPublicKey, encryptionKey, sharedSecret); 45 | } 46 | 47 | private async requestPairingData(verifyPublic: Buffer, verifyPrivate: Buffer): Promise { 48 | let encodedData = tlv.encode( 49 | tlv.Tag.Sequence, 0x01, 50 | tlv.Tag.PublicKey, verifyPublic 51 | ); 52 | let message = { 53 | status: 0, 54 | state: 3, 55 | isRetrying: true, 56 | isUsingSystemPairing: true, 57 | pairingData: encodedData 58 | }; 59 | 60 | await this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', message, false); 61 | let pairingDataResponse = await this.device.waitForSequence(0x02); 62 | let pairingData = pairingDataResponse.payload.pairingData; 63 | let decodedData = tlv.decode(pairingData); 64 | let sessionPublicKey = decodedData[tlv.Tag.PublicKey]; 65 | let encryptedData = decodedData[tlv.Tag.EncryptedData]; 66 | 67 | if (sessionPublicKey.length != 32) { 68 | throw new Error(`sessionPublicKey must be 32 bytes (but was ${sessionPublicKey.length})`); 69 | } 70 | 71 | let cipherText = encryptedData.slice(0, -16); 72 | let hmac = encryptedData.slice(-16); 73 | let sharedSecret = curve25519.deriveSharedSecret(verifyPrivate, sessionPublicKey); 74 | let encryptionKey = enc.HKDF( 75 | "sha512", 76 | Buffer.from("Pair-Verify-Encrypt-Salt"), 77 | sharedSecret, 78 | Buffer.from("Pair-Verify-Encrypt-Info"), 79 | 32 80 | ); 81 | let decryptedData = enc.verifyAndDecrypt(cipherText, hmac, null, Buffer.from('PV-Msg02'), encryptionKey); 82 | 83 | return { 84 | sessionPublicKey: sessionPublicKey, 85 | sharedSecret: sharedSecret, 86 | encryptionKey: encryptionKey, 87 | pairingData: decryptedData 88 | }; 89 | } 90 | 91 | private async completeVerification(verifyPublic: Buffer, sessionPublicKey: Buffer, encryptionKey: Buffer, sharedSecret: Buffer): Promise<{}> { 92 | let material = Buffer.concat([verifyPublic, Buffer.from(this.device.credentials.pairingId), sessionPublicKey]); 93 | let keyPair = ed25519.MakeKeypair(this.device.credentials.encryptionKey); 94 | let signed = ed25519.Sign(material, keyPair); 95 | let plainTLV = tlv.encode( 96 | tlv.Tag.Username, Buffer.from(this.device.credentials.pairingId), 97 | tlv.Tag.Signature, signed 98 | ); 99 | let encryptedTLV = Buffer.concat(enc.encryptAndSeal(plainTLV, null, Buffer.from('PV-Msg03'), encryptionKey)); 100 | let tlvData = tlv.encode( 101 | tlv.Tag.Sequence, 0x03, 102 | tlv.Tag.EncryptedData, encryptedTLV 103 | ); 104 | let message = { 105 | status: 0, 106 | state: 3, 107 | isRetrying: false, 108 | isUsingSystemPairing: true, 109 | pairingData: tlvData 110 | }; 111 | 112 | await this.device.sendMessage('CryptoPairingMessage', 'CryptoPairingMessage', message, false); 113 | await this.device.waitForSequence(0x04); 114 | let readKey = enc.HKDF( 115 | "sha512", 116 | Buffer.from("MediaRemote-Salt"), 117 | sharedSecret, 118 | Buffer.from("MediaRemote-Read-Encryption-Key"), 119 | 32 120 | ); 121 | let writeKey = enc.HKDF( 122 | "sha512", 123 | Buffer.from("MediaRemote-Salt"), 124 | sharedSecret, 125 | Buffer.from("MediaRemote-Write-Encryption-Key"), 126 | 32 127 | ); 128 | 129 | return { 130 | readKey: readKey, 131 | writeKey: writeKey 132 | }; 133 | } 134 | } -------------------------------------------------------------------------------- /src/test/appletv.spec.ts: -------------------------------------------------------------------------------- 1 | import { AppleTV } from '../lib/appletv'; 2 | import { Message } from '../lib/message'; 3 | import { expect } from 'chai'; 4 | import { Socket } from 'net'; 5 | import * as mdns from 'mdns'; 6 | import * as sinon from 'sinon'; 7 | import 'mocha'; 8 | 9 | describe('apple tv tests', function() { 10 | beforeEach(function() { 11 | let socket = new Socket({}); 12 | 13 | sinon.stub(socket, 'write'); 14 | sinon.stub(socket, 'connect').callsFake(function(port: number, host: string, callback: any) { 15 | callback(); 16 | }); 17 | 18 | this.device = new AppleTV({ 19 | addresses: ['127.0.0.1'], 20 | port: 12345, 21 | flags: 0, 22 | fullname: '', 23 | host: '', 24 | interfaceIndex: 0, 25 | networkInterface: '', 26 | replyDomain: '', 27 | type: null, 28 | txtRecord: { 29 | Name: "Mock Apple TV", 30 | UniqueIdentifier: "MockAppleTVUUID" 31 | } 32 | }, socket); 33 | 34 | this.fake = sinon.stub(this.device.connection, 'sendProtocolMessage'); 35 | this.device.connection.isOpen = true; 36 | 37 | this.sentMessages = function() { 38 | var messages = []; 39 | 40 | for (var i = 0; i < this.fake.callCount; i++) { 41 | messages.push(new Message(this.fake.getCall(i).args[0])); 42 | } 43 | 44 | return messages; 45 | } 46 | }); 47 | 48 | it('should send introduction', async function() { 49 | await this.device.openConnection(); 50 | 51 | let messages = this.sentMessages(); 52 | 53 | expect(messages.length).to.equal(1); 54 | expect(messages[0].type).to.equal(Message.Type.DeviceInfoMessage); 55 | }); 56 | 57 | it('should request artwork', async function() { 58 | let width = 640; 59 | let height = 480; 60 | await this.device.openConnection(); 61 | try { 62 | await this.device.requestArtwork(width, height); 63 | } catch (error) {} 64 | 65 | let messages = this.sentMessages(); 66 | 67 | expect(messages.length).to.equal(2); 68 | expect(messages[1].type).to.equal(Message.Type.PlaybackQueueRequestMessage); 69 | expect(messages[1].payload.artworkWidth).to.equal(width); 70 | expect(messages[1].payload.artworkHeight).to.equal(height); 71 | expect(messages[1].payload.length).to.equal(1); 72 | expect(messages[1].payload.location).to.equal(0); 73 | }); 74 | 75 | it('should press and release menu', async function() { 76 | await this.device.openConnection(); 77 | await this.device.sendKeyCommand(AppleTV.Key.Menu); 78 | 79 | let messages = this.sentMessages(); 80 | 81 | expect(messages.length).to.equal(3); 82 | expect(messages[1].type).to.equal(Message.Type.SendHidEventMessage); 83 | expect(messages[2].type).to.equal(Message.Type.SendHidEventMessage); 84 | }); 85 | 86 | it('should read now playing', async function() { 87 | await this.device.openConnection(); 88 | 89 | var spy = sinon.spy(); 90 | this.device.on('nowPlaying', spy); 91 | this.device.connection.emit('message', require('./fixtures/now-playing.json')); 92 | 93 | let messages = this.sentMessages(); 94 | 95 | expect(messages.length).to.equal(1); 96 | expect(spy.lastCall.lastArg.title).to.equal('Seinfeld'); 97 | expect(spy.lastCall.lastArg.appDisplayName).to.equal('Hulu'); 98 | expect(spy.lastCall.lastArg.appBundleIdentifier).to.equal('com.hulu.plus'); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/test/browser.spec.ts: -------------------------------------------------------------------------------- 1 | import { Browser } from '../lib/browser'; 2 | import { AppleTV } from '../lib/appletv'; 3 | import { expect } from 'chai'; 4 | import * as mdns from 'mdns'; 5 | import 'mocha'; 6 | 7 | const AppleTVName = "Test Apple TV"; 8 | const AppleTVIdentifier = "TestAppleTVIdentifier"; 9 | 10 | describe('apple tv discovery', function() { 11 | it('should discover apple tv', async function() { 12 | this.timeout(5000); 13 | 14 | let ad = mdns.createAdvertisement(mdns.tcp('mediaremotetv'), 54321, { 15 | name: AppleTVName, 16 | txtRecord: { 17 | Name: AppleTVName, 18 | UniqueIdentifier: AppleTVIdentifier 19 | } 20 | }); 21 | ad.start(); 22 | 23 | let browser = new Browser(); 24 | let devices = await browser.scan(AppleTVIdentifier); 25 | 26 | expect(devices.length).to.be.greaterThan(0); 27 | 28 | let device = devices[0]; 29 | 30 | expect(device.uid).to.equal(AppleTVIdentifier); 31 | expect(device.name).to.equal(AppleTVName); 32 | 33 | ad.stop(); 34 | }); 35 | }); 36 | 37 | // describe('apple tv pairing', function() { 38 | // beforeEach(function() { this.mitm = Mitm(); }); 39 | // afterEach(function() { this.mitm.disable(); }); 40 | 41 | // it('should pair with apple tv', async function() { 42 | // this.mitm.on("connection", function(socket) { console.log("Hello back!") }); 43 | 44 | // this.timeout(10000); 45 | 46 | // let server = new MockServer(); 47 | // let device = await server.device; 48 | 49 | // await device.openConnection(); 50 | 51 | // let callback = await device.pair(); 52 | 53 | // }); 54 | // }); 55 | -------------------------------------------------------------------------------- /src/test/encryption.spec.ts: -------------------------------------------------------------------------------- 1 | import enc from '../lib/util/encryption'; 2 | import * as crypto from 'crypto'; 3 | import { expect } from 'chai'; 4 | import 'mocha'; 5 | 6 | describe('test encryption', function() { 7 | it('should encrypt and decrypt string', function() { 8 | let value = Buffer.from("some string"); 9 | let nonce = Buffer.from('PS-Msg06'); 10 | let key = enc.HKDF( 11 | "sha512", 12 | Buffer.from("Pair-Setup-Encrypt-Salt"), 13 | crypto.randomBytes(32), 14 | Buffer.from("Pair-Setup-Encrypt-Info"), 15 | 32 16 | ); 17 | let encrypted = enc.encryptAndSeal(value, null, nonce, key); 18 | let decrypted = enc.verifyAndDecrypt(encrypted[0], encrypted[1], null, nonce, key); 19 | 20 | expect(decrypted.toString()).to.equal(value.toString()); 21 | }); 22 | }); -------------------------------------------------------------------------------- /src/test/fixtures/now-playing.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": { 3 | "type": "SET_STATE_MESSAGE", 4 | "priority": 0, 5 | ".setStateMessage": { 6 | "nowPlayingInfo": { 7 | "duration": 4001.2299999999996, 8 | "elapsedTime": 3901.299601862999, 9 | "playbackRate": 1, 10 | "timestamp": 602989141.971391, 11 | "title": "Seinfeld", 12 | "uniqueIdentifier": "18446744073106635240", 13 | "isAlwaysLive": false 14 | }, 15 | "supportedCommands": { 16 | "supportedCommands": [ 17 | { 18 | "command": "Play", 19 | "enabled": true 20 | }, 21 | { 22 | "command": "Pause", 23 | "enabled": true 24 | }, 25 | { 26 | "command": "TogglePlayPause", 27 | "enabled": true 28 | }, 29 | { 30 | "command": "EnableLanguageOption", 31 | "enabled": true 32 | }, 33 | { 34 | "command": "DisableLanguageOption", 35 | "enabled": true 36 | }, 37 | { 38 | "command": "Stop", 39 | "enabled": true 40 | }, 41 | { 42 | "command": "SkipForward", 43 | "enabled": true, 44 | "preferredIntervals": [ 45 | 10 46 | ] 47 | }, 48 | { 49 | "command": "SkipBackward", 50 | "enabled": true, 51 | "preferredIntervals": [ 52 | 10 53 | ] 54 | }, 55 | { 56 | "command": "BeginFastForward", 57 | "enabled": true 58 | }, 59 | { 60 | "command": "BeginRewind", 61 | "enabled": true 62 | }, 63 | { 64 | "command": "ChangePlaybackRate", 65 | "enabled": true 66 | }, 67 | { 68 | "command": "SeekToPlaybackPosition", 69 | "enabled": true, 70 | "canScrub": 0 71 | }, 72 | { 73 | "command": "NextInContext", 74 | "enabled": true 75 | }, 76 | { 77 | "command": "PreviousInContext", 78 | "enabled": true 79 | } 80 | ] 81 | }, 82 | "displayID": "com.hulu.plus", 83 | "displayName": "Hulu", 84 | "playbackState": "Playing", 85 | "playbackQueueCapabilities": {}, 86 | "playbackStateTimestamp": 602987248.872297 87 | } 88 | }, 89 | "type": 4, 90 | "identifier": "", 91 | "payload": { 92 | "nowPlayingInfo": { 93 | "duration": 4001.2299999999996, 94 | "elapsedTime": 3901.299601862999, 95 | "playbackRate": 1, 96 | "timestamp": 602989141.971391, 97 | "title": "Seinfeld", 98 | "uniqueIdentifier": "18446744073106635240", 99 | "isAlwaysLive": false 100 | }, 101 | "supportedCommands": { 102 | "supportedCommands": [ 103 | { 104 | "command": "Play", 105 | "enabled": true 106 | }, 107 | { 108 | "command": "Pause", 109 | "enabled": true 110 | }, 111 | { 112 | "command": "TogglePlayPause", 113 | "enabled": true 114 | }, 115 | { 116 | "command": "EnableLanguageOption", 117 | "enabled": true 118 | }, 119 | { 120 | "command": "DisableLanguageOption", 121 | "enabled": true 122 | }, 123 | { 124 | "command": "Stop", 125 | "enabled": true 126 | }, 127 | { 128 | "command": "SkipForward", 129 | "enabled": true, 130 | "preferredIntervals": [ 131 | 10 132 | ] 133 | }, 134 | { 135 | "command": "SkipBackward", 136 | "enabled": true, 137 | "preferredIntervals": [ 138 | 10 139 | ] 140 | }, 141 | { 142 | "command": "BeginFastForward", 143 | "enabled": true 144 | }, 145 | { 146 | "command": "BeginRewind", 147 | "enabled": true 148 | }, 149 | { 150 | "command": "ChangePlaybackRate", 151 | "enabled": true 152 | }, 153 | { 154 | "command": "SeekToPlaybackPosition", 155 | "enabled": true, 156 | "canScrub": 0 157 | }, 158 | { 159 | "command": "NextInContext", 160 | "enabled": true 161 | }, 162 | { 163 | "command": "PreviousInContext", 164 | "enabled": true 165 | } 166 | ] 167 | }, 168 | "displayID": "com.hulu.plus", 169 | "displayName": "Hulu", 170 | "playbackState": "Playing", 171 | "playbackQueueCapabilities": {}, 172 | "playbackStateTimestamp": 602987248.872297 173 | } 174 | } -------------------------------------------------------------------------------- /src/test/helpers/mock-server.ts: -------------------------------------------------------------------------------- 1 | import { createServer, Server } from 'net'; 2 | import { AppleTV } from '../../lib/appletv'; 3 | import { Connection } from '../../lib/connection'; 4 | import { Message } from '../../lib/message'; 5 | import * as mdns from 'mdns'; 6 | 7 | export class MockServer { 8 | public device: AppleTV; 9 | public message: Promise; 10 | 11 | private server: Server; 12 | 13 | constructor() { 14 | let port = 65416; 15 | this.device = new AppleTV({ 16 | addresses: ['127.0.0.1'], 17 | port: port, 18 | txtRecord: { 19 | Name: "Mock Apple TV", 20 | UniqueIdentifier: "MockAppleTVUUID" 21 | } 22 | } as mdns.Service); 23 | let d = this.device; 24 | let that = this; 25 | 26 | this.message = new Promise(function(resolve, reject) { 27 | that.server = createServer(function(socket) { 28 | let connection = new Connection(d, socket); 29 | d.connection = connection; 30 | d.on('message', function(message) { 31 | resolve(message); 32 | }); 33 | }); 34 | 35 | that.server.listen(port); 36 | }); 37 | } 38 | 39 | close() { 40 | this.server.close(); 41 | this.device.connection.close(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/pairing.spec.ts: -------------------------------------------------------------------------------- 1 | // import { Pairing } from '../lib/pairing'; 2 | // import { AppleTV } from '../lib/appletv'; 3 | // import { Message } from '../lib/message'; 4 | // import { MockServer } from './helpers/mock-server'; 5 | // import { expect } from 'chai'; 6 | // import 'mocha'; 7 | 8 | // describe('apple tv pairing', function() { 9 | // beforeEach(function() { 10 | // this.server = new MockServer(); 11 | // this.device = this.server.device; 12 | // }); 13 | 14 | // afterEach(function() { 15 | // this.server.close(); 16 | // }); 17 | 18 | // it('should send introduction', async function() { 19 | // this.device.openConnection(); 20 | // let message = await this.server.message; 21 | 22 | // expect(message.type).to.equal(Message.Type.DeviceInfoMessage); 23 | // }); 24 | // }); 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "lib": [ 7 | "es2015.promise", 8 | "es2017" 9 | ], 10 | "outDir": "./dist" 11 | }, 12 | "include": [ 13 | "src/**/*" 14 | ] 15 | } --------------------------------------------------------------------------------