├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── config.json ├── docs ├── apps.md ├── configuration │ ├── appletv.md │ ├── artwork.md │ ├── features.md │ ├── itunes.md │ ├── macros.md │ └── playlists.md ├── install.md ├── servers.md └── services │ ├── inputcontrolservice.md │ ├── mediaskippingservice.md │ ├── mediatypes.md │ ├── nowplayingservice.md │ ├── playercontrolsservice.md │ └── playlistcontrolservice.md ├── examples ├── appletv │ ├── config.json │ └── readme.md ├── features │ ├── config.json │ └── readme.md ├── install │ ├── config.json │ └── readme.md ├── itunes │ ├── config.json │ └── readme.md ├── multiple-remotes │ ├── config.json │ └── readme.md └── playlists │ ├── config.json │ └── readme.md ├── package-lock.json ├── package.json └── src ├── DacpAccessory.js ├── InputControlService.js ├── MacroCommands.js ├── MacrosService.js ├── MediaSkippingService.js ├── NowPlayingService.js ├── PlayerControlsService.js ├── PlaylistService.js ├── SpeakerService.js ├── artwork └── ArtworkCamera.js ├── daap └── Decoder.js ├── dacp ├── DacpBrowser.js ├── DacpClient.js ├── DacpConnection.js └── DacpRemote.js ├── hap ├── InputControlTypes.js ├── MediaSkippingTypes.js ├── NowPlayingTypes.js ├── PlayerControlsTypes.js └── PlaylistTypes.js └── index.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 8, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2, 15 | { 16 | "SwitchCase": 1 17 | } 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | "quotes": [ 24 | "error", 25 | "single" 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ], 31 | "no-console": [ 32 | "off" 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | 4 | examples/*/persist/** 5 | examples/*/accessories/** 6 | 7 | tests/** -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | 5 | script: 6 | - npm run lint 7 | 8 | before_install: 9 | - sudo apt-get install libavahi-compat-libdnssd-dev 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 0.9 - 2018-02-26 4 | 5 | - Added input control mechanisms for Apple TV 6 | - Added macro execution mechanism for Apple TV 7 | - Added album artwork support 8 | 9 | ## Version 0.8 - 2018-02-22 10 | 11 | - Added support to trigger the playback of Playlists 12 | - Improved error handling 13 | - Added on demand DACP packet tracing 14 | - Updated documentation 15 | 16 | ## Version 0.7.5 - 2017-12-27 17 | 18 | #### Bugfixes 19 | 20 | - Fix #6 (by @nitaybz): Limit the range of the media type characteristic to make rule creation easier. 21 | 22 | ## Version 0.7.4 - 2017-12-13 23 | 24 | #### Bugfixes 25 | 26 | - Bugfix for #4: IllegalStateError: Backoff in progress. 27 | 28 | #### Alternate Play/Pause switch mode 29 | 30 | iOS' Home app is unfortunately incompatible with custom services and 31 | characteristics. As such this plugin was not working for users of the Home app until 32 | now. This version adds a configuration setting to change the mode that the 33 | play/pause button is published to enable Home to use it like a power switch. 34 | 35 | ## Version 0.7.3 - 2017-12-12 36 | 37 | #### Bugfixes 38 | 39 | - Fixed start issue `TypeError: Cannot read property 'name' of undefined` 40 | - Fixed issue retrieving playback position from Apple TV causing needless reconnects 41 | 42 | ## Version 0.7.2 - 2017-12-10 43 | 44 | Fixed missing changelog updates 45 | 46 | ## Version 0.7.1 - 2017-12-10 47 | 48 | Fixed broken formatting in [README.md](README.md) 49 | 50 | ## Version 0.7.0 - 2017-12-10 51 | 52 | #### Improved feature documentation 53 | 54 | The [README.md](README.md) has an updated documentation of the features. 55 | 56 | #### Renamed feature toggles 57 | 58 | By default all features are available for all devices. Use feature toggles to 59 | disable specific services. 60 | 61 | For consistency reasons the feature toggles have been changed to represent a 62 | disabled state. The naming has been changed accordingly. Where you'd previously 63 | write `volume-control` you'd now write `no-volume-controls`. See the README.md 64 | for an example. 65 | 66 | #### More reliable reachable status 67 | 68 | The reachable status was previously reported when the accessory has seen 69 | MDNS announcements for the Apple TV or iTunes. This version updates the 70 | reachable state depending upon the actual network connection state. 71 | 72 | #### Made zero the default media type value in the NowPlayingService 73 | 74 | This aligns the value with reports from Apple TV, when playing media from apps. 75 | The reported media type is zero in those cases. 76 | 77 | #### Report actual playback position via getproperty calls instead of guessing 78 | 79 | Previous version tried to guess the playback position by essentially counting 80 | the seconds. This version periodically acquires the actual playback position and 81 | updates the characteristic in response to reports from iTunes or Apple TV. 82 | 83 | #### Now Playing Service shows all characteristics immediately 84 | 85 | If nothing was playing most of the characteristics were missing from the now 86 | playing service as they were optional. 87 | 88 | #### Added media skipping service 89 | 90 | The service provides two additional controls to skip forward and backward to 91 | the next or previous track respectively. The service is enabled by default for 92 | all accessory and can be disabled for each accessory individually using a 93 | feature togggle. 94 | 95 | ## Version 0.0.6 - 2017-12-10 96 | 97 | - Implemented exponential backoff to recover broken DACP connections 98 | - Added media type to the NowPlayingService 99 | - Added genre to the NowPlayingService 100 | - Emptying values in the NowPlayingService if Apple TV or iTunes disappears 101 | - Added this change log 102 | - Reworded pairing messages in the log 103 | - Added decoder for 'mers' and 'merr' DMAP messages 104 | - Setting accessory as unreachable if Apple TV or iTunes disappears 105 | 106 | ## Version 0.0.5 - 2017-12-09 107 | 108 | - Write errors to the log when a DACP connection fails 109 | 110 | ## Version 0.0.4 - 2017-12-08 111 | 112 | - Changed license to MIT in package 113 | - Started basic error handling and recovery for DACP connections 114 | - Improved parsing of DMAP message containers 115 | - Basic error handling in DacpClient 116 | - Improved messages in homebridge logs for pairing purposes 117 | 118 | ## Version 0.0.3 - 2017-12-08 119 | 120 | - No changes 121 | 122 | ## Version 0.0.2 - 2017-12-08 123 | 124 | - Added Play/Pause switch to control playback 125 | 126 | ## Version 0.0.1 - 2017-12-08 127 | 128 | - Created initial basic accessory and initial work on DMAP support. -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Credits and homebridge-dacp Contributors 2 | 3 | * [@grover](https://github.com/grover) 4 | 5 | * Maintainer and creator of the plugin 6 | 7 | * [@SphtKr](https://github.com/SphtKr) 8 | 9 | * Author of [homebridge-itunes](https://github.com/SphtKr/homebridge-itunes), which served as inspiration for this plugin. 10 | 11 | * [@JamesMensah](https://github.com/JamesMensah), [@jdebardi](https://github.com/jdebardi) 12 | 13 | * Original idea of creating a switch that's programmable with a key sequence, served as the foundation of the [Macro](docs/configuration/macros.md) functionality. 14 | 15 | Especially thank you to the countless others those who've reverse engineered DAAP, DACP and DMAP and the various tools that I've used to create this plugin. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michael Fröhlich 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 | # Homebridge DACP 2 | 3 | This platform plugin enables Homebridge to control devices or programs, which implement [DACP](https://en.wikipedia.org/wiki/Digital_Audio_Control_Protocol). Examples of programs or 4 | devices that can be controlled by this plugin are Apple TV and iTunes. 5 | 6 | Any system capable of running [Homebridge](https://github.com/nfarina/homebridge) can be 7 | used to run `homebridge-dacp`. The only need is network access to the device or program in 8 | question. There is no need to run this on the same machine as iTunes to expose it via HomeKit. 9 | 10 | homebridge-dacp provides volume control and play/pause controls via HomeKit. These can be 11 | used in conjunction with the [homebridge-callmonitor plugin](https://github.com/grover/homebridge-callmonitor) 12 | to pause media playback or to reduce the playback volume while on a phone call. 13 | 14 | The Speaker service, which provides volume control and mute, can be disabled if it is not 15 | required - for example if the Apple TV is connected to an A/V receiver that is integrated 16 | with HomeKit via a different plugin. 17 | 18 | ## Status 19 | 20 | [![HitCount](http://hits.dwyl.io/grover/homebridge-dacp.svg)](https://github.com/grover/homebridge-dacp) 21 | [![Build Status](https://travis-ci.org/grover/homebridge-dacp.png?branch=master)](https://travis-ci.org/grover/homebridge-dacp) 22 | [![Node version](https://img.shields.io/node/v/homebridge-dacp.svg?style=flat)](http://nodejs.org/download/) 23 | [![NPM Version](https://badge.fury.io/js/homebridge-dacp.svg?style=flat)](https://npmjs.org/package/homebridge-dacp) 24 | 25 | ## Changelog 26 | 27 | See the [changelog](CHANGELOG.md) for changes between versions of this package. 28 | 29 | ## Documentation 30 | 31 | * [Supported HomeKit Apps](docs/apps.md) 32 | * [Supported DACP servers](docs/servers.md) 33 | * [Installation instruction](docs/install.md) 34 | * [Configuring Apple TV](docs/configuration/appletv.md) 35 | * [Configuring iTunes](docs/configuration/itunes.md) 36 | * [Feature Toggles](docs/configuration/features.md) 37 | * [Configuring Playlists](docs/configuration/playlists.md) 38 | * [Macros](docs/configuration/macros.md) 39 | * [Artwork](docs/configuration/artwork.md) 40 | * Custom Services and Characteristics 41 | * [Media Skipping Service](docs/services/mediaskippingservice.md) 42 | * [Now Playing Service](docs/services/nowplayingservice.md) 43 | * [Player Controls Service](docs/services/playercontrolsservice.md) 44 | * [Playlist Control Service](docs/services/playlistcontrolservice.md) 45 | * [Input Control Service](docs/services/inputcontrolservice.md) 46 | * Examples 47 | * [Basic installation](examples/install) 48 | * [Apple TV configuration](examples/appletv) 49 | * [iTunes configuration](examples/itunes) 50 | * [Features](examples/features) 51 | * [Playlists](examples/playlists) 52 | * [Multiple remotes for multiple apps/devices](examples/multiple-remotes) 53 | 54 | ## Some asks for friendly gestures 55 | 56 | If you use this and like it - please leave a note by staring this package here or on GitHub. 57 | 58 | If you use it and have a problem, file an issue at [GitHub](https://github.com/grover/homebridge-dacp/issues) - I'll try to help. 59 | 60 | If you tried this, but don't like it: tell me about it in an issue too. I'll try my best 61 | to address these in my spare time. 62 | 63 | If you fork this, go ahead - I'll accept pull requests for enhancements. 64 | 65 | ## Contributors 66 | 67 | This work is based on the work of others, see [CONTRIBUTING](CONTRIBUTING.md) for the credits. 68 | 69 | ## License 70 | 71 | MIT License 72 | 73 | Copyright (c) 2017 Michael Fröhlich 74 | 75 | Permission is hereby granted, free of charge, to any person obtaining a copy 76 | of this software and associated documentation files (the "Software"), to deal 77 | in the Software without restriction, including without limitation the rights 78 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 79 | copies of the Software, and to permit persons to whom the Software is 80 | furnished to do so, subject to the following conditions: 81 | 82 | The above copyright notice and this permission notice shall be included in all 83 | copies or substantial portions of the Software. 84 | 85 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 86 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 87 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 88 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 89 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 90 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 91 | SOFTWARE. 92 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge-DACP AppleTV Example", 4 | "username": "AC:BB:CC:DD:EE:FF", 5 | "port": 54718, 6 | "pin": "135-79-864" 7 | }, 8 | "platforms": [ 9 | { 10 | "platform": "DACP", 11 | "devices": [ 12 | { 13 | "name": "AppleTV", 14 | "features": { 15 | "no-volume-controls": true 16 | } 17 | } 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /docs/apps.md: -------------------------------------------------------------------------------- 1 | # HomeKit Applications 2 | 3 | This plugin has been tested on iOS 11 with the following HomeKit apps: 4 | 5 | * Apple's Home app 6 | * Elgato Eve 7 | 8 | Apple's Home app does not support custom services/characteristics extensions - as such not all functionality is available there. You can still use the Home app with limitations and use other apps to set up the rules and scenes for you. 9 | 10 | Other apps might work, but I can't test them all. If you're using an app that's not listed write me an [issue](https://www.github.com/grover/homebridge-dacp/issues) to update the documentation accordingly. 11 | 12 | # Home App limitations 13 | 14 | The home app will not support the following features: 15 | 16 | * Now Playing information 17 | * Track skipping 18 | * Playlists 19 | 20 | The play/pause functionality can be changed to act as a regular switch using the [features](configuration/features.md), which at least enables some functionality. 21 | -------------------------------------------------------------------------------- /docs/configuration/appletv.md: -------------------------------------------------------------------------------- 1 | # Using homebridge-dacp with Apple TV 2 | 3 | Your Apple TV devices speak the DACP protocol, albeit with some differences to a regular iTunes. For example [playlists](playlists.md) are not supported on Apple TVs as they do not expose the database required to search, queue and play back media. 4 | 5 | If you're curious as to the differences between iTunes and Apple TV: Take a look at the Apple Remote app on the Appstore. The capabilities of the app are to a certain extent also available to this plugin. 6 | 7 | ## What you can do 8 | 9 | On Apple TV the following functionality is available: 10 | 11 | * Play/Pause 12 | * Now playing information (Unfortunately not all apps provide this) 13 | 14 | Even with these constraints this enables you to set up rules that control the Apple TV to pause media playback in a scene and resume it later. 15 | 16 | ## Configuration 17 | 18 | The following configuration is recommended for Apple TV: 19 | 20 | ```json 21 | { 22 | "name": "AppleTV", 23 | "features": { 24 | "no-volume-controls": true, 25 | "input-controls": true 26 | } 27 | } 28 | ``` 29 | 30 | See also the [AppleTV example configuration](../../examples/appletv/config.json) for a full example. 31 | 32 | ### Features 33 | 34 | You may want to play around with `no-volume-controls` as this depends on your home theater set up. The documentation for this and others can be found on the [features page](features.md). 35 | 36 | The `input-controls` feature enables the input control service, which provides the Top Menu, Menu, Select and navigation buttons through HomeKit. This is not useable with the official Apple Home app. There's an equivalent available for Apple Home users documented on the features page. 37 | 38 | ## Pairing 39 | 40 | With the above configuration you've created a virtual remote control called `AppleTV` for your Apple TV. The next step is to pair the Apple TV with the remote. On your Apple TV: 41 | 42 | * Open the settings app 43 | * Navigate to 44 | * Choose 45 | * Select your newly created remote (`AppleTV` above) 46 | * When asked enter the pairing code of the remote control (see below) 47 | 48 | ### Pairing code 49 | 50 | When you launch `homebridge` with this configuration, a log similar to the following will be displayed: 51 | 52 | ```text 53 | [DACP] Found accessory in config: "AppleTV" 54 | [DACP] 55 | [DACP] Skipping creation of the accessory "AppleTV" because it doesn't have a pairing code or 56 | [DACP] service name yet. You need to pair the device/iTunes, reconfigure and restart homebridge. 57 | [DACP] 58 | [DACP] Beginning remote control announcements for the accessory "AppleTV". 59 | [DACP] 60 | [DACP] Use passcode 7118 to pair with this remote control. 61 | [DACP] 62 | ``` 63 | 64 | The plugin is advertising the new remote control for pairing and can be paired using the passcode printed. 65 | 66 | Enter the pairing code 7118 on your Apple TV. 67 | 68 | ## Complete the pairing 69 | 70 | After you've entered the pairing code on your Apple TV you'll see lines like the following in your homebridge log. 71 | 72 | ```text 73 | [DACP] Completed pairing for "AppleTV": 74 | [DACP] 75 | [DACP] { 76 | [DACP] "name": "AppleTV", 77 | [DACP] "pairing": "16BC60A46299FEC4", 78 | [DACP] "serviceName": "AEA342CEA7A8E7EE" 79 | [DACP] } 80 | [DACP] 81 | [DACP] Please add the above block to the accessory in your homebridge config.json. 82 | [DACP] 83 | [DACP] YOU MUST RESTART HOMEBRIDGE AFTER YOU ADDED THE ABOVE LINES OR THE ACCESSORY 84 | [DACP] WILL NOT WORK. 85 | [DACP] 86 | ``` 87 | 88 | Insert the `pairing` and `serviceName` fields in your ```config.json``` and restart homebridge to enable the remote control. 89 | 90 | You can repeat the process for all your devices. It is recommended that you set up your remotes one by one. 91 | -------------------------------------------------------------------------------- /docs/configuration/artwork.md: -------------------------------------------------------------------------------- 1 | # Artwork Support 2 | 3 | **Works on Apple TV and iTunes** 4 | 5 | This plugin is able to stream the artwork of the currently playing media item via a seperate camera feed. This works in the Home app. 6 | 7 | ## Installation 8 | 9 | 1. In order to enable the artwork support you must have an ffmpeg installed on the local system. 10 | 2. Stop homebridge if it is running 11 | 3. Enable the artwork feature for a DACP server 12 | 4. Run homebridge 13 | 5. Pair the newly created camera device with HomeKit 14 | 15 | ## Configuration 16 | 17 | An appropriate configuration in step 3 looks as follows: 18 | 19 | ```json 20 | { 21 | "platform": "DACP", 22 | "devices": [ 23 | { 24 | "name": "iTunes", 25 | "features": { 26 | "album-artwork": "/tmp/nowplaying.png" 27 | } 28 | } 29 | ] 30 | } 31 | ``` 32 | 33 | (There's several other settings and features left out of the above JSON snippet.) 34 | 35 | The `album-artwork` feature points to a file path, which will hold temporary images of the currently playing artwork. This file may not exist 36 | on the system and it is recommended to store it in a temporary location, best backed by a memory file system. 37 | 38 | There's no further configuration need. The plugin will pick up the omx video codec if running on a Raspberry Pi and use x264 on all other platforms. -------------------------------------------------------------------------------- /docs/configuration/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | Features provide the capability to control if specific characteristics will show up for a device. All features are boolean true/false switches and described below: 4 | 5 | ## Alternate Play/Pause Switch 6 | 7 | By default this plugin is not compatible with Apple Home. In order to at least enable basic play/pause functionality, you need to enable the `alternate-playpause-switch` feature. 8 | 9 | ```json 10 | { 11 | "bridge": { 12 | ... 13 | }, 14 | "platforms": [ 15 | { 16 | "platform": "DACP", 17 | "devices": [ 18 | { 19 | "name": "Alternate Play/Pause Example", 20 | "features": { 21 | "alternate-playpause-switch": true 22 | } 23 | } 24 | ] 25 | } 26 | ] 27 | } 28 | ``` 29 | 30 | ## No Volume Controls 31 | 32 | In most cases your AppleTV will not control the volume of played media, but your TV or AVR will. This is also true if the Remote has working volume control buttons as the volume up/down commands are sent either directly to the TV/AVR through infrared signals or as commands through the HDMI cable. Thus the plugin can't really control the volume of your TV/AVR. 33 | 34 | To disable the volume controls, enable the `no-volume-control` feature. 35 | 36 | ```json 37 | { 38 | "bridge": { 39 | ... 40 | }, 41 | "platforms": [ 42 | { 43 | "platform": "DACP", 44 | "devices": [ 45 | { 46 | "name": "No-Volume-Control Example", 47 | "features": { 48 | "no-volume-control": true 49 | } 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | ``` 56 | 57 | ## No Skip Controls 58 | 59 | If you don't want to see track skipping controls, you can disable them with the `no-skip-controls` feature. 60 | 61 | ```json 62 | { 63 | "bridge": { 64 | ... 65 | }, 66 | "platforms": [ 67 | { 68 | "platform": "DACP", 69 | "devices": [ 70 | { 71 | "name": "No-Skip-Controls Example", 72 | "features": { 73 | "no-skip-controls": true 74 | } 75 | } 76 | ] 77 | } 78 | ] 79 | } 80 | ``` 81 | 82 | ## Input Control 83 | 84 | This plugin provides a virtual representation of the typical remote control buttons through the input control service. This feature toggle 85 | enables the input controls in a mode that's not useable in Apple Home. 86 | 87 | ```json 88 | { 89 | "bridge": { 90 | ... 91 | }, 92 | "platforms": [ 93 | { 94 | "platform": "DACP", 95 | "devices": [ 96 | { 97 | "name": "Input-Controls Example", 98 | "features": { 99 | "input-controls": true 100 | } 101 | } 102 | ] 103 | } 104 | ] 105 | } 106 | ``` 107 | 108 | ## Alternate Input Controls 109 | 110 | The alternate input controls provide a similar experience as the input control feature above, but with many independent switches. This feature toggle is either a boolean or a list of buttons to provide to HomeKit: 111 | 112 | ```json 113 | { 114 | "bridge": { 115 | ... 116 | }, 117 | "platforms": [ 118 | { 119 | "platform": "DACP", 120 | "devices": [ 121 | { 122 | "name": "Alternate-Input-Controls Example", 123 | "features": { 124 | "alternate-input-controls": true 125 | } 126 | } 127 | ] 128 | } 129 | ] 130 | } 131 | ``` 132 | 133 | If set to `true`, this enables all buttons as in the `input-controls` above as independented switches. The following example restricts that 134 | to only provide the Menu and TopMenu buttons: 135 | 136 | ```json 137 | { 138 | "bridge": { 139 | ... 140 | }, 141 | "platforms": [ 142 | { 143 | "platform": "DACP", 144 | "devices": [ 145 | { 146 | "name": "Alternate-Input-Controls Example", 147 | "features": { 148 | "alternate-input-controls": ["menu", "topmenu"] 149 | } 150 | } 151 | ] 152 | } 153 | ] 154 | } 155 | ``` 156 | 157 | The following buttons can be listed in the `alternate-input-controls`: 158 | 159 | * menu 160 | * topmenu 161 | * select 162 | * up 163 | * down 164 | * left 165 | * right 166 | 167 | ## Album Artwork 168 | 169 | Points to the temporary file path used to capture the artwork of the currently playing media file. See [Artwork Support](artwork.md) for more. 170 | 171 | ## Using multiple feature toggles 172 | 173 | You can use multiple feature toggles at the same time. The [features](../../examples/features) example shows a full configuration of the DACP plugin with multiple features ready for your use. 174 | -------------------------------------------------------------------------------- /docs/configuration/itunes.md: -------------------------------------------------------------------------------- 1 | # Using homebridge-dacp with iTunes 2 | 3 | iTunes provides an extensive implementation of the DACP protocol. This plugin only uses a subset of the protocol: 4 | 5 | * Volume controls 6 | * Play/Pause controls 7 | * Now playing information 8 | * Track skipping 9 | * [Playlist support](playlists.md) 10 | 11 | ## Configuration 12 | 13 | The following configuration is recommended for Apple TV: 14 | 15 | ```json 16 | { 17 | "name": "iTunes" 18 | } 19 | ``` 20 | 21 | See also the [iTunes example configuration](../../examples/itunes/config.json) for a full example. 22 | 23 | ### Features 24 | 25 | Some [features](features.md) might be of interest to you in addition to the basic configuration. Additionally you can configure [playlists](playlists.md) - which helps you trigger playlists in response to certain scenes. 26 | 27 | ## Pairing 28 | 29 | With the above configuration you've created a virtual remote control called `iTunes` for your iTunes. The next step is to pair iTunes with the remote. Apple has provided [support documentation](https://support.apple.com/kb/ph19503) on the pairing process. Follow the process and the following instructions on this page to complete the pairing process. 30 | 31 | ### Pairing code 32 | 33 | When you launch `homebridge` with this configuration, a log similar to the following will be displayed: 34 | 35 | ```text 36 | [DACP] Found accessory in config: "iTunes" 37 | [DACP] 38 | [DACP] Skipping creation of the accessory "iTunes" because it doesn't have a pairing code or 39 | [DACP] service name yet. You need to pair the device/iTunes, reconfigure and restart homebridge. 40 | [DACP] 41 | [DACP] Beginning remote control announcements for the accessory "iTunes remote". 42 | [DACP] 43 | [DACP] Use passcode 7118 to pair with this remote control. 44 | [DACP] 45 | ``` 46 | 47 | The plugin is advertising the new remote control for pairing and can be paired using the passcode printed. 48 | 49 | Enter the pairing code 7118 in iTunes. 50 | 51 | ## Complete the pairing 52 | 53 | After you've entered the pairing code in iTunes you'll see lines like the following in your homebridge log. 54 | 55 | ```text 56 | [DACP] Completed pairing for "iTunes": 57 | [DACP] 58 | [DACP] { 59 | [DACP] "name": "iTunes remote", 60 | [DACP] "pairing": "16BC60A46299FEC4", 61 | [DACP] "serviceName": "AEA342CEA7A8E7EE" 62 | [DACP] } 63 | [DACP] 64 | [DACP] Please add the above block to the accessory in your homebridge config.json. 65 | [DACP] 66 | [DACP] YOU MUST RESTART HOMEBRIDGE AFTER YOU ADDED THE ABOVE LINES OR THE ACCESSORY 67 | [DACP] WILL NOT WORK. 68 | [DACP] 69 | ``` 70 | 71 | **Note**: The pairing and service name codes will differ for each iTunes. Do not copy the above values, use those printed in your homebridge log. 72 | 73 | Insert the `pairing` and `serviceName` fields in your ```config.json``` and restart homebridge to enable the remote control. 74 | 75 | You can repeat the process for all your computers and devices. It is recommended that you set up your remotes one by one. 76 | -------------------------------------------------------------------------------- /docs/configuration/macros.md: -------------------------------------------------------------------------------- 1 | # Macros support 2 | 3 | **AppleTV only** 4 | 5 | This plugin can initiate a sequence of key presses to trigger specific actions on the Apple TV. The macro language is comprised of all the keys on 6 | the remote control and they are executed in sequence. 7 | 8 | ## Configure a macro 9 | 10 | Assuming you have a regular Apple TV configuration as shown in the [Apple TV example](../../examples/appletv) you can add macros in the following manner: 11 | 12 | ```json 13 | { 14 | "bridge": { 15 | ... 16 | }, 17 | "platforms": [ 18 | { 19 | "platform": "DACP", 20 | "devices": [ 21 | { 22 | "name": "AppleTV", 23 | "macros": { 24 | "Settings": [ 25 | "topmenu", 26 | "left", 27 | "left", 28 | "left", 29 | "left", 30 | "up", 31 | "up", 32 | "up", 33 | "up", 34 | "up", 35 | "up", 36 | "down", 37 | "down", 38 | "down", 39 | "down", 40 | "down", 41 | "select" 42 | ] 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ``` 50 | 51 | In the example one macro is added, which is named `Settings`. 52 | 53 | ## Using the macro feature 54 | 55 | Once macros have been added and homebridge has been restarted, you can launch any macro by toggling the button in your HomeKit enabled app. This does not work in Apple Home. 56 | 57 | The plugin will execute the macro in sequence. 58 | 59 | ## Supported macro commands 60 | 61 | The following commands (keys) are currently supported in a macro: 62 | 63 | | Command | Action | 64 | |---|---| 65 | | topmenu | Simulates the press of the Top Menu key on the remote. | 66 | | menu | Simulates the press of the Menu key on the remote. | 67 | | select | Simulates the press of the selection key on the remote. | 68 | | up | Simulates an upward stroke or the press of the up button on the remote. | 69 | | down | Simulates a downward stroke or the press of the down button on the remote. | 70 | | left | Simulates a leftward stroke or the press of the left button on the remote. | 71 | | right | Simulates a rightward stroke or the press of the right button on the remote. | 72 | | wait5s | Waits for five seconds before executing the next command in the sequence. | 73 | 74 | -------------------------------------------------------------------------------- /docs/configuration/playlists.md: -------------------------------------------------------------------------------- 1 | # Playlist support 2 | 3 | This plugin can initiate the playback of playlists in iTunes. This feature is not supported for Apple TV. 4 | 5 | To configure playlists, you must follow the following steps: 6 | 7 | 1. Create the playlist in iTunes 8 | 2. Configure the playlist 9 | 3. Restart homebridge 10 | 11 | ## Configure a playlist 12 | 13 | Assuming you have a regular iTunes configuration as shown in the [iTunes example](../../examples/itunes) you can add playlists in the following manner: 14 | 15 | ```json 16 | { 17 | "bridge": { 18 | ... 19 | }, 20 | "platforms": [ 21 | { 22 | "platform": "DACP", 23 | "devices": [ 24 | { 25 | "name": "iTunes", 26 | "playlists": [ 27 | "Test" 28 | ] 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | ``` 35 | 36 | In the example one playlist is added, which is named `Test`. 37 | 38 | # Using the playlist feature 39 | 40 | Once playlists have been added and homebridge has been restarted, you can launch any playlist by toggling the button in your HomeKit enabled app. This does not work in Apple Home. -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation instructions 2 | 3 | If you haven't done so: install [homebridge](https://github.com/nfarina/homebridge) per the official [installation instructions](https://github.com/nfarina/homebridge#installation). 4 | 5 | After you've installed homebridge go ahead and install the plugin with: 6 | 7 | ```bash 8 | npm install -g homebridge-dacp --unsafe-perm 9 | ``` 10 | 11 | You might need ```sudo``` privileges to install into the global node_modules. 12 | 13 | ## Basic configuration 14 | 15 | To use the plugin, add the following basic configuration to your `config.json` that configures homebridge in the ```platforms``` section: 16 | 17 | ```json 18 | { 19 | "platform": "DACP", 20 | "devices": [ 21 | ] 22 | } 23 | ``` 24 | 25 | After you've saved your `config.json` you can go ahead and launch homebridge and observe the logs. 26 | 27 | ## First run 28 | 29 | With the above configuration homebridge and homebridge-dacp will print the following logs: 30 | 31 | ```text 32 | Loading 1 platforms... 33 | [DACP] Initializing DACP platform... 34 | [DACP] DACP Platform Plugin Loaded - Version 0.7.6 35 | [DACP] Starting DACP browser... 36 | ``` 37 | 38 | This shows that you've installed homebridge and homebridge-dacp correctly. 39 | 40 | You can go ahead and pair your [Apple TV](configuration/appletv.md) or [iTunes](configuration/itunes.md). -------------------------------------------------------------------------------- /docs/servers.md: -------------------------------------------------------------------------------- 1 | # Supported DACP servers 2 | 3 | This plugin has been tested with: 4 | 5 | * Apple TV 4 6 | * iTunes 12.7 7 | 8 | ## Other products 9 | 10 | There's several [DACP servers](https://en.wikipedia.org/wiki/Digital_Audio_Control_Protocol) out there, which might work with this plugin too. I've not tested them or integrated them with the plugin. Should you happen to use one with this plugin, send me a note such that I can update the documentation. -------------------------------------------------------------------------------- /docs/services/inputcontrolservice.md: -------------------------------------------------------------------------------- 1 | # Media Skipping Service 2 | 3 | Service ID: `5F862E4E-9D42-4636-9F1E-0D4BC5572705` 4 | 5 | This service simulates the remote control buttons for the device. This contains 6 | the following fields: 7 | 8 | | Characteristic | UUID | Type | Permissions | Description | 9 | |----------------|------|------|-------------|-------------| 10 | | TopMenuButton | `53426A9B-1AB0-44CB-B88B-82D96EFC51CE` | Boolean | Read, Write, Notify | Simulates a press of the Top Menu button on an AppleTV remote. | 11 | | MenuButton | `CB68261D-DB68-46B8-B1F0-5BDFEC872039` | Boolean | Read, Write, Notify | Simulates a press of the Menu button on an AppleTV remote. | 12 | | SelectButton | `C67044BB-EE9F-4F72-9816-FEE962BE1EB1` | Boolean | Read, Write, Notify | Simulates a press of the Select button on an AppleTV remote. | 13 | | UpButton | `3B005F2F-E2AE-4895-A37E-53280E2EA764` | Boolean | Read, Write, Notify | Simulates a press of the Up button on an AppleTV remote. | 14 | | DownButton | `B17E1EC9-314B-46F1-97D5-0A371B662D2A` | Boolean | Read, Write, Notify | Simulates a press of the Down button on an AppleTV remote. | 15 | | LeftButton | `76261837-BFE3-413B-9803-36122EE1D994` | Boolean | Read, Write, Notify | Simulates a press of the Left button on an AppleTV remote. | 16 | | RightButton | `A3DECC2A-4852-4347-A548-9972E0490891` | Boolean | Read, Write, Notify | Simulates a press of the Right button on an AppleTV remote. | 17 | 18 | Source code: [InputControlTypes.js](src/hap/InputControlTypes.js) 19 | -------------------------------------------------------------------------------- /docs/services/mediaskippingservice.md: -------------------------------------------------------------------------------- 1 | # Media Skipping Service 2 | 3 | Service ID: `07163D16-8F0E-4B36-9AC4-18BE183B9EDE` 4 | 5 | This service provides skip forward/backward controls for the device. This contains 6 | the following fields: 7 | 8 | | Characteristic | UUID | Type | Permissions | Description | 9 | |----------------|------|------|-------------|-------------| 10 | | SkipForward | `CD56B40B-F98B-4ACA-BF5E-4AD4E9C77D1C` | Boolean | Read, Write, Notify | Skips forward to the next track. Automatically resets to false after the skip operation has completed. | 11 | | SkipBackward | `CFFE477D-70C8-4630-B33B-25073F137191` | Boolean | Read, Write, Notify | Skips backward to the previous track. Automatically resets to false after the skip operation has completed. | 12 | 13 | Source code: [MediaSkippingTypes.js](src/hap/MediaSkippingTypes.js) 14 | 15 | -------------------------------------------------------------------------------- /docs/services/mediatypes.md: -------------------------------------------------------------------------------- 1 | # Media Types 2 | 3 | The protocol enables one to detect the media types of a playing media item. This is exposed in the Media Type characteristic of the Now Playing Service. 4 | 5 | The following media types are known: 6 | 7 | | Code | Type | 8 | |------|------| 9 | | 1 | Audio track | 10 | | 2 | Movie | 11 | | 4 | Podcast | 12 | | 8 | Audio book | 13 | | 64 | TV show | 14 | 15 | If you know one not on this list, file an issue or better yet a pull request. -------------------------------------------------------------------------------- /docs/services/nowplayingservice.md: -------------------------------------------------------------------------------- 1 | # Now Playing Service 2 | 3 | Service ID: `F7138C87-EABF-420A-BFF0-76FC04DD81CD` 4 | 5 | This service provides status information about the currently playing media. This contains 6 | the following fields: 7 | 8 | | Characteristic | UUID | Type | Permissions | Description | 9 | |----------------|------|------|-------------|-------------| 10 | | Title | `00003001-0000-1000-8000-135D67EC4377` | String | Read, Notify | The title of the currently playing track. | 11 | | Album | `00003002-0000-1000-8000-135D67EC4377` | String | Read, Notify | The album of the currently playing track. | 12 | | Artist | `00003003-0000-1000-8000-135D67EC4377` | String | Read, Notify | The artist of the currently playing track. | 13 | | Genre | `8087750B-8B8C-451E-B907-8E3BAD8DCB1E` | String | Read, Notify | The genre of the currently playing track. | 14 | | Media Type | `9898982C-7B70-47AD-A81D-211BFE5AFBF2` | Number | Read, Notify | The media type of the currently playing track. See [Media Types](doc/MediaTypes.md) for more. | 15 | | Position | `00002007-0000-1000-8000-135D67EC4377` | String | Read, Notify | The current playback position. | 16 | | Duration | `00003005-0000-1000-8000-135D67EC4377` | String | Read, Notify | The duration of the current track. | 17 | 18 | Source Code: [NowPlayingTypes.js](src/hap/NowPlayingTypes.js) -------------------------------------------------------------------------------- /docs/services/playercontrolsservice.md: -------------------------------------------------------------------------------- 1 | # Player Controls Service 2 | 3 | Service ID: `EFD51587-6F54-4093-9E8D-FA3975DCDCE6` 4 | 5 | This service provides a play/pause control for the device. This contains 6 | the following fields: 7 | 8 | | Characteristic | UUID | Type | Permissions | Description | 9 | |----------------|------|------|-------------|-------------| 10 | | PlayPause | `BA16B86C-DC86-482A-A70C-CC9C924DB842` | Boolean | Read, Write, Notify | Represents the playback state of the device/iTunes. | 11 | 12 | Source Code: [PlayerControlsTypes.js](src/hap/PlayerControlsTypes.js) 13 | -------------------------------------------------------------------------------- /docs/services/playlistcontrolservice.md: -------------------------------------------------------------------------------- 1 | # Playlist Control Service 2 | 3 | Service ID: `24B5B813-8D9C-49C3-ABFB-EDE879A4FF99` 4 | 5 | This service provides a start control for the each configured playlist. The 6 | the following fields: 7 | 8 | | Characteristic | UUID | Type | Permissions | Description | 9 | |----------------|------|------|-------------|-------------| 10 | | StartPlaylist | dynamic | Boolean | Read, Write, Notify | A toggle control to start playback of a specific playlist. | 11 | 12 | Source Code: [PlaylistTypes.js](src/hap/PlaylistTypes.js) -------------------------------------------------------------------------------- /examples/appletv/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge-DACP AppleTV Example", 4 | "username": "AC:BB:CC:DD:EE:FF", 5 | "port": 54718, 6 | "pin": "135-79-864" 7 | }, 8 | "platforms": [ 9 | { 10 | "platform": "DACP", 11 | "devices": [ 12 | { 13 | "name": "AppleTV", 14 | "features": { 15 | "no-volume-controls": true 16 | } 17 | } 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /examples/appletv/readme.md: -------------------------------------------------------------------------------- 1 | # Apple TV example 2 | 3 | This shows a basic homebridge configuration with the DACP plugin for the Apple TV. It also disables the volume control feature as described in [features](../../docs/configuration/features.md). 4 | 5 | You still need to pair this example to your [Apple TV](../../docs/configuration/appletv.md). 6 | -------------------------------------------------------------------------------- /examples/features/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge-DACP Multiple Features Example", 4 | "username": "AD:BB:CC:DD:EE:FF", 5 | "port": 54718, 6 | "pin": "135-79-864" 7 | }, 8 | "platforms": [ 9 | { 10 | "platform": "DACP", 11 | "devices": [ 12 | { 13 | "name": "Multiple-Features", 14 | "features": { 15 | "alternate-playpause-switch": true, 16 | "no-volume-controls": true, 17 | "no-skip-controls": true 18 | } 19 | } 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /examples/features/readme.md: -------------------------------------------------------------------------------- 1 | # Multiple features example 2 | 3 | Multiple features can be configured at the same time for a single remote. This example shows how the [features](../../docs/configuration/features.md) can be used. 4 | 5 | Specifically this example: 6 | 7 | * Uses a regular HomeKit switch for Play/Pause to make it compatible with Apple Home. 8 | * Disables volume control support 9 | * Disables track skipping 10 | 11 | You still need to pair this example to your [iTunes](../../docs/configuration/itunes.md) or [Apple TV](../../docs/configuration/appletv.md). -------------------------------------------------------------------------------- /examples/install/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge-DACP Install Example", 4 | "username": "AA:BB:CC:DD:EE:FF", 5 | "port": 54718, 6 | "pin": "135-79-864" 7 | }, 8 | "platforms": [ 9 | { 10 | "platform": "DACP", 11 | "devices": [] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /examples/install/readme.md: -------------------------------------------------------------------------------- 1 | # Install example 2 | 3 | This shows a basic homebridge configuration with the DACP plugin. It provides no remote controls. 4 | 5 | You still need to pair this example to your [iTunes](../../docs/configuration/itunes.md) or [Apple TV](../../docs/configuration/appletv.md). 6 | -------------------------------------------------------------------------------- /examples/itunes/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge-DACP iTunes Example", 4 | "username": "AB:BB:CC:DD:EE:FF", 5 | "port": 54718, 6 | "pin": "135-79-864" 7 | }, 8 | "platforms": [ 9 | { 10 | "platform": "DACP", 11 | "devices": [ 12 | { 13 | "name": "iTunes" 14 | } 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /examples/itunes/readme.md: -------------------------------------------------------------------------------- 1 | # iTunes example 2 | 3 | This example shows the configuration of the DACP plugin for iTunes 12.7. 4 | 5 | You still need to pair this example to your [iTunes](../../docs/configuration/itunes.md). 6 | -------------------------------------------------------------------------------- /examples/multiple-remotes/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge-DACP Multiple Remotes Example", 4 | "username": "AF:BB:CC:DD:EE:FF", 5 | "port": 54718, 6 | "pin": "135-79-864" 7 | }, 8 | "platforms": [ 9 | { 10 | "platform": "DACP", 11 | "devices": [ 12 | { 13 | "name": "AppleTV-1", 14 | "features": { 15 | "no-volume-controls": true 16 | } 17 | }, 18 | { 19 | "name": "AppleTV-2", 20 | "features": { 21 | "no-volume-controls": true 22 | } 23 | }, 24 | { 25 | "name": "iTunes" 26 | } 27 | ] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /examples/multiple-remotes/readme.md: -------------------------------------------------------------------------------- 1 | # Multiple remote controls example 2 | 3 | The plugin is able to create and manage multiple remote controls at the same time. This example is a combination of the [iTunes](../itunes) and [AppleTV](../appletv) examples. The examples shows the general structure of creating two remotes for Apple TVs and one remote for an iTunes installation. 4 | 5 | You need to pair this example to an iTunes installations and two Apple TV boxes. 6 | -------------------------------------------------------------------------------- /examples/playlists/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge-DACP Playlists Example", 4 | "username": "AE:BB:CC:DD:EE:FF", 5 | "port": 54718, 6 | "pin": "135-79-864" 7 | }, 8 | "platforms": [ 9 | { 10 | "platform": "DACP", 11 | "devices": [ 12 | { 13 | "name": "iTunes", 14 | "playlists": [ 15 | "Test" 16 | ] 17 | } 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /examples/playlists/readme.md: -------------------------------------------------------------------------------- 1 | # iTunes playlists example 2 | 3 | This example shows the configuration of the DACP plugin for iTunes 12.7 with the support of playlists. 4 | 5 | You still need to pair this example to your [iTunes](../../docs/configuration/itunes.md) and create a playlist `Test` in your iTunes. See also [playlists](../../docs/configuration/playlists.md) for more information. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-dacp", 3 | "version": "0.9.2", 4 | "description": "A homebridge plugin to control iTunes, AirPlay speakers and Apple TV devices", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "lint": "./node_modules/.bin/eslint src", 8 | "release": "npm-github-release" 9 | }, 10 | "author": "Michael Fröhlich", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/grover/homebridge-dacp.git" 15 | }, 16 | "dependencies": { 17 | "backoff": "^2.5.0", 18 | "debug": "^3.1.0", 19 | "detect-rpi": "^1.2.0", 20 | "mdns": "^2.3.4", 21 | "mdns-resolver": "0.0.1", 22 | "moment": "^2.22.1", 23 | "request": "^2.85.0", 24 | "sequential-task-queue": "^1.2.0" 25 | }, 26 | "keywords": [ 27 | "homebridge", 28 | "homebridge-plugin", 29 | "appletv", 30 | "itunes", 31 | "airplay", 32 | "dacp", 33 | "homekit", 34 | "home-automation" 35 | ], 36 | "engines": { 37 | "node": ">=9.3.0", 38 | "homebridge": ">=0.4.36" 39 | }, 40 | "devDependencies": { 41 | "eslint": "^4.19.1", 42 | "npm-github-release": "^0.9.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DacpAccessory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const backoff = require('backoff'); 5 | 6 | const DacpClient = require('./dacp/DacpClient'); 7 | 8 | const MediaSkippingService = require('./MediaSkippingService'); 9 | const NowPlayingService = require('./NowPlayingService'); 10 | const PlayerControlsService = require('./PlayerControlsService'); 11 | const SpeakerService = require('./SpeakerService'); 12 | const PlaylistService = require('./PlaylistService'); 13 | const InputControlService = require('./InputControlService'); 14 | const MacrosService = require('./MacrosService'); 15 | 16 | let Characteristic, Service; 17 | 18 | class DacpAccessory { 19 | 20 | constructor(api, log, config) { 21 | this.api = api; 22 | Characteristic = this.api.hap.Characteristic; 23 | Service = this.api.hap.Service; 24 | 25 | this.log = log; 26 | this.name = config.name; 27 | this.serviceName = config.serviceName; 28 | 29 | this.config = config; 30 | 31 | this._isAnnounced = false; 32 | this._isReachable = false; 33 | this._playStatusUpdateListeners = []; 34 | 35 | // Maximum backoff is 15mins when a device/program is visible 36 | this._backoff = backoff.exponential({ 37 | initialDelay: 100, 38 | maxDelay: 900000 39 | }).on('backoff', (number, delay) => this._onBackoffStarted(delay)) 40 | .on('ready', () => this._connectToDacpDevice()); 41 | 42 | this._dacpClient = new DacpClient(log) 43 | .on('failed', e => this._onDacpFailure(e)); 44 | 45 | this._services = this.createServices(this.api.hap); 46 | } 47 | 48 | getServices() { 49 | return this._services; 50 | } 51 | 52 | createServices(homebridge) { 53 | return [ 54 | this.getAccessoryInformationService(), 55 | this.getBridgingStateService(), 56 | this.getPlayerControlsService(homebridge), 57 | this.getSpeakerService(homebridge), 58 | this.getNowPlayingService(homebridge), 59 | this.getMediaSkippingService(homebridge), 60 | this.getPlaylistService(homebridge), 61 | ...this.getInputControlService(homebridge), 62 | ...this.getMacrosService(homebridge) 63 | ].filter(m => m != null); 64 | } 65 | 66 | getAccessoryInformationService() { 67 | return new Service.AccessoryInformation() 68 | .setCharacteristic(Characteristic.Name, this.name) 69 | .setCharacteristic(Characteristic.Manufacturer, 'Michael Froehlich') 70 | .setCharacteristic(Characteristic.Model, 'DACP Accessory') 71 | .setCharacteristic(Characteristic.SerialNumber, '42') 72 | .setCharacteristic(Characteristic.FirmwareRevision, this.config.version) 73 | .setCharacteristic(Characteristic.HardwareRevision, this.config.version); 74 | } 75 | 76 | getBridgingStateService() { 77 | this._bridgingService = new Service.BridgingState(); 78 | 79 | this._bridgingService.getCharacteristic(Characteristic.Reachable) 80 | .on('get', this._getReachable.bind(this)) 81 | .updateValue(this._isReachable); 82 | 83 | return this._bridgingService; 84 | } 85 | 86 | getSpeakerService(homebridge) { 87 | if (this.config.features['no-volume-controls'] === true) { 88 | return; 89 | } 90 | 91 | this._speakerService = new SpeakerService(homebridge, this.log, this.name, this._dacpClient); 92 | return this._speakerService.getService(); 93 | } 94 | 95 | getPlayerControlsService(homebridge) { 96 | let service = Service.PlayerControlsService; 97 | let characteristic = Characteristic.PlayPause; 98 | 99 | if (this.config.features['alternate-playpause-switch'] === true) { 100 | service = Service.Switch; 101 | characteristic = Characteristic.On; 102 | } 103 | 104 | this._playerControlsService = new PlayerControlsService(homebridge, this.log, this.name, this._dacpClient, service, characteristic); 105 | this._playStatusUpdateListeners.push(this._playerControlsService); 106 | 107 | return this._playerControlsService.getService(); 108 | } 109 | 110 | getNowPlayingService(homebridge) { 111 | const artworkFile = this.config.features['album-artwork']; 112 | 113 | this._nowPlayingService = new NowPlayingService(homebridge, this.log, this.name, this._dacpClient, artworkFile); 114 | this._playStatusUpdateListeners.push(this._nowPlayingService); 115 | 116 | return this._nowPlayingService.getService(); 117 | } 118 | 119 | getMediaSkippingService(homebridge) { 120 | if (this.config.features['no-skip-controls'] === true) { 121 | return undefined; 122 | } 123 | 124 | this._mediaSkippingService = new MediaSkippingService(homebridge, this.log, this.name, this._dacpClient); 125 | return this._mediaSkippingService.getService(); 126 | } 127 | 128 | getPlaylistService(homebridge) { 129 | if (!this.config.playlists) { 130 | return undefined; 131 | } 132 | 133 | this._playlistService = new PlaylistService(homebridge, this.log, this.name, this._dacpClient, this.config); 134 | return this._playlistService.getService(); 135 | } 136 | 137 | getInputControlService(homebridge) { 138 | if (this.config.features === undefined || 139 | (this.config.features.hasOwnProperty('input-controls') === false && this.config.features.hasOwnProperty('alternate-input-controls') === false)) { 140 | return []; 141 | } 142 | 143 | this._inputControlService = new InputControlService(homebridge, this.log, this.name, this._dacpClient, this.config.features); 144 | return this._inputControlService.getService(); 145 | } 146 | 147 | getMacrosService(homebridge) { 148 | if (this.config.macros === undefined) { 149 | return []; 150 | } 151 | 152 | this._macroService = new MacrosService(homebridge, this.log, this.name, this._dacpClient, this.config.macros); 153 | return this._macroService.getService(); 154 | } 155 | 156 | identify(callback) { 157 | this.log(`Identify requested on ${this.name}`); 158 | callback(); 159 | } 160 | 161 | accessoryUp(service) { 162 | if (this._isAnnounced) { 163 | return; 164 | } 165 | 166 | this.log(`The accessory ${this.name} is announced as ${service.host}:${service.port}.`); 167 | this.log(`It's an ${service.txtRecord.DvTy} named ${service.txtRecord.CtlN}`); 168 | 169 | this._service = service; 170 | this._isAnnounced = true; 171 | 172 | // Let backoff trigger the connection 173 | this._backoff.backoff(); 174 | } 175 | 176 | accessoryDown() { 177 | this.log(`The accessory ${this.name} is down.`); 178 | 179 | this._isAnnounced = false; 180 | 181 | // Do not attempt to reconnect again 182 | this._backoff.reset(); 183 | 184 | this._services.forEach(service => { 185 | if (service.accessoryDown) { 186 | service.accessoryDown(); 187 | } 188 | }); 189 | 190 | this._dacpClient.disconnect(); 191 | this._service = undefined; 192 | this._setReachable(false); 193 | } 194 | 195 | _schedulePlayStatusUpdate() { 196 | this._dacpClient.requestPlayStatus() 197 | .then(response => { 198 | this._playStatusUpdateListeners.forEach(listener => { 199 | listener.update(response); 200 | }); 201 | }) 202 | .then(() => { 203 | if (this._speakerService) { 204 | return this._speakerService.update(); 205 | } 206 | 207 | return Promise.resolve(); 208 | }) 209 | .then(() => { 210 | this._schedulePlayStatusUpdate(); 211 | }) 212 | .catch(e => { 213 | this.log(`[${this.name}] Retrieving updates from DACP server failed with error ${util.inspect(e)}`); 214 | }); 215 | } 216 | 217 | _connectToDacpDevice() { 218 | // Do not connect if a backoff interval expires and 219 | // the device has gone down in the mean time. 220 | if (!this._isAnnounced) { 221 | return; 222 | } 223 | 224 | const settings = { 225 | host: `${this._service.host}:${this._service.port}`, 226 | pairing: this.config.pairing 227 | }; 228 | 229 | this.log(`Connecting to ${this.name} (${this._service.host}:${this._service.port})`); 230 | this._dacpClient.connect(settings) 231 | .then(serverInfo => { 232 | if (serverInfo.minm) { 233 | this.log(`Connected to ${serverInfo.minm}`); 234 | } 235 | this._backoff.reset(); 236 | 237 | this._setReachable(true); 238 | this._schedulePlayStatusUpdate(); 239 | }) 240 | .catch(error => { 241 | this.log(`[${this.name}] Connection to DACP server failed: ${util.inspect(error)}`); 242 | 243 | this._backoff.backoff(); 244 | }); 245 | } 246 | 247 | _onDacpFailure(e) { 248 | this.log(`Fatal error while talking to ${this.name}:`); 249 | this.log(''); 250 | this.log(` Error: ${util.inspect(e)}`); 251 | this.log(''); 252 | 253 | this._backoff.backoff(); 254 | } 255 | 256 | _onBackoffStarted(delay) { 257 | if (this._isAnnounced) { 258 | this.log(`Attempting to reconnect to ${this.name} in ${delay / 1000} seconds.`); 259 | } 260 | } 261 | 262 | _getReachable(callback) { 263 | this.log(`Returning reachability state: ${this._isReachable}`); 264 | callback(undefined, this._isReachable); 265 | } 266 | 267 | _setReachable(state) { 268 | if (this._isReachable === state) { 269 | return; 270 | } 271 | 272 | this._isReachable = state; 273 | 274 | this._bridgingService.getCharacteristic(Characteristic.Reachable) 275 | .updateValue(this._isReachable); 276 | } 277 | } 278 | 279 | module.exports = DacpAccessory; 280 | -------------------------------------------------------------------------------- /src/InputControlService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Characteristic, Service; 4 | 5 | const MacroCommands = require('./MacroCommands'); 6 | 7 | class InputControlService { 8 | 9 | constructor(homebridge, log, name, dacp, features) { 10 | Characteristic = homebridge.Characteristic; 11 | Service = homebridge.Service; 12 | 13 | this.log = log; 14 | this.name = name; 15 | this._dacp = dacp; 16 | this._timeout = undefined; 17 | 18 | this.createServices(features); 19 | } 20 | 21 | getService() { 22 | return this._services; 23 | } 24 | 25 | createServices(features) { 26 | const useInputControls = features['input-controls']; 27 | const useAlternateInputControls = features['alternate-input-controls']; 28 | 29 | if (useInputControls) { 30 | this._createInputControlService(); 31 | } 32 | else if (useAlternateInputControls) { 33 | this._createAlternateInputControls(useAlternateInputControls); 34 | } 35 | } 36 | 37 | _createInputControlService() { 38 | const svc = new Service.InputControlService(`${this.name} Remote`); 39 | 40 | const buttons = [ 41 | { c: Characteristic.TopMenuButton, title: 'Topmenu', commands: MacroCommands.topmenu }, 42 | { c: Characteristic.MenuButton, title: 'Menu', commands: MacroCommands.menu }, 43 | { c: Characteristic.SelectButton, title: 'Select', commands: MacroCommands.select }, 44 | { c: Characteristic.UpButton, title: 'Up', commands: MacroCommands.up }, 45 | { c: Characteristic.DownButton, title: 'Down', commands: MacroCommands.down }, 46 | { c: Characteristic.LeftButton, title: 'Left', commands: MacroCommands.left }, 47 | { c: Characteristic.RightButton, title: 'Right', commands: MacroCommands.right } 48 | ]; 49 | 50 | buttons.forEach(entry => { 51 | this._bindAssignmentHandler(svc, entry.c, entry.title, entry.commands); 52 | }); 53 | 54 | this._services = [svc]; 55 | } 56 | 57 | _createAlternateInputControls(alternateControls) { 58 | this._services = [ 59 | this._createKeyService(alternateControls, 'Topmenu', MacroCommands.topmenu), 60 | this._createKeyService(alternateControls, 'Menu', MacroCommands.menu), 61 | this._createKeyService(alternateControls, 'Select', MacroCommands.select), 62 | this._createKeyService(alternateControls, 'Up', MacroCommands.up), 63 | this._createKeyService(alternateControls, 'Down', MacroCommands.down), 64 | this._createKeyService(alternateControls, 'Left', MacroCommands.left), 65 | this._createKeyService(alternateControls, 'Right', MacroCommands.right) 66 | ].filter(s => s != undefined); 67 | } 68 | 69 | _createKeyService(features, title, commands) { 70 | if (features !== true && features instanceof Array) { 71 | if (!features.includes(title.toLowerCase())) { 72 | return; 73 | } 74 | } 75 | 76 | const svc = new Service.Switch(`${this.name} ${title}`, `input - ${title}`); 77 | this._bindAssignmentHandler(svc, Characteristic.On, title, commands); 78 | 79 | return svc; 80 | } 81 | 82 | _bindAssignmentHandler(svc, characteristic, title, commands) { 83 | const c = svc.getCharacteristic(characteristic); 84 | c.on('set', this._onKeyPress.bind(this, title, commands, c)); 85 | return c; 86 | } 87 | 88 | async _onKeyPress(title, commands, characteristic, value, callback) { 89 | if (!value) { 90 | callback(); 91 | return; 92 | } 93 | 94 | this.log(`Simulate '${title}' key press`); 95 | try { 96 | for (const cmd of commands) { 97 | await this._dacp.sendRemoteKey(cmd); 98 | } 99 | 100 | callback(); 101 | } 102 | catch (error) { 103 | this.log(`Failed to send the ${title} key request to the device.`); 104 | // TODO: Pass error back to HomeKit 105 | callback(); 106 | } 107 | 108 | this._resetCharacteristics(characteristic); 109 | } 110 | 111 | _resetCharacteristics(characteristic) { 112 | setTimeout(() => characteristic.updateValue(false), 100); 113 | } 114 | } 115 | 116 | module.exports = InputControlService; 117 | -------------------------------------------------------------------------------- /src/MacroCommands.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MacroCommands = { 4 | 'topmenu': [ 5 | 'topmenu' 6 | ], 7 | 'menu': [ 8 | 'menu' 9 | ], 10 | 'select': [ 11 | 'select' 12 | ], 13 | 'up': [ 14 | 'touchDown&time=0&point=20,275', 15 | 'touchMove&time=1&point=20,260', 16 | 'touchMove&time=2&point=20,245', 17 | 'touchMove&time=3&point=20,230', 18 | 'touchMove&time=4&point=20,215', 19 | 'touchMove&time=5&point=20,200', 20 | 'touchUp&time=6&point=20,185' 21 | ], 22 | 'down': [ 23 | 'touchDown&time=0&point=20,250', 24 | 'touchMove&time=1&point=20,255', 25 | 'touchMove&time=2&point=20,260', 26 | 'touchMove&time=3&point=20,265', 27 | 'touchMove&time=4&point=20,270', 28 | 'touchMove&time=5&point=20,275', 29 | 'touchUp&time=6&point=20,275' 30 | ], 31 | 'left': [ 32 | 'touchDown&time=0&point=75,100', 33 | 'touchMove&time=1&point=70,100', 34 | 'touchMove&time=3&point=65,100', 35 | 'touchMove&time=4&point=60,100', 36 | 'touchMove&time=5&point=55,100', 37 | 'touchMove&time=6&point=50,100', 38 | 'touchUp&time=7&point=50,100' 39 | ], 40 | 'right': [ 41 | 'touchDown&time=0&point=50,100', 42 | 'touchMove&time=1&point=55,100', 43 | 'touchMove&time=3&point=60,100', 44 | 'touchMove&time=4&point=65,100', 45 | 'touchMove&time=5&point=70,100', 46 | 'touchMove&time=6&point=75,100', 47 | 'touchUp&time=7&point=75,100' 48 | ] 49 | }; 50 | 51 | module.exports = MacroCommands; -------------------------------------------------------------------------------- /src/MacrosService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Characteristic, Service; 4 | 5 | const util = require('util'); 6 | 7 | const MacroCommands = require('./MacroCommands'); 8 | 9 | class MacrosService { 10 | 11 | constructor(homebridge, log, name, dacp, macros) { 12 | Characteristic = homebridge.Characteristic; 13 | Service = homebridge.Service; 14 | 15 | this.log = log; 16 | this.name = name; 17 | this._dacp = dacp; 18 | this._timeout = undefined; 19 | 20 | this._commands = { 21 | 'topmenu': this._execKey.bind(this, MacroCommands['topmenu']), 22 | 'menu': this._execKey.bind(this, MacroCommands['menu']), 23 | 'select': this._execKey.bind(this, MacroCommands['select']), 24 | 'up': this._execKey.bind(this, MacroCommands['up']), 25 | 'down': this._execKey.bind(this, MacroCommands['down']), 26 | 'left': this._execKey.bind(this, MacroCommands['left']), 27 | 'right': this._execKey.bind(this, MacroCommands['right']), 28 | 'wait5s': this._execWait.bind(this, 5) 29 | }; 30 | 31 | this._createServices(macros); 32 | } 33 | 34 | getService() { 35 | return this._services; 36 | } 37 | 38 | _createServices(macros) { 39 | 40 | this._services = []; 41 | 42 | for (const name of Object.keys(macros)) { 43 | const sw = this._createKeyService(name, macros[name]); 44 | this._services.push(sw); 45 | } 46 | } 47 | 48 | _createKeyService(title, macro) { 49 | const svc = new Service.Switch(`${title}`, `macro-${title}`); 50 | this._bindAssignmentHandler(svc, Characteristic.On, title, macro); 51 | return svc; 52 | } 53 | 54 | _bindAssignmentHandler(svc, characteristic, title, macro) { 55 | const c = svc.getCharacteristic(characteristic); 56 | c.on('set', this._onKeyPress.bind(this, title, macro, c)); 57 | } 58 | 59 | async _onKeyPress(title, macro, characteristic, value, callback) { 60 | if (!value) { 61 | callback(); 62 | return; 63 | } 64 | 65 | this.log(`Execute macro '${title}'`); 66 | try { 67 | callback(); 68 | 69 | for (const cmd of macro) { 70 | const fn = this._commands[cmd]; 71 | await fn(); 72 | } 73 | } 74 | catch (error) { 75 | this.log(`Failed to execute '${title}'. Error: ${util.inspect(error)}`); 76 | callback(error); 77 | } 78 | 79 | this._resetCharacteristics(characteristic); 80 | } 81 | 82 | _resetCharacteristics(characteristic) { 83 | setTimeout(() => characteristic.updateValue(false), 100); 84 | } 85 | 86 | async _execKey(commands) { 87 | for (const cmd of commands) { 88 | await this._dacp.sendRemoteKey(cmd); 89 | } 90 | } 91 | 92 | async _execWait(duration) { 93 | await new Promise(resolve => setTimeout(resolve, duration * 1000)); 94 | } 95 | } 96 | 97 | module.exports = MacrosService; 98 | -------------------------------------------------------------------------------- /src/MediaSkippingService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Characteristic, Service; 4 | 5 | class MediaSkippingService { 6 | 7 | constructor(homebridge, log, name, dacp) { 8 | Characteristic = homebridge.Characteristic; 9 | Service = homebridge.Service; 10 | 11 | this.log = log; 12 | this.name = name; 13 | this._dacp = dacp; 14 | 15 | this._service = this.createService(); 16 | } 17 | 18 | getService() { 19 | return this._service; 20 | } 21 | 22 | createService() { 23 | const svc = new Service.MediaSkippingService(this.name); 24 | 25 | svc.getCharacteristic(Characteristic.SkipForward) 26 | .on('set', this._triggerSkipForward.bind(this)); 27 | svc.getCharacteristic(Characteristic.SkipBackward) 28 | .on('set', this._triggerSkipBackward.bind(this)); 29 | 30 | return svc; 31 | } 32 | 33 | _triggerSkipForward(skip, callback) { 34 | if (!skip) { 35 | callback(); 36 | return; 37 | } 38 | 39 | this.log('Skipping forward.'); 40 | 41 | try { 42 | this._dacp.nextTrack(); 43 | this.log('Skipped to the next item.'); 44 | callback(); 45 | } 46 | catch (error) { 47 | this.log('Failed to send the playback request to the device.'); 48 | // TODO: Pass error back to HomeKit 49 | callback(); 50 | } 51 | 52 | this._resetCharacteristics(); 53 | } 54 | 55 | async _triggerSkipBackward(skip, callback) { 56 | if (!skip) { 57 | callback(); 58 | return; 59 | } 60 | 61 | this.log('Skipping backward.'); 62 | 63 | try { 64 | await this._dacp.prevTrack(); 65 | this.log('Skippped to the previous item.'); 66 | callback(); 67 | } 68 | catch (error) { 69 | this.log('Failed to send the playback request to the device.'); 70 | // TODO: Pass error back to HomeKit 71 | callback(); 72 | } 73 | 74 | this._resetCharacteristics(); 75 | } 76 | 77 | _resetCharacteristics() { 78 | setTimeout(() => { 79 | this._service.getCharacteristic(Characteristic.SkipForward) 80 | .updateValue(false); 81 | this._service.getCharacteristic(Characteristic.SkipBackward) 82 | .updateValue(false); 83 | }, 100); 84 | } 85 | } 86 | 87 | module.exports = MediaSkippingService; -------------------------------------------------------------------------------- /src/NowPlayingService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Characteristic, Service; 4 | 5 | const moment = require('moment'); 6 | const util = require('util'); 7 | const fs = require('fs'); 8 | 9 | class NowPlayingService { 10 | 11 | constructor(homebridge, log, name, dacp, artworkFile) { 12 | Characteristic = homebridge.Characteristic; 13 | Service = homebridge.Service; 14 | 15 | this.log = log; 16 | this.name = name; 17 | this._dacp = dacp; 18 | this._timeout = undefined; 19 | this._artworkFile = artworkFile; 20 | 21 | this.createService(); 22 | } 23 | 24 | getService() { 25 | return this._service; 26 | } 27 | 28 | createService() { 29 | this._service = new Service.NowPlayingService(this.name); 30 | this._resetCharacteristicsToDefaults(); 31 | } 32 | 33 | accessoryDown() { 34 | this._resetCharacteristicsToDefaults(); 35 | } 36 | 37 | _resetCharacteristicsToDefaults() { 38 | this._state = { 39 | track: '', 40 | album: '', 41 | artist: '', 42 | genre: '', 43 | type: -1, 44 | position: Number.NaN, 45 | duration: Number.NaN, 46 | playerState: 0 47 | }; 48 | 49 | this._updateCharacteristics(); 50 | } 51 | 52 | update(response) { 53 | const prevTrack = this._state.track; 54 | 55 | this._state = { 56 | track: this._getProperty(response, 'cann', ''), 57 | album: this._getProperty(response, 'canl', ''), 58 | artist: this._getProperty(response, 'cana', ''), 59 | genre: this._getProperty(response, 'cang', ''), 60 | mediaType: this._getProperty(response, 'cmmk', 0), 61 | remaining: this._getProperty(response, 'cant', Number.NaN), 62 | duration: this._getProperty(response, 'cast', Number.NaN), 63 | playerState: this._getProperty(response, 'caps', 0) 64 | }; 65 | 66 | this._updateCharacteristics(); 67 | 68 | if (prevTrack !== this._state.track) { 69 | this._updateArtwork(); 70 | } 71 | } 72 | 73 | _getProperty(response, prop, defaultValue) { 74 | if (response.hasOwnProperty(prop)) { 75 | return response[prop]; 76 | } 77 | 78 | return defaultValue; 79 | } 80 | 81 | _updateCharacteristics() { 82 | 83 | this._service.getCharacteristic(Characteristic.Title) 84 | .updateValue(this._state.track); 85 | 86 | this._service.getCharacteristic(Characteristic.Album) 87 | .updateValue(this._state.album); 88 | 89 | this._service.getCharacteristic(Characteristic.Artist) 90 | .updateValue(this._state.artist); 91 | 92 | this._service.getCharacteristic(Characteristic.Genre) 93 | .updateValue(this._state.genre); 94 | 95 | this._service.getCharacteristic(Characteristic.MediaType) 96 | .updateValue(this._state.mediaType); 97 | 98 | this._updatePosition(); 99 | this._respectPlayerState(); 100 | } 101 | 102 | _updatePosition() { 103 | this._setTime(Characteristic.MediaCurrentPosition, (this._state.duration - this._state.remaining) / 1000); 104 | this._setTime(Characteristic.MediaItemDuration, this._state.duration / 1000); 105 | } 106 | 107 | _setTime(characteristic, totalSeconds) { 108 | let minutes = Math.floor(totalSeconds / 60); 109 | let seconds = Math.round(totalSeconds - (minutes * 60)); 110 | 111 | let value = ''; 112 | if (!Number.isNaN(seconds)) { 113 | value = moment({ minutes: minutes, seconds: seconds }).format('mm:ss'); 114 | } 115 | 116 | this._service.getCharacteristic(characteristic) 117 | .updateValue(value); 118 | } 119 | 120 | _respectPlayerState() { 121 | if (this._state.playerState === 4) { 122 | if (!this._timeout) { 123 | this._timeout = setTimeout(this._requestPlaybackPosition.bind(this), 1000); 124 | } 125 | } 126 | else if (this._timeout) { 127 | clearTimeout(this._timeout); 128 | this._timeout = undefined; 129 | } 130 | } 131 | 132 | async _requestPlaybackPosition() { 133 | try { 134 | const response = await this._dacp.getProperty('dacp.playingtime'); 135 | this._timeout = undefined; 136 | 137 | this._state.remaining = this._getProperty(response, 'cant', Number.NaN); 138 | this._state.duration = this._getProperty(response, 'cast', Number.NaN); 139 | 140 | this._updateCharacteristics(); 141 | this._respectPlayerState(); 142 | } 143 | catch (e) { 144 | this.log(`Failed to retrieve the current playback position. Stopping continuous updates. Error: ${util.inspect(e)}`); 145 | this._resetCharacteristicsToDefaults(); 146 | } 147 | } 148 | 149 | _updateArtwork() { 150 | if (typeof this._artworkFile === 'string') { 151 | this._dacp.getArtwork() 152 | .then(response => { 153 | fs.writeFile(this._artworkFile, response.body, (err) => { 154 | if (err) { 155 | this.log(`Failed to write artwork: ${util.inspect(err)}`); 156 | this.log('Will not write any artwork from now on.'); 157 | this._artworkFile = undefined; 158 | } 159 | }); 160 | }) 161 | .catch(e => { 162 | this.log(`Artwork request failed. Error: ${util.inspect(e)}`); 163 | }); 164 | } 165 | } 166 | } 167 | 168 | module.exports = NowPlayingService; 169 | -------------------------------------------------------------------------------- /src/PlayerControlsService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class PlayerControlsService { 4 | 5 | constructor(homebridge, log, name, dacp, serviceCtor, characteristicCtor) { 6 | this.log = log; 7 | this.name = name; 8 | this._dacp = dacp; 9 | this._serviceCtor = serviceCtor; 10 | this._characteristicCtor = characteristicCtor; 11 | 12 | this._service = this.createService(); 13 | } 14 | 15 | getService() { 16 | return this._service; 17 | } 18 | 19 | createService() { 20 | const svc = new this._serviceCtor(this.name); 21 | 22 | svc.getCharacteristic(this._characteristicCtor) 23 | .on('get', this._getPlayState.bind(this)) 24 | .on('set', this._setPlayState.bind(this)); 25 | 26 | return svc; 27 | } 28 | 29 | update(response) { 30 | this._isPlaying = response.caps === 4; 31 | 32 | this._service.getCharacteristic(this._characteristicCtor) 33 | .updateValue(this._isPlaying); 34 | } 35 | 36 | _getPlayState(callback) { 37 | this.log(`Returning current playback state: ${this._isPlaying ? 'playing' : 'paused'}`); 38 | callback(undefined, this._isPlaying); 39 | } 40 | 41 | async _setPlayState(isPlaying, callback) { 42 | this.log(`Setting current playback state: ${isPlaying ? 'playing' : 'paused'}`); 43 | 44 | this._isPlaying = isPlaying; 45 | try { 46 | await this._dacp.play(); 47 | this.log('Playback status update done.'); 48 | callback(); 49 | } 50 | catch (error) { 51 | this.log('Failed to send the playback request to the device.'); 52 | callback(); 53 | } 54 | } 55 | } 56 | 57 | module.exports = PlayerControlsService; -------------------------------------------------------------------------------- /src/PlaylistService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Characteristic, Service; 4 | 5 | class PlaylistService { 6 | 7 | constructor(homebridge, log, name, dacp, config) { 8 | Characteristic = homebridge.Characteristic; 9 | Service = homebridge.Service; 10 | 11 | this.log = log; 12 | this.name = name; 13 | this._dacp = dacp; 14 | this._config = config; 15 | 16 | this._service = this.createService(); 17 | } 18 | 19 | getService() { 20 | return this._service; 21 | } 22 | 23 | createService() { 24 | const svc = new Service.PlaylistControlService('Playlist'); 25 | 26 | this._config.playlists.forEach(playlist => { 27 | const c = new Characteristic.StartPlaylist(playlist); 28 | c.on('set', this._startPlaylist.bind(this, playlist, c)) 29 | .updateValue(false); 30 | 31 | svc.addCharacteristic(c); 32 | }); 33 | 34 | return svc; 35 | } 36 | 37 | async _startPlaylist(playlist, characteristic, value, callback) { 38 | 39 | if (value === false) { 40 | this.done(callback, characteristic); 41 | return; 42 | } 43 | 44 | if (this._dacp.isAppleTV()) { 45 | this.log('Playlists not supported on AppleTV.'); 46 | this.done(callback, characteristic); 47 | return; 48 | } 49 | 50 | this.log(`Starting playlist ${playlist}`); 51 | 52 | try { 53 | await this._dacp.queue(playlist); 54 | await this._dacp.playQueue(); 55 | 56 | this.log('Playlist started.'); 57 | this.done(callback, characteristic); 58 | } 59 | catch (error) { 60 | this.log('Failed to start playlist.'); 61 | callback(error); 62 | } 63 | } 64 | 65 | done(callback, characteristic) { 66 | callback(); 67 | 68 | setTimeout(() => { 69 | characteristic.updateValue(false); 70 | }, 500); 71 | } 72 | } 73 | 74 | module.exports = PlaylistService; -------------------------------------------------------------------------------- /src/SpeakerService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | 5 | let Characteristic, Service; 6 | 7 | class SpeakerService { 8 | 9 | constructor(homebridge, log, name, dacp) { 10 | Characteristic = homebridge.Characteristic; 11 | Service = homebridge.Service; 12 | 13 | this.log = log; 14 | this.name = name; 15 | this._dacp = dacp; 16 | 17 | this._hasMuted = false; 18 | this._volume = 0; 19 | this._restoreVolume = undefined; 20 | 21 | this._service = this.createService(); 22 | } 23 | 24 | getService() { 25 | return this._service; 26 | } 27 | 28 | createService() { 29 | const speakerService = new Service.Speaker(this.name); 30 | 31 | speakerService.getCharacteristic(Characteristic.Volume) 32 | .on('get', this._getVolume.bind(this)) 33 | .on('set', this._setVolume.bind(this)) 34 | .updateValue(this._volume, undefined, undefined); 35 | 36 | speakerService.getCharacteristic(Characteristic.Mute) 37 | .on('get', this._getMute.bind(this)) 38 | .on('set', this._setMute.bind(this)) 39 | .updateValue(this._volume === 0, undefined, undefined); 40 | 41 | return speakerService; 42 | } 43 | 44 | async update() { 45 | try { 46 | const response = await this._dacp.getProperty('dmcp.volume'); 47 | if (response && response.cmvo !== undefined) { 48 | this._updateSpeakerCharacteristics(response.cmvo); 49 | } 50 | } 51 | catch (error) { 52 | this.log(`Failed to retrieve speaker volume ${util.inspect(error)}`); 53 | } 54 | } 55 | 56 | _updateSpeakerCharacteristics(volume) { 57 | this.log(`Updating characteristics with current volume: v=${volume}`); 58 | 59 | this._service.getCharacteristic(Characteristic.Volume) 60 | .updateValue(volume, undefined, undefined); 61 | this._service.getCharacteristic(Characteristic.Mute) 62 | .updateValue(volume === 0, undefined, undefined); 63 | 64 | this._volume = volume; 65 | if (volume != 0 && this._hasMuted) { 66 | this._unmutedViaDevice(); 67 | } 68 | } 69 | 70 | async _getVolume(callback) { 71 | try { 72 | const response = await this._dacp.getProperty('dmcp.volume'); 73 | response.cmvo = response.cmvo || 0; 74 | this.log(`Returning current volume: v=${response.cmvo}`); 75 | callback(undefined, response.cmvo); 76 | this._volume = response.cmvo; 77 | } 78 | catch (error) { 79 | callback(error, undefined); 80 | } 81 | } 82 | 83 | async _setVolume(volume, callback) { 84 | this.log(`Setting current volume to v=${volume}`); 85 | 86 | try { 87 | await this._dacp.setProperty('dmcp.volume', volume); 88 | callback(); 89 | this._updateSpeakerCharacteristics(volume); 90 | } 91 | catch (error) { 92 | callback(error); 93 | } 94 | } 95 | 96 | async _getMute(callback) { 97 | try { 98 | const response = await this._dacp.getProperty('dmcp.volume'); 99 | response.cmvo = response.cmvo || 0; 100 | this.log(`Returning current mute state: v=${response.cmvo === 0}`); 101 | callback(undefined, response.cmvo === 0); 102 | } 103 | catch (error) { 104 | callback(error, undefined); 105 | } 106 | } 107 | 108 | _setMute(muted, callback) { 109 | this.log(`Setting current mute state to ${muted ? 'muted' : 'unmuted'}`); 110 | 111 | if (muted) { 112 | this._mute(callback); 113 | } 114 | else { 115 | this._unmuteViaHomeKit(callback); 116 | } 117 | } 118 | 119 | _mute(callback) { 120 | this._hasMuted = true; 121 | this._restoreVolume = this._volume; 122 | 123 | this._setVolume(0, callback); 124 | } 125 | 126 | _unmuteViaHomeKit(callback) { 127 | // Unmuted via HomeKit, muted via HomeKit 128 | const volume = this._restoreVolume || 25; 129 | this._setVolume(volume, callback); 130 | this._hasMuted = false; 131 | this._restoreVolume = undefined; 132 | } 133 | 134 | _unmutedViaDevice() { 135 | // Unmuted via device, muted via HomeKit 136 | this._hasMuted = false; 137 | this._restoreVolume = undefined; 138 | } 139 | } 140 | 141 | module.exports = SpeakerService; 142 | -------------------------------------------------------------------------------- /src/artwork/ArtworkCamera.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ip = require('ip'); 4 | var spawn = require('child_process').spawn; 5 | 6 | class ArtworkCamera { 7 | 8 | constructor(log, hap, options) { 9 | this.log = log; 10 | this.hap = hap; 11 | this.options = options; 12 | 13 | 14 | this.services = []; 15 | this.streamControllers = []; 16 | 17 | this.pendingSessions = {}; 18 | this.ongoingSessions = {}; 19 | 20 | var numberOfStreams = options.maxStreams || 2; 21 | var videoResolutions = []; 22 | 23 | var maxWidth = options.maxWidth; 24 | var maxHeight = options.maxHeight; 25 | var maxFPS = (options.maxFPS > 30) ? 30 : options.maxFPS; 26 | 27 | if (maxWidth >= 320) { 28 | if (maxHeight >= 240) { 29 | videoResolutions.push([320, 240, maxFPS]); 30 | if (maxFPS > 15) { 31 | videoResolutions.push([320, 240, 15]); 32 | } 33 | } 34 | 35 | if (maxHeight >= 180) { 36 | videoResolutions.push([320, 180, maxFPS]); 37 | if (maxFPS > 15) { 38 | videoResolutions.push([320, 180, 15]); 39 | } 40 | } 41 | } 42 | 43 | if (maxWidth >= 480) { 44 | if (maxHeight >= 360) { 45 | videoResolutions.push([480, 360, maxFPS]); 46 | } 47 | 48 | if (maxHeight >= 270) { 49 | videoResolutions.push([480, 270, maxFPS]); 50 | } 51 | } 52 | 53 | if (maxWidth >= 640) { 54 | if (maxHeight >= 480) { 55 | videoResolutions.push([640, 480, maxFPS]); 56 | } 57 | 58 | if (maxHeight >= 360) { 59 | videoResolutions.push([640, 360, maxFPS]); 60 | } 61 | } 62 | 63 | if (maxWidth >= 1280) { 64 | if (maxHeight >= 960) { 65 | videoResolutions.push([1280, 960, maxFPS]); 66 | } 67 | 68 | if (maxHeight >= 720) { 69 | videoResolutions.push([1280, 720, maxFPS]); 70 | } 71 | } 72 | 73 | if (maxWidth >= 1920) { 74 | if (maxHeight >= 1080) { 75 | videoResolutions.push([1920, 1080, maxFPS]); 76 | } 77 | } 78 | 79 | let streamOptions = { 80 | proxy: false, // Requires RTP/RTCP MUX Proxy 81 | srtp: true, // Supports SRTP AES_CM_128_HMAC_SHA1_80 encryption 82 | video: { 83 | resolutions: videoResolutions, 84 | codec: { 85 | profiles: [0, 1, 2], // Enum, please refer StreamController.VideoCodecParamProfileIDTypes 86 | levels: [0, 1, 2] // Enum, please refer StreamController.VideoCodecParamLevelTypes 87 | } 88 | }, 89 | audio: { 90 | codecs: [ 91 | { 92 | type: 'OPUS', // Audio Codec 93 | samplerate: 24 // 8, 16, 24 KHz 94 | }, 95 | { 96 | type: 'AAC-eld', 97 | samplerate: 16 98 | } 99 | ] 100 | } 101 | }; 102 | 103 | this.createCameraControlService(); 104 | this._createStreamControllers(numberOfStreams, streamOptions); 105 | } 106 | 107 | createCameraControlService() { 108 | var controlService = new this.hap.Service.CameraControl(); 109 | this.services.push(controlService); 110 | } 111 | 112 | _createStreamControllers(maxStreams, streamOptions) { 113 | for (var i = 0; i < maxStreams; i++) { 114 | var streamController = new this.hap.StreamController(i, streamOptions, this); 115 | 116 | this.services.push(streamController.service); 117 | this.streamControllers.push(streamController); 118 | } 119 | } 120 | 121 | handleCloseConnection(connectionID) { 122 | this.streamControllers.forEach(function (controller) { 123 | controller.handleCloseConnection(connectionID); 124 | }); 125 | } 126 | 127 | handleSnapshotRequest(request, callback) { 128 | const vf = `scale=w='min(${request.width},iw)':h='min(${request.height},ih)':force_original_aspect_ratio=decrease,pad=${request.width}:${request.height}:(ow-iw)/2:(oh-ih)/2`; 129 | const ffmpegCommand = `-i ${this.options.artworkImageSource} -t 1 -vf ${vf} -f image2 -`; 130 | 131 | this.log(`Capturing still image via ffmpeg with options: ${ffmpegCommand}`); 132 | 133 | let ffmpeg = spawn('ffmpeg', ffmpegCommand.split(' '), { env: process.env }); 134 | var imageBuffer = Buffer(0); 135 | 136 | ffmpeg.stdout.on('data', function (data) { 137 | imageBuffer = Buffer.concat([imageBuffer, data]); 138 | }); 139 | ffmpeg.on('close', function () { 140 | callback(undefined, imageBuffer); 141 | }); 142 | } 143 | 144 | prepareStream(request, callback) { 145 | var sessionInfo = {}; 146 | 147 | let sessionID = request['sessionID']; 148 | let targetAddress = request['targetAddress']; 149 | 150 | sessionInfo['address'] = targetAddress; 151 | 152 | var response = {}; 153 | 154 | let videoInfo = request['video']; 155 | if (videoInfo) { 156 | let targetPort = videoInfo['port']; 157 | let srtp_key = videoInfo['srtp_key']; 158 | let srtp_salt = videoInfo['srtp_salt']; 159 | 160 | let videoResp = { 161 | port: targetPort, 162 | ssrc: 1, 163 | srtp_key: srtp_key, 164 | srtp_salt: srtp_salt 165 | }; 166 | 167 | response['video'] = videoResp; 168 | 169 | sessionInfo['video_port'] = targetPort; 170 | sessionInfo['video_srtp'] = Buffer.concat([srtp_key, srtp_salt]); 171 | sessionInfo['video_ssrc'] = 1; 172 | } 173 | 174 | let audioInfo = request['audio']; 175 | if (audioInfo) { 176 | let targetPort = audioInfo['port']; 177 | let srtp_key = audioInfo['srtp_key']; 178 | let srtp_salt = audioInfo['srtp_salt']; 179 | 180 | let audioResp = { 181 | port: targetPort, 182 | ssrc: 1, 183 | srtp_key: srtp_key, 184 | srtp_salt: srtp_salt 185 | }; 186 | 187 | response['audio'] = audioResp; 188 | 189 | sessionInfo['audio_port'] = targetPort; 190 | sessionInfo['audio_srtp'] = Buffer.concat([srtp_key, srtp_salt]); 191 | sessionInfo['audio_ssrc'] = 1; 192 | } 193 | 194 | let currentAddress = ip.address(); 195 | var addressResp = { 196 | address: currentAddress 197 | }; 198 | 199 | if (ip.isV4Format(currentAddress)) { 200 | addressResp['type'] = 'v4'; 201 | } else { 202 | addressResp['type'] = 'v6'; 203 | } 204 | 205 | response['address'] = addressResp; 206 | this.pendingSessions[this.hap.uuid.unparse(sessionID)] = sessionInfo; 207 | 208 | callback(response); 209 | } 210 | 211 | handleStreamRequest(request) { 212 | var sessionID = request.sessionID; 213 | var requestType = request.type; 214 | if (sessionID) { 215 | let sessionIdentifier = this.hap.uuid.unparse(sessionID); 216 | 217 | if (requestType == 'start') { 218 | var sessionInfo = this.pendingSessions[sessionIdentifier]; 219 | if (sessionInfo) { 220 | var width = 1280; 221 | var height = 720; 222 | var fps = 30; 223 | var bitrate = 300; 224 | 225 | let videoInfo = request.video; 226 | if (videoInfo) { 227 | width = videoInfo.width; 228 | height = videoInfo.height; 229 | 230 | let expectedFPS = videoInfo.fps; 231 | if (expectedFPS < fps) { 232 | fps = expectedFPS; 233 | } 234 | 235 | bitrate = videoInfo['max_bit_rate']; 236 | } 237 | 238 | let targetAddress = sessionInfo.address; 239 | let targetVideoPort = sessionInfo.video_port; 240 | let videoKey = sessionInfo.video_srtp; 241 | let videoSsrc = sessionInfo.video_ssrc; 242 | 243 | const vf = `scale=w='min(${width},iw)':h='min(${height},ih)':force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`; 244 | const ffmpegCommand = `-v info -re -loop 1 -i ${this.options.artworkImageSource} -vf ${vf} -threads 0 -vcodec ${this.options.vcodec} -an -pix_fmt yuv420p -r ${fps} -f rawvideo -tune zerolatency -b:v ${bitrate}k -bufsize ${bitrate}k -payload_type 99 -ssrc ${videoSsrc} -f rtp -srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params ${videoKey.toString('base64')} srtp://${targetAddress}:${targetVideoPort}?rtcpport=${targetVideoPort}&localrtcpport=${targetVideoPort}&pkt_size=1378`; 245 | this.log(`Launching video stream for session ${sessionIdentifier} via ffmpeg with options: ${ffmpegCommand}`); 246 | 247 | let ffmpeg = spawn(this.options.binary, ffmpegCommand.split(' '), { env: process.env, stdio: 'inherit' }); 248 | this.ongoingSessions[sessionIdentifier] = ffmpeg; 249 | } 250 | 251 | delete this.pendingSessions[sessionIdentifier]; 252 | } else if (requestType == 'stop') { 253 | var ffmpegProcess = this.ongoingSessions[sessionIdentifier]; 254 | if (ffmpegProcess) { 255 | ffmpegProcess.kill('SIGKILL'); 256 | this.log(`Killing ffmpeg for session: ${sessionIdentifier}`); 257 | } 258 | 259 | delete this.ongoingSessions[sessionIdentifier]; 260 | } 261 | } 262 | } 263 | } 264 | 265 | module.exports = ArtworkCamera; -------------------------------------------------------------------------------- /src/daap/Decoder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const daap = { 4 | 'abal': { 5 | 'name': 'daap.browsealbumlistung', 6 | 'type': 'list' 7 | }, 8 | 'abar': { 9 | 'name': 'daap.browseartistlisting', 10 | 'type': 'list' 11 | }, 12 | 'abcp': { 13 | 'name': 'daap.browsecomposerlisting', 14 | 'type': 'list' 15 | }, 16 | 'abgn': { 17 | 'name': 'daap.browsegenrelisting', 18 | 'type': 'list' 19 | }, 20 | 'abpl': { 21 | 'name': 'daap.baseplaylist', 22 | 'type': 'byte' 23 | }, 24 | 'abro': { 25 | 'name': 'daap.databasebrowse', 26 | 'type': 'list' 27 | }, 28 | 'adbs': { 29 | 'name': 'daap.databasesongs', 30 | 'type': 'list' 31 | }, 32 | 'aelb': { 33 | 'name': 'unknown', 34 | 'type': 'byte' 35 | }, 36 | 'aeCR': { 37 | 'name': 'com.apple.itunes.content-rating', 38 | 'type': 'string' 39 | }, 40 | 'aeCS': { 41 | 'name': 'com.apple.itunes.artworkchecksum', 42 | 'type': 'int' 43 | }, 44 | 'aeDL': { 45 | 'name': 'com.apple.itunes.drm-downloader-user-id', 46 | 'type': 'long' 47 | }, 48 | 'aeDP': { 49 | 'name': 'com.apple.itunes.drm-platform-id', 50 | 'type': 'int' 51 | }, 52 | 'aeDR': { 53 | 'name': 'com.apple.itunes.drm-user-id', 54 | 'type': 'long' 55 | }, 56 | 'aeDV': { 57 | 'name': 'com.apple.itunes.drm-versions', 58 | 'type': 'int' 59 | }, 60 | 'aeEN': { 61 | 'name': 'com.apple.itunes.episode-num-str', 62 | 'type': 'int' 63 | }, 64 | 'aeES': { 65 | 'name': 'com.apple.itunes.episode-sort', 66 | 'type': 'int' 67 | }, 68 | 'aeFA': { 69 | 'name': 'com.apple.itunes.drm-family-id', 70 | 'type': 'long' 71 | }, 72 | 'aeFP': { 73 | 'name': 'com.apple.itunes.req-fplay', 74 | 'type': 'byte' 75 | }, 76 | 'aeFR': { 77 | 'name': 'unknown tag', 78 | 'type': 'byte' 79 | }, 80 | 'aeGD': { 81 | 'name': 'com.apple.itunes.gapless-enc-dr', 82 | 'type': 'int' 83 | }, 84 | 'aeGE': { 85 | 'name': 'com.apple.itunes.gapless-enc-del', 86 | 'type': 'int' 87 | }, 88 | 'aeGH': { 89 | 'name': 'com.apple.itunes.gapless-heur', 90 | 'type': 'int' 91 | }, 92 | 'aeGR': { 93 | 'name': 'com.apple.itunes.gapless-resy', 94 | 'type': 'long' 95 | }, 96 | 'aeGU': { 97 | 'name': 'com.apple.itunes.gapless-dur', 98 | 'type': 'long' 99 | }, 100 | 'aeGs': { 101 | 'name': 'com.apple.itunes.can-be-genius-seed', 102 | 'type': 'byte' 103 | }, 104 | 'aeHD': { 105 | 'name': 'com.apple.itunes.is-hd-video', 106 | 'type': 'byte' 107 | }, 108 | 'aeHV': { 109 | 'name': 'com.apple.itunes.has-video', 110 | 'type': 'byte' 111 | }, 112 | 'aeK1': { 113 | 'description': '', 114 | 'name': 'com.apple.itunes.drm-key1-id', 115 | 'type': 'long' 116 | }, 117 | 'aeK2': { 118 | 'name': 'com.apple.itunes.drm-key2-id', 119 | 'type': 'long' 120 | }, 121 | 'aeMK': { 122 | 'name': 'com.apple.itunes.mediakind', 123 | 'type': 'byte' 124 | }, 125 | 'aeMX': { 126 | 'name': 'com.apple.itunes.movie-info-xml', 127 | 'type': 'string' 128 | }, 129 | 'aeMk': { 130 | 'name': 'com.apple.itunes.extended-media-kind', 131 | 'type': 'byte' 132 | }, 133 | 'aeMQ': { 134 | 'name': 'aeMQ', 135 | 'type': 'byte' 136 | }, 137 | 'aeND': { 138 | 'name': 'com.apple.itunes.non-drm-user-id', 139 | 'type': 'long' 140 | }, 141 | 'aeNV': { 142 | 'description': '', 143 | 'name': 'com.apple.itunes.norm-volume', 144 | 'type': 'int' 145 | }, 146 | 'aePC': { 147 | 'description': '', 148 | 'name': 'com.apple.itunes.is-podcast', 149 | 'type': 'byte' 150 | }, 151 | 'aePP': { 152 | 'name': 'com.apple.itunes.is-podcast-playlist', 153 | 'type': 'byte' 154 | }, 155 | 'aePS': { 156 | 'name': 'com.apple.itunes.special-playlist', 157 | 'type': 'byte' 158 | }, 159 | 'aeSE': { 160 | 'name': 'com.apple.itunes.store-pers-id', 161 | 'type': 'long' 162 | }, 163 | 'aeSG': { 164 | 'name': 'com.apple.itunes.saved-genius', 165 | 'type': 'byte' 166 | }, 167 | 'aeSL': { 168 | 'name': 'unknown', 169 | 'type': 'byte' 170 | }, 171 | 'aeSN': { 172 | 'name': 'com.apple.itunes.series-name', 173 | 'type': 'string' 174 | }, 175 | 'aeSP': { 176 | 'name': 'com.apple.itunes.smart-playlist', 177 | 'type': 'byte' 178 | }, 179 | 'aeSR': { 180 | 'name': 'unknown', 181 | 'type': 'byte' 182 | }, 183 | 'aeSU': { 184 | 'name': 'com.apple.itunes.season-num', 185 | 'type': 'int' 186 | }, 187 | 'aeSV': { 188 | 'name': 'com.apple.itunes.music-sharing-version', 189 | 'type': 'version' 190 | }, 191 | 'aeSX': { 192 | 'name': 'unknown tag', 193 | 'type': 'long' 194 | }, 195 | 'aeTr': { 196 | 'name': 'unknown tag', 197 | 'type': 'byte' 198 | }, 199 | 'aeXD': { 200 | 'name': 'com.apple.itunes.xid', 201 | 'type': 'string' 202 | }, 203 | 'aels': { 204 | 'name': 'com.apple.itunes.liked-state', 205 | 'type': 'byte' 206 | }, 207 | 'agrp': { 208 | 'name': 'daap.songgrouping', 209 | 'type': 'string' 210 | }, 211 | 'ajal': { 212 | 'name': 'com.apple.itunes.store.album-liked-state', 213 | 'type': 'byte' 214 | }, 215 | 'aply': { 216 | 'description': 'response to /databases/id/containers', 217 | 'name': 'daap.databaseplaylists', 218 | 'type': 'struct' 219 | }, 220 | 'apro': { 221 | 'name': 'daap.protocolversion', 222 | 'type': 'version' 223 | }, 224 | 'apso': { 225 | 'description': 'response to /databases/id/miids/id/items', 226 | 'name': 'daap.playlistsongs', 227 | 'type': 'struct' 228 | }, 229 | 'arif': { 230 | 'name': 'daap.resolveinfo', 231 | 'type': 'struct' 232 | }, 233 | 'arsv': { 234 | 'name': 'daap.resolve', 235 | 'type': 'struct' 236 | }, 237 | 'asaa': { 238 | 'name': 'daap.songalbumartist', 239 | 'type': 'string' 240 | }, 241 | 'asac': { 242 | 'name': 'daap.songartworkcount', 243 | 'type': 'short' 244 | }, 245 | 'asai': { 246 | 'name': 'daap.songalbumid', 247 | 'type': 'long' 248 | }, 249 | 'asal': { 250 | 'description': 'the song ones should be self exp.', 251 | 'name': 'daap.songalbum', 252 | 'type': 'string' 253 | }, 254 | 'asar': { 255 | 'name': 'daap.songartist', 256 | 'type': 'string' 257 | }, 258 | 'asas': { 259 | 'name': 'daap.songalbumuserratingstatus', 260 | 'type': 'byte' 261 | }, 262 | 'asbk': { 263 | 'name': 'daap.bookmarkable', 264 | 'type': 'byte' 265 | }, 266 | 'asbr': { 267 | 'name': 'daap.songbitrate', 268 | 'type': 'short' 269 | }, 270 | 'asbt': { 271 | 'name': 'daap.songsbeatsperminute', 272 | 'type': 'short' 273 | }, 274 | 'ascd': { 275 | 'name': 'daap.songcodectype', 276 | 'type': 'int' 277 | }, 278 | 'ascm': { 279 | 'name': 'daap.songcomment', 280 | 'type': 'string' 281 | }, 282 | 'ascn': { 283 | 'name': 'daap.songcontentdescription', 284 | 'type': 'string' 285 | }, 286 | 'asco': { 287 | 'name': 'daap.songcompilation', 288 | 'type': 'byte' 289 | }, 290 | 'ascp': { 291 | 'name': 'daap.songcomposer', 292 | 'type': 'string' 293 | }, 294 | 'ascr': { 295 | 'name': 'daap.songcontentrating', 296 | 'type': 'byte' 297 | }, 298 | 'ascs': { 299 | 'name': 'daap.songcodecsubtype', 300 | 'type': 'int' 301 | }, 302 | 'asct': { 303 | 'name': 'daap.songcategory', 304 | 'type': 'string' 305 | }, 306 | 'asda': { 307 | 'name': 'daap.songdateadded', 308 | 'type': 'date' 309 | }, 310 | 'asdb': { 311 | 'name': 'daap.songdisabled', 312 | 'type': 'byte' 313 | }, 314 | 'asdc': { 315 | 'name': 'daap.songdisccount', 316 | 'type': 'short' 317 | }, 318 | 'asdk': { 319 | 'name': 'daap.songdatakind', 320 | 'type': 'byte' 321 | }, 322 | 'asdm': { 323 | 'name': 'daap.songdatemodified', 324 | 'type': 'date' 325 | }, 326 | 'asdn': { 327 | 'name': 'daap.songdiscnumber', 328 | 'type': 'short' 329 | }, 330 | 'asdt': { 331 | 'name': 'daap.songdescription', 332 | 'type': 'string' 333 | }, 334 | 'ased': { 335 | 'name': 'daap.songextradata', 336 | 'type': 'short' 337 | }, 338 | 'aseq': { 339 | 'name': 'daap.songeqpreset', 340 | 'type': 'string' 341 | }, 342 | 'ases': { 343 | 'name': 'daap.songexcludefromshuffle', 344 | 'type': 'byte' 345 | }, 346 | 'asfm': { 347 | 'name': 'daap.songformat', 348 | 'type': 'string' 349 | }, 350 | 'asgn': { 351 | 'name': 'daap.songgenre', 352 | 'type': 'string' 353 | }, 354 | 'asgp': { 355 | 'name': 'daap.songgapless', 356 | 'type': 'byte' 357 | }, 358 | 'asgr': { 359 | 'name': 'com.apple.itunes.gapless-resy', 360 | 'type': 'int' 361 | }, 362 | 'ashp': { 363 | 'name': 'daap.songhasbeenplayed', 364 | 'type': 'byte' 365 | }, 366 | 'askd': { 367 | 'name': 'daap.songlastskipdate', 368 | 'type': 'date' 369 | }, 370 | 'askp': { 371 | 'name': 'daap.songuserskipcount', 372 | 'type': 'int' 373 | }, 374 | 'aslr': { 375 | 'name': 'daap.songalbumuserrating', 376 | 'type': 'byte' 377 | }, 378 | 'asls': { 379 | 'name': 'daap.songlongsize', 380 | 'type': 'long' 381 | }, 382 | 'aspc': { 383 | 'name': 'daap.songuserplaycount', 384 | 'type': 'int' 385 | }, 386 | 'aspl': { 387 | 'name': 'daap.songdateplayed', 388 | 'type': 'date' 389 | }, 390 | 'aspu': { 391 | 'name': 'daap.songpodcasturl', 392 | 'type': 'string' 393 | }, 394 | 'asri': { 395 | 'name': 'daap.songartistid', 396 | 'type': 'long' 397 | }, 398 | 'asrs': { 399 | 'name': 'daap.songuserratingstatus', 400 | 'type': 'byte' 401 | }, 402 | 'asrv': { 403 | 'name': 'daap.songrelativevolume', 404 | 'type': 'byte' 405 | }, 406 | 'assa': { 407 | 'name': 'daap.sortartist', 408 | 'type': 'string' 409 | }, 410 | 'assc': { 411 | 'name': 'daap.sortcomposer', 412 | 'type': 'string' 413 | }, 414 | 'asse': { 415 | 'name': 'unknown tag', 416 | 'type': 'int' 417 | }, 418 | 'assl': { 419 | 'name': 'daap.sortalbumartist', 420 | 'type': 'string' 421 | }, 422 | 'assn': { 423 | 'name': 'daap.sortname', 424 | 'type': 'string' 425 | }, 426 | 'assp': { 427 | 'description': '(in milliseconds)', 428 | 'name': 'daap.songstoptime', 429 | 'type': 'int' 430 | }, 431 | 'assr': { 432 | 'name': 'daap.songsamplerate', 433 | 'type': 'int' 434 | }, 435 | 'asss': { 436 | 'name': 'daap.sortseriesname', 437 | 'type': 'string' 438 | }, 439 | 'asst': { 440 | 'description': '(in milliseconds)', 441 | 'name': 'daap.songstarttime', 442 | 'type': 'int' 443 | }, 444 | 'assu': { 445 | 'name': 'daap.sortalbum', 446 | 'type': 'string' 447 | }, 448 | 'assz': { 449 | 'name': 'daap.songsize', 450 | 'type': 'int' 451 | }, 452 | 'astc': { 453 | 'name': 'daap.songtrackcount', 454 | 'type': 'short' 455 | }, 456 | 'astm': { 457 | 'description': '(in milliseconds)', 458 | 'name': 'daap.songtime', 459 | 'type': 'int' 460 | }, 461 | 'astn': { 462 | 'name': 'daap.songtracknumber', 463 | 'type': 'short' 464 | }, 465 | 'asul': { 466 | 'name': 'daap.songdataurl', 467 | 'type': 'string' 468 | }, 469 | 'asur': { 470 | 'name': 'daap.songuserrating', 471 | 'type': 'byte' 472 | }, 473 | 'asyr': { 474 | 'name': 'daap.songyear', 475 | 'type': 'short' 476 | }, 477 | 'ated': { 478 | 'name': 'daap.supportsextradata', 479 | 'type': 'short' 480 | }, 481 | 'atSV': { 482 | 'name': 'unknown', 483 | 'type': 'version' 484 | }, 485 | 'avdb': { 486 | 'description': 'response to a /databases', 487 | 'name': 'daap.serverdatabases', 488 | 'type': 'struct' 489 | }, 490 | 'cmst': { 491 | 'name': 'dacp.playstatus', 492 | 'type': 'struct' 493 | }, 494 | 'cafe': { 495 | 'name': 'dacp.fullscreenenabled', 496 | 'type': 'byte' 497 | }, 498 | 'cafs': { 499 | 'name': 'dacp.fullscreen', 500 | 'type': 'byte' 501 | }, 502 | 'caks': { 503 | 'name': 'dacp.unknown', 504 | 'type': 'byte' 505 | }, 506 | 'cana': { 507 | 'name': 'dacp.nowplayingartist', 508 | 'type': 'string' 509 | }, 510 | 'cang': { 511 | 'name': 'dacp.nowplayinggenre', 512 | 'type': 'string' 513 | }, 514 | 'canl': { 515 | 'name': 'dacp.nowplayingalbum', 516 | 'type': 'string' 517 | }, 518 | 'canp': { 519 | 'name': 'dacp.nowplayingids', 520 | 'type': 'int' 521 | }, 522 | 'caps': { 523 | 'name': 'dacp.playerstate', 524 | 'type': 'byte' 525 | }, 526 | 'casc': { 527 | 'name': 'dacp.unknown', 528 | 'type': 'byte' 529 | }, 530 | 'cash': { 531 | 'name': 'dacp.shufflestate', 532 | 'type': 'byte' 533 | }, 534 | 'casu': { 535 | 'name': 'dacp.su', 536 | 'type': 'byte' 537 | }, 538 | 'carp': { 539 | 'name': 'dacp.repeatstatus', 540 | 'type': 'byte' 541 | }, 542 | 'caar': { 543 | 'type': 'int' 544 | }, 545 | 'caas': { 546 | 'type': 'int' 547 | }, 548 | 'cacr': { 549 | 'name': 'Cue response', 550 | 'type': 'struct', 551 | }, 552 | 'cant': { 553 | 'name': 'dacp.remainingtime', 554 | 'type': 'int' 555 | }, 556 | 'cast': { 557 | 'name': 'dacp.tracklength', 558 | 'type': 'int' 559 | }, 560 | 'cann': { 561 | 'name': 'daap.nowplayingtrack', 562 | 'type': 'string' 563 | }, 564 | 'casa': { 565 | 'name': 'unknown tag', 566 | 'type': 'int' 567 | }, 568 | 'cavc': { 569 | 'type': 'byte' 570 | }, 571 | 'cave': { 572 | 'name': 'dacp.visualizerenabled', 573 | 'type': 'byte' 574 | }, 575 | 'cavs': { 576 | 'name': 'dacp.visualizer', 577 | 'type': 'byte' 578 | }, 579 | 'ceGS': { 580 | 'name': 'com.apple.itunes.genius-selectable', 581 | 'type': 'byte' 582 | }, 583 | 'ceQu': { 584 | 'name': 'ceQu', 585 | 'type': 'byte' 586 | }, 587 | 'cmgt': { 588 | 'name': 'dmcp.getpropertyresponse', 589 | 'type': 'struct' 590 | }, 591 | 'cmmk': { 592 | 'name': 'dmcp.mediakind', 593 | 'type': 'int' 594 | }, 595 | 'cmsr': { 596 | 'name': 'daap.revisionnumber', 597 | 'type': 'int' 598 | }, 599 | 'cmvo': { 600 | 'name': 'dmcp.volume', 601 | 'type': 'int' 602 | }, 603 | 'mbcl': { 604 | 'name': 'dmap.bag', 605 | 'type': 'list' 606 | }, 607 | 'mccr': { 608 | 'description': 'the response to the content-codes request', 609 | 'name': 'dmap.contentcodesresponse', 610 | 'type': 'struct' 611 | }, 612 | 'mcna': { 613 | 'description': 'the full name of the code', 614 | 'name': 'dmap.contentcodesname', 615 | 'type': 'string' 616 | }, 617 | 'mcnm': { 618 | 'description': 'the four letter code', 619 | 'name': 'dmap.contentcodesnumber', 620 | 'type': 'int' 621 | }, 622 | 'mcon': { 623 | 'description': 'an arbitrary container', 624 | 'name': 'dmap.container', 625 | 'type': 'struct' 626 | }, 627 | 'mctc': { 628 | 'name': 'dmap.containercount', 629 | 'type': 'int' 630 | }, 631 | 'mcti': { 632 | 'description': 'the id of an item in its container', 633 | 'name': 'dmap.containeritemid', 634 | 'type': 'int' 635 | }, 636 | 'mcty': { 637 | 'description': 'the type of the code (see appendix b for type values)', 638 | 'name': 'dmap.contentcodestype', 639 | 'type': 'short' 640 | }, 641 | 'mdcl': { 642 | 'description': 'a dictionary entry', 643 | 'name': 'dmap.dictionary', 644 | 'type': 'struct' 645 | }, 646 | 'mdst': { 647 | 'name': 'dmap.downloadstatus', 648 | 'type': 'byte' 649 | }, 650 | 'meia': { 651 | 'name': 'dmap.itemdateadded', 652 | 'type': 'date' 653 | }, 654 | 'meip': { 655 | 'name': 'dmap.itemdateplayed', 656 | 'type': 'date' 657 | }, 658 | 'merr': { 659 | 'name': 'dmap.error', 660 | 'type': 'struct' 661 | }, 662 | 'mers': { 663 | 'name': 'dmap.error2', 664 | 'type': 'int' 665 | }, 666 | 'mext': { 667 | 'name': 'dmap.objectextradata', 668 | 'type': 'short' 669 | }, 670 | 'miid': { 671 | 'description': 'an item\'s id', 672 | 'name': 'dmap.itemid', 673 | 'type': 'int' 674 | }, 675 | 'mikd': { 676 | 'description': 'the kind of item. So far, only \'2\' has been seen, an audio file?', 677 | 'name': 'dmap.itemkind', 678 | 'type': 'byte' 679 | }, 680 | 'mimc': { 681 | 'description': 'number of items in a container', 682 | 'name': 'dmap.itemcount', 683 | 'type': 'int' 684 | }, 685 | 'minm': { 686 | 'description': 'an items name', 687 | 'name': 'dmap.itemname', 688 | 'type': 'string' 689 | }, 690 | 'mlcl': { 691 | 'description': 'a list', 692 | 'name': 'dmap.listing', 693 | 'type': 'list' 694 | }, 695 | 'mlid': { 696 | 'description': 'the session id for the login session', 697 | 'name': 'dmap.sessionid', 698 | 'type': 'int' 699 | }, 700 | 'mlit': { 701 | 'description': 'a single item in said list', 702 | 'name': 'dmap.listingitem', 703 | 'type': 'struct' 704 | }, 705 | 'mlog': { 706 | 'description': 'response to a /login', 707 | 'name': 'dmap.loginresponse', 708 | 'type': 'struct' 709 | }, 710 | 'mpco': { 711 | 'name': 'dmap.parentcontainerid', 712 | 'type': 'int' 713 | }, 714 | 'mper': { 715 | 'description': 'a persistent id', 716 | 'name': 'dmap.persistentid', 717 | 'type': 'ulong' 718 | }, 719 | 'mpro': { 720 | 'name': 'dmap.protocolversion', 721 | 'type': 'version' 722 | }, 723 | 'mrco': { 724 | 'description': 'number of items returned in a request', 725 | 'name': 'dmap.returnedcount', 726 | 'type': 'int' 727 | }, 728 | 'msal': { 729 | 'name': 'dmap.supportsuatologout', 730 | 'type': 'byte' 731 | }, 732 | 'msas': { 733 | 'name': 'dmap.authenticationschemes', 734 | 'type': 'byte' 735 | }, 736 | 'msau': { 737 | 'name': 'dmap.authenticationmethod', 738 | 'type': 'byte' 739 | }, 740 | 'msbr': { 741 | 'name': 'dmap.supportsbrowse', 742 | 'type': 'byte' 743 | }, 744 | 'mscu': { 745 | 'name': 'mscu', 746 | 'type': 'long' 747 | }, 748 | 'msdc': { 749 | 'name': 'dmap.databasescount', 750 | 'type': 'int' 751 | }, 752 | 'msed': { 753 | 'name': 'dmap.supportsedit', 754 | 'type': 'byte' 755 | }, 756 | 'msex': { 757 | 'name': 'dmap.supportsextensions', 758 | 'type': 'byte' 759 | }, 760 | 'msix': { 761 | 'name': 'dmap.supportsindex', 762 | 'type': 'byte' 763 | }, 764 | 'mslr': { 765 | 'name': 'dmap.loginrequired', 766 | 'type': 'byte' 767 | }, 768 | 'msma': { 769 | 'name': 'dmap.machineaddress', 770 | 'type': 'long' 771 | }, 772 | 'msml': { 773 | 'name': 'dmap.speakermachinelist', 774 | 'type': 'list' 775 | }, 776 | 'mspi': { 777 | 'name': 'dmap.supportspersistentids', 778 | 'type': 'byte' 779 | }, 780 | 'msqy': { 781 | 'name': 'dmap.supportsquery', 782 | 'type': 'byte' 783 | }, 784 | 'msrs': { 785 | 'name': 'dmap.supportsresolve', 786 | 'type': 'byte' 787 | }, 788 | 'msrv': { 789 | 'description': 'response to a /server-info', 790 | 'name': 'dmap.serverinforesponse', 791 | 'type': 'struct' 792 | }, 793 | 'mstc': { 794 | 'name': 'dmap.utctime', 795 | 'type': 'int' 796 | }, 797 | 'mstm': { 798 | 'name': 'dmap.timeoutinterval', 799 | 'type': 'int' 800 | }, 801 | 'msto': { 802 | 'name': 'dmap.utcoffset', 803 | 'type': 'int' 804 | }, 805 | 'msts': { 806 | 'name': 'dmap.statusstring', 807 | 'type': 'string' 808 | }, 809 | 'mstt': { 810 | 'description': 'the response status code, these appear to be http status codes, e.g. 200', 811 | 'name': 'dmap.status', 812 | 'type': 'int' 813 | }, 814 | 'msup': { 815 | 'name': 'dmap.supportsupdate', 816 | 'type': 'byte' 817 | }, 818 | 'msur': { 819 | 'description': 'revision to use for requests', 820 | 'name': 'dmap.serverrevision', 821 | 'type': 'int' 822 | }, 823 | 'mtco': { 824 | 'name': 'dmap.specifiedtotalcount number of items in response to a request', 825 | 'type': 'int' 826 | }, 827 | 'mudl': { 828 | 'description': 'used in updates? (document soon)', 829 | 'name': 'dmap.deletedidlisting', 830 | 'type': 'list' 831 | }, 832 | 'mupd': { 833 | 'description': 'response to a /update', 834 | 'name': 'dmap.updateresponse', 835 | 'type': 'struct' 836 | }, 837 | 'musr': { 838 | 'name': 'dmap.serverrevision', 839 | 'type': 'int' 840 | }, 841 | 'muty': { 842 | 'name': 'dmap.updatetype', 843 | 'type': 'byte' 844 | }, 845 | 'ppro': { 846 | 'name': 'dpap.protocolversion', 847 | 'type': 'version' 848 | }, 849 | 'prsv': { 850 | 'name': 'daap.resolve', 851 | 'type': 'struct' 852 | } 853 | }; 854 | 855 | function _decodeList(buffer, start, end) { 856 | const result = []; 857 | 858 | for (let index = start; index <= end - 8;) { 859 | const code = buffer.toString('utf8', index, index + 4); 860 | const length = buffer.slice(index + 4, index + 8).readUInt32BE(0); 861 | const type = daap[code]; 862 | 863 | if (type) { 864 | let value = null; 865 | try { 866 | if (type.type === 'byte') { 867 | value = buffer.readUInt8(index + 8); 868 | } else if (type.type === 'date') { 869 | value = buffer.readIntBE(index + 8, 4); 870 | } else if (type.type === 'short') { 871 | value = buffer.readUInt16BE(index + 8); 872 | } else if (type.type === 'int') { 873 | value = buffer.readUInt32BE(index + 8); 874 | } else if (type.type === 'long') { 875 | value = buffer.readIntBE(index + 8, 8); 876 | } else if (type.type === 'list') { 877 | value = _decodeList(buffer, index + 8, index + 8 + length); 878 | } else if (type.type === 'struct') { 879 | value = _decode(buffer, index + 8, index + 8 + length); 880 | } else if (type.type === 'string') { 881 | value = buffer.toString('utf8', index + 8, index + 8 + length); 882 | } else if (type.type === 'version') { 883 | const v = buffer.readUInt32BE(index + 8); 884 | const major = Math.floor(v / 65536); 885 | const minor = v - major * 65536; 886 | value = { major: major, minor: minor }; 887 | } 888 | else { 889 | throw new Error('What?'); 890 | } 891 | 892 | result.push(value); 893 | } catch (e) { 894 | console.log('error on %s', code); 895 | console.error(e); 896 | } 897 | } 898 | else { 899 | console.log(`Skipping '${code}' - don't know how to parse. length=${length}`); 900 | } 901 | 902 | index += 8 + length; 903 | } 904 | 905 | return result; 906 | } 907 | 908 | function _decode(buffer, start, end) { 909 | const result = {}; 910 | 911 | for (let index = start; index <= end - 8;) { 912 | const code = buffer.toString('utf8', index, index + 4); 913 | const length = buffer.slice(index + 4, index + 8).readUInt32BE(0); 914 | const type = daap[code]; 915 | if (type) { 916 | let value = null; 917 | try { 918 | if (type.type === 'byte') { 919 | value = buffer.readUInt8(index + 8); 920 | } else if (type.type === 'date') { 921 | value = buffer.readIntBE(index + 8, 4); 922 | } else if (type.type === 'short') { 923 | value = buffer.readUInt16BE(index + 8); 924 | } else if (type.type === 'int') { 925 | value = buffer.readUInt32BE(index + 8); 926 | } else if (type.type === 'long') { 927 | value = buffer.readIntBE(index + 8, 8); 928 | } else if (type.type === 'ulong') { 929 | value = buffer.readUIntBE(index + 8, 8); 930 | } else if (type.type === 'list') { 931 | value = _decodeList(buffer, index + 8, index + 8 + length); 932 | } else if (type.type === 'struct') { 933 | value = _decode(buffer, index + 8, index + 8 + length); 934 | } else if (type.type === 'string') { 935 | value = buffer.toString('utf8', index + 8, index + 8 + length); 936 | } else if (type.type === 'version') { 937 | const v = buffer.readUInt32BE(index + 8); 938 | const major = Math.floor(v / 65536); 939 | const minor = v - major * 65536; 940 | value = { major: major, minor: minor }; 941 | } 942 | else { 943 | throw new Error('What?'); 944 | } 945 | 946 | result[code] = value; 947 | } catch (e) { 948 | console.log('error on %s', code); 949 | console.error(e); 950 | } 951 | } 952 | else { 953 | console.log(`Skipping '${code}' - don't know how to parse. length=${length}`); 954 | } 955 | 956 | index += 8 + length; 957 | } 958 | 959 | return result; 960 | } 961 | 962 | function decode(buffer) { 963 | return _decode(buffer, 0, buffer.length); 964 | } 965 | 966 | function encode(obj) { 967 | let b = Buffer.alloc(0); 968 | 969 | for (const key in obj) { 970 | if (obj.hasOwnProperty(key)) { 971 | let value = obj[key]; 972 | const valueType = typeof value; 973 | 974 | switch (valueType) { 975 | case 'number': { 976 | const valueBuf = Buffer.alloc(4); 977 | valueBuf.writeUInt32BE(value); 978 | value = valueBuf; 979 | break; 980 | } 981 | 982 | case 'string': 983 | value = Buffer.from(value, 'utf8'); 984 | break; 985 | } 986 | 987 | const valueKey = Buffer.from(key, 'ascii'); 988 | const valueLength = new Buffer(4); 989 | valueLength.writeUInt32BE(value.length); 990 | 991 | b = Buffer.concat([b, valueKey, valueLength, value]); 992 | } 993 | } 994 | 995 | return b; 996 | } 997 | 998 | module.exports = { 999 | decode: decode, 1000 | encode: encode 1001 | }; -------------------------------------------------------------------------------- /src/dacp/DacpBrowser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const EventEmitter = require('events').EventEmitter; 5 | 6 | const mdns = require('mdns'); 7 | const mdnsResolver = require('mdns-resolver'); 8 | 9 | class DacpBrowser extends EventEmitter { 10 | 11 | constructor(log) { 12 | super(); 13 | 14 | this.log = log; 15 | } 16 | 17 | start() { 18 | this.log('Starting DACP browser...'); 19 | 20 | const ServiceName = 'touch-able'; 21 | const ResolverSequence = [ 22 | mdns.rst.DNSServiceResolve() 23 | ]; 24 | 25 | this._browser = mdns.createBrowser( 26 | mdns.tcp(ServiceName), 27 | { resolverSequence: ResolverSequence }); 28 | 29 | this._browser.on('serviceUp', this._onServiceUp.bind(this)); 30 | 31 | this._browser.on('serviceDown', service => { 32 | this.emit('serviceDown', service); 33 | }); 34 | 35 | this._browser.on('error', error => { 36 | this.emit('error', error); 37 | 38 | this.stop(); 39 | }); 40 | 41 | this._browser.start(); 42 | } 43 | 44 | stop() { 45 | if (this._browser) { 46 | this._browser.stop(); 47 | this._browser = undefined; 48 | } 49 | } 50 | 51 | _onServiceUp(service) { 52 | mdnsResolver.resolve4(service.host) 53 | .then(host => { 54 | service.host = host; 55 | return service; 56 | }) 57 | .catch(e => { 58 | this.log(`Failed to resolve service ${service.host} using IPv4 MDNS lookups.`); 59 | this.log(`Specific error: ${util.inspect(e)}`); 60 | 61 | return mdnsResolver.resolve6(service.host) 62 | .then(host => { 63 | service.host = `[${host}]`; 64 | return service; 65 | }); 66 | }) 67 | .catch(e => { 68 | this.log(`Failed to resolve service ${service.host} using IPv4 and IPv6 MDNS lookups.`); 69 | this.log('Discarding service.'); 70 | this.log(`Specific error: ${util.inspect(e)} `); 71 | }) 72 | .then(service => { 73 | this.emit('serviceUp', service); 74 | }); 75 | } 76 | } 77 | 78 | module.exports = DacpBrowser; -------------------------------------------------------------------------------- /src/dacp/DacpClient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events').EventEmitter; 4 | const util = require('util'); 5 | const debug = require('debug')('dacp-client'); 6 | 7 | const DacpConnection = require('./DacpConnection'); 8 | 9 | class DacpClient extends EventEmitter { 10 | 11 | constructor(log, settings) { 12 | super(); 13 | 14 | this.log = log; 15 | this._settings = settings; 16 | 17 | // Pool of connections to actually use 18 | this._connections = []; 19 | this._revisionNumber = 0; 20 | } 21 | 22 | async connect(settings) { 23 | if (this._settings) { 24 | debug('Can\'t connect an already active client.'); 25 | throw new Error('Can\'t connect an already active client.'); 26 | } 27 | this._settings = settings; 28 | 29 | return await this._withConnection(this.STATUS_CONNECTION, async (connection) => { 30 | const serverInfo = await connection.sendRequest('server-info'); 31 | if (!serverInfo || !serverInfo.msrv) { 32 | debug('Missing server info response container'); 33 | throw new Error('Missing server info response container'); 34 | } 35 | 36 | if (serverInfo.msrv.msdc > 0) { 37 | await this.getDatabases(); 38 | } 39 | 40 | this.emit('connected'); 41 | debug('connected'); 42 | 43 | return serverInfo.msrv; 44 | }); 45 | } 46 | 47 | async disconnect() { 48 | return await this._withConnection(this.STATUS_CONNECTION, async (connection) => { 49 | await connection.sendRequest('logout'); 50 | this._reset(); 51 | }); 52 | } 53 | 54 | isAppleTV() { 55 | // AppleTV doesn't provide the database required to play a playlist 56 | return this._sessionId !== undefined && this._databaseId === undefined; 57 | } 58 | 59 | async getDatabases() { 60 | return await this._withConnection(this.STATUS_CONNECTION, async (connection) => { 61 | try { 62 | const response = await connection.sendRequest('databases'); 63 | if (response.avdb && response.avdb.mlcl && response.avdb.mlcl[0]) { 64 | this._databaseId = response.avdb.mlcl[0].miid; 65 | this._databasePersistentId = response.avdb.mlcl[0].mper; 66 | } 67 | } 68 | catch (e) { 69 | // Apple TV doesn't support the /databases request, which also 70 | // indicates that we do not have playlist support or other stuff. 71 | } 72 | }); 73 | } 74 | 75 | 76 | async queue(name) { 77 | if (this._databaseId === undefined) { 78 | throw new Error('Not supported on Apple TV.'); 79 | } 80 | 81 | return await this._withConnection(this.CONTROL_CONNECTION, async (connection) => { 82 | 83 | const playlist = await this.getItem(name); 84 | if (playlist && playlist.miid) { 85 | await this.clearNowPlayingQueue(); 86 | 87 | await connection.sendRequest('ctrl-int/1/playqueue-edit', { 88 | 'command': 'add', 89 | 'query': `'dmap.itemid:${playlist.miid}'`, 90 | 'query-modifier': 'containers', 91 | 'mode': 3 92 | }); 93 | } 94 | }); 95 | } 96 | 97 | async getItem(name) { 98 | if (this._databaseId === undefined) { 99 | throw new Error('Not supported on Apple TV.'); 100 | } 101 | 102 | return await this._withConnection(this.CONTROL_CONNECTION, async (connection) => { 103 | const response = await connection.sendRequest(`databases/${this._databaseId}/containers`, { 104 | meta: 'all', 105 | query: `dmap.itemname:${name}` 106 | }); 107 | 108 | if (response && response.aply) { 109 | if (response.aply.mrco > 0) { 110 | return response.aply.mlcl.find(mlit => mlit.minm === name); 111 | } 112 | } 113 | 114 | return undefined; 115 | }); 116 | } 117 | 118 | async clearNowPlayingQueue() { 119 | return await this._withConnection(this.CONTROL_CONNECTION, async (connection) => { 120 | return await connection.sendRequest('ctrl-int/1/playqueue-edit', { 121 | 'command': 'clear', 122 | 'mode': '0x6D61696E' 123 | }); 124 | }); 125 | } 126 | 127 | async requestPlayStatus() { 128 | return await this._withConnection(this.STATUS_CONNECTION, async (connection) => { 129 | const qs = {}; 130 | if (this._revisionNumber) { 131 | qs['revision-number'] = this._revisionNumber; 132 | } 133 | else { 134 | qs['revision-number'] = 0; 135 | } 136 | 137 | const response = await connection.sendRequest( 138 | 'ctrl-int/1/playstatusupdate', 139 | qs); 140 | 141 | if (response.cmst === undefined || response.cmst.cmsr === undefined) { 142 | const e = new Error('Missing revision number in play status update response'); 143 | e.response = response; 144 | throw e; 145 | } 146 | 147 | this._revisionNumber = response.cmst.cmsr; 148 | return response.cmst; 149 | }); 150 | } 151 | 152 | async getProperty(prop) { 153 | return await this._withConnection(this.CONTROL_CONNECTION, async (connection) => { 154 | const response = await connection.sendRequest( 155 | 'ctrl-int/1/getproperty', 156 | { 'properties': prop }); 157 | 158 | if (!response || !(response.cmgt || response.cmst)) { 159 | throw new Error('Missing get property response container'); 160 | } 161 | 162 | return response.cmgt || response.cmst; 163 | }); 164 | } 165 | 166 | async setProperty(prop, value) { 167 | return await this._withConnection(this.CONTROL_CONNECTION, async (connection) => { 168 | const data = {}; 169 | data[prop] = value; 170 | return connection.sendRequest('ctrl-int/1/setproperty', data); 171 | }); 172 | } 173 | 174 | async play() { 175 | return await this._withConnection(this.CONTROL_CONNECTION, async (connection) => { 176 | return connection.sendRequest('ctrl-int/1/playpause'); 177 | }); 178 | } 179 | 180 | async playQueue() { 181 | return await this._withConnection(this.CONTROL_CONNECTION, async (connection) => { 182 | return connection.sendRequest('ctrl-int/1/playqueue-edit', { 183 | 'command': 'playnow', 184 | 'index': 1 185 | }); 186 | }); 187 | } 188 | 189 | async nextTrack() { 190 | return await this._withConnection(this.CONTROL_CONNECTION, async (connection) => { 191 | return connection.sendRequest('ctrl-int/1/nextitem'); 192 | }); 193 | } 194 | 195 | async prevTrack() { 196 | return await this._withConnection(this.CONTROL_CONNECTION, async (connection) => { 197 | return connection.sendRequest('ctrl-int/1/previtem'); 198 | }); 199 | } 200 | 201 | async sendRemoteKey(key) { 202 | const data = { 203 | 'prompt-id': 1 204 | }; 205 | 206 | const body = { 207 | 'cmcc': 0x30, 208 | 'cmbe': key 209 | }; 210 | 211 | return await this._withConnection(this.CONTROL_CONNECTION, async (connection) => { 212 | return connection.sendRequest('ctrl-int/1/controlpromptentry', data, body); 213 | }); 214 | } 215 | 216 | async getArtwork() { 217 | return await this._withConnection(this.STATUS_CONNECTION, async (connection) => { 218 | return connection.sendRequest('ctrl-int/1/nowplayingartwork?', { 219 | mw: 1024, 220 | mh: 576 221 | }); 222 | }); 223 | } 224 | 225 | async _withConnection(type, action) { 226 | try { 227 | let c = this._connections[type]; 228 | if (!c) { 229 | this.log(`Creating ${type} connection to ${this._settings.host}`); 230 | c = await this._createConnection(); 231 | this._connections[type] = c; 232 | c.on('failed', this._onConnectionFailed.bind(this, type)); 233 | } 234 | 235 | return await action(c); 236 | } 237 | catch (e) { 238 | this._reset(); 239 | 240 | this.emit('failed', e); 241 | throw e; 242 | } 243 | } 244 | 245 | async _createConnection() { 246 | const c = new DacpConnection(this._settings.host, this._settings.pairing); 247 | this._sessionId = await c.connect(this._sessionId); 248 | return c; 249 | } 250 | 251 | _onConnectionFailed(type, error) { 252 | this.log(`Connection ${type} failed with error ${util.inspect(error)}`); 253 | this._reset(); 254 | 255 | this.emit('failed', error); 256 | } 257 | 258 | _reset() { 259 | this._connections.forEach(connection => { 260 | connection.close(); 261 | }); 262 | this._connections = []; 263 | this._settings = undefined; 264 | this._sessionId = undefined; 265 | this._databaseId = undefined; 266 | } 267 | } 268 | 269 | DacpClient.prototype.STATUS_CONNECTION = 'status'; 270 | DacpClient.prototype.CONTROL_CONNECTION = 'properties'; 271 | 272 | module.exports = DacpClient; 273 | -------------------------------------------------------------------------------- /src/dacp/DacpConnection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const EventEmitter = require('events').EventEmitter; 5 | const SequentialTaskQueue = require('sequential-task-queue').SequentialTaskQueue; 6 | const request = require('request'); 7 | const URL = require('url'); 8 | 9 | const util = require('util'); 10 | const debug = require('debug')('dacp'); 11 | 12 | const daap = require('../daap/Decoder'); 13 | 14 | class DacpConnection extends EventEmitter { 15 | 16 | constructor(host, pairing) { 17 | super(); 18 | 19 | // Force all the requests to stay on the same socket 20 | this._agent = new http.Agent({ 21 | keepAlive: true, 22 | maxFreeSockets: 1, 23 | maxSockets: 1 24 | }); 25 | 26 | this._host = host; 27 | this._pairing = pairing; 28 | 29 | this._sessionId = undefined; 30 | 31 | /** 32 | * 'disconnected': Disconnected 33 | * 'authenticating': Connecting to the DACP server 34 | * 'connected': Connected to the DACP server 35 | */ 36 | this._state = 'disconnected'; 37 | 38 | // Force all requests to happen in order 39 | this._taskQueue = new SequentialTaskQueue(); 40 | } 41 | 42 | async connect(sessionId) { 43 | if (this._state !== 'disconnected') { 44 | throw new Error('Can\'t login on an already active client.'); 45 | } 46 | 47 | if (sessionId) { 48 | this._sessionId = sessionId; 49 | this._setState('connected', this._sessionId); 50 | return this._sessionId; 51 | } 52 | 53 | this._setState('authenticating'); 54 | const response = await this._sendRequest('login', { 'pairing-guid': '0x' + this._pairing }); 55 | if (response.mlog && response.mlog.mlid) { 56 | this._sessionId = response.mlog.mlid; 57 | console.log(`Established connection to ${this._host} with session ID ${this._sessionId}`); 58 | 59 | this._revisionNumber = 1; 60 | this._setState('connected', this._sessionId); 61 | return this._sessionId; 62 | } 63 | else { 64 | throw new Error('Missing session ID in authentication response'); 65 | } 66 | } 67 | 68 | close() { 69 | // Not sure what to do here yet. 70 | } 71 | 72 | async sendRequest(relativeUri, data, body) { 73 | return this._taskQueue.push(() => { 74 | this._assertConnected(); 75 | return this._sendRequest(relativeUri, data, body); 76 | }); 77 | } 78 | 79 | _assertConnected() { 80 | if (this._state === 'disconnected') { 81 | throw new Error('Can\'t send requests to disconnected DACP servers.'); 82 | } 83 | } 84 | 85 | async _sendRequest(relativeUri, data, body) { 86 | return new Promise((resolve, reject) => { 87 | const uri = `http://${this._host}/${relativeUri}`; 88 | data = data || {}; 89 | 90 | if (this._sessionId) { 91 | data['session-id'] = this._sessionId; 92 | } 93 | 94 | let qs = ''; 95 | for (let key of Object.keys(data)) { 96 | qs += `${qs.length > 0 ? '&' : '?'}${key}=${data[key]}`; 97 | } 98 | 99 | const url = new URL.URL(uri); 100 | url.search = qs; 101 | 102 | const options = { 103 | encoding: null, 104 | url: url, 105 | headers: { 106 | 'Accept': '*/*', 107 | // 'Accept-Encoding': 'gzip', 108 | 'Viewer-Only-Client': '1', 109 | 'Client-DAAP-Version': '3.12', 110 | 'Client-ATV-Sharing-Version': '1.2', 111 | 'Client-iTunes-Sharing-Version': '3.10', 112 | 'User-Agent': 'TVRemote/186 CFNetwork/808.1.4 Darwin/16.1.0' 113 | }, 114 | agent: this._agent 115 | }; 116 | 117 | if (body) { 118 | options.body = daap.encode(body); 119 | } 120 | 121 | request(options, (error, response) => { 122 | // this.log(`Done ${JSON.stringify(options)}`); 123 | if (error || (response && response.statusCode >= 300)) { 124 | const e = { 125 | error: error, 126 | response: response, 127 | options: options 128 | }; 129 | 130 | if (response) { 131 | debug(`${url} failed: ${response.statusCode}`); 132 | } 133 | 134 | if (error) { 135 | debug(`${url} failed with error: ${error}`); 136 | this.emit('failed', error); 137 | } 138 | 139 | reject(e); 140 | return; 141 | } 142 | 143 | try { 144 | if (response.headers['content-type'] === 'application/x-dmap-tagged') { 145 | response = daap.decode(response.body); 146 | debug(`${url} responded: ${util.inspect(response)}`); 147 | } 148 | } 149 | catch (e) { 150 | this.emit('failed', error); 151 | reject(e); 152 | return; 153 | } 154 | 155 | resolve(response); 156 | }); 157 | }); 158 | } 159 | 160 | _setState(state) { 161 | if (state === 'failed') { 162 | this._sessionId = undefined; 163 | this._revisionNumber = 1; 164 | } 165 | 166 | if (state === this._state) { 167 | return; 168 | } 169 | 170 | this._state = state; 171 | 172 | const args = Array.from(arguments).slice(1); 173 | this.emit(state, ...args); 174 | } 175 | } 176 | 177 | module.exports = DacpConnection; 178 | -------------------------------------------------------------------------------- /src/dacp/DacpRemote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events').EventEmitter; 4 | const crypto = require('crypto'); 5 | const http = require('http'); 6 | const mdns = require('mdns'); 7 | const querystring = require('querystring'); 8 | 9 | class DacpRemote extends EventEmitter { 10 | 11 | constructor(config, log) { 12 | super(); 13 | 14 | this.config = config; 15 | this.log = log; 16 | 17 | var txtRecord = { 18 | 'DvNm': this.config.deviceName, 19 | 'DvTy': this.config.deviceType, 20 | 'Pair': this.config.pair, 21 | 'RemV': '10000', 22 | 'RemN': 'Remote', 23 | 'txtvers': '1' 24 | }; 25 | 26 | this._httpServer = http.createServer(this._handleRequest.bind(this)); 27 | this._httpServer.listen(); 28 | 29 | this._ad = mdns.createAdvertisement( 30 | mdns.tcp('_touch-remote'), 31 | this._httpServer.address().port, 32 | { txtRecord: txtRecord }); 33 | 34 | this._pairingHash = this._buildPairingHash(this.config.pair, this.config.pairPasscode); 35 | } 36 | 37 | _handleRequest(request, response) { 38 | 39 | const query = querystring.parse(request.url.substring(request.url.indexOf('?') + 1)); 40 | 41 | if (query.servicename && query.pairingcode && query.pairingcode.toUpperCase() == this._pairingHash.toUpperCase()) { 42 | this.emit('paired', { serviceName: query.servicename }); 43 | this._sendPairingSuccess(response); 44 | } 45 | else { 46 | this._sendPairingFailure(response); 47 | } 48 | 49 | } 50 | 51 | _sendPairingSuccess(response) { 52 | const values = { 53 | 'cmpg': this._newBuffer(this.config.pair, 'hex'), 54 | 'cmnm': this.config.deviceName, 55 | 'cmty': this.config.deviceType 56 | }; 57 | 58 | const buffers = []; 59 | 60 | for (var property in values) { 61 | if (values.hasOwnProperty(property)) { 62 | const valBuffer = this._newBuffer(values[property]); 63 | const lenBuffer = this._newBuffer(this._binaryLength(valBuffer.length)); 64 | const propBuffer = this._newBuffer(property); 65 | buffers.push(propBuffer, lenBuffer, valBuffer); 66 | } 67 | } 68 | 69 | var body = Buffer.concat(buffers); 70 | var header = Buffer.concat([this._newBuffer('cmpa'), this._newBuffer(this._binaryLength(body.length))]); 71 | var encoded = Buffer.concat([header, body]); 72 | 73 | response.writeHead(200, { 74 | 'Content-Length': encoded.length 75 | }); 76 | 77 | response.end(encoded); 78 | } 79 | 80 | _sendPairingFailure(response) { 81 | response.writeHead(404, { 82 | 'Content-Length': '0' 83 | }); 84 | response.end(); 85 | } 86 | 87 | _newBuffer(contents, type) { 88 | return typeof Buffer.from == 'function' ? Buffer.from(contents, type) : new Buffer(contents, type); 89 | } 90 | 91 | _binaryLength(length) { 92 | var ascii = ''; 93 | for (var i = 3; i >= 0; i--) { 94 | ascii += String.fromCharCode((length >> (8 * i)) & 255); 95 | } 96 | return ascii; 97 | } 98 | 99 | _buildPairingHash(pair, passcode) { 100 | var merged = pair; 101 | 102 | for (var ctr = 0; ctr < passcode.length; ctr++) 103 | merged += passcode[ctr] + '\x00'; 104 | 105 | return crypto.createHash('md5').update(merged).digest('hex'); 106 | } 107 | 108 | start() { 109 | this.log(`Starting DACP remote announcements for "${this.config.deviceName}" - pin "${this.config.pairPasscode}"...`); 110 | this._ad.start(); 111 | } 112 | 113 | stop() { 114 | this._ad.stop(); 115 | this._httpServer.close(); 116 | } 117 | } 118 | 119 | module.exports = DacpRemote; -------------------------------------------------------------------------------- /src/hap/InputControlTypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inherits = require('util').inherits; 4 | 5 | module.exports = { 6 | registerWith: function (hap) { 7 | 8 | const Characteristic = hap.Characteristic; 9 | const Service = hap.Service; 10 | 11 | //////////////////////////////////////////////////////////////////////////// 12 | // Top Menu Characteristic 13 | //////////////////////////////////////////////////////////////////////////// 14 | Characteristic.TopMenuButton = function () { 15 | Characteristic.call(this, 'Top Menu', Characteristic.TopMenuButton.UUID); 16 | this.setProps({ 17 | format: Characteristic.Formats.BOOL, 18 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 19 | }); 20 | this.value = this.getDefaultValue(); 21 | }; 22 | Characteristic.TopMenuButton.UUID = '53426A9B-1AB0-44CB-B88B-82D96EFC51CE'; 23 | inherits(Characteristic.TopMenuButton, Characteristic); 24 | 25 | //////////////////////////////////////////////////////////////////////////// 26 | // Menu Characteristic 27 | //////////////////////////////////////////////////////////////////////////// 28 | Characteristic.MenuButton = function () { 29 | Characteristic.call(this, 'Menu', Characteristic.MenuButton.UUID); 30 | this.setProps({ 31 | format: Characteristic.Formats.BOOL, 32 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 33 | }); 34 | this.value = this.getDefaultValue(); 35 | }; 36 | Characteristic.MenuButton.UUID = 'CB68261D-DB68-46B8-B1F0-5BDFEC872039'; 37 | inherits(Characteristic.MenuButton, Characteristic); 38 | 39 | //////////////////////////////////////////////////////////////////////////// 40 | // Select Characteristic 41 | //////////////////////////////////////////////////////////////////////////// 42 | Characteristic.SelectButton = function () { 43 | Characteristic.call(this, 'Select', Characteristic.SelectButton.UUID); 44 | this.setProps({ 45 | format: Characteristic.Formats.BOOL, 46 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 47 | }); 48 | this.value = this.getDefaultValue(); 49 | }; 50 | Characteristic.SelectButton.UUID = 'C67044BB-EE9F-4F72-9816-FEE962BE1EB1'; 51 | inherits(Characteristic.SelectButton, Characteristic); 52 | 53 | //////////////////////////////////////////////////////////////////////////// 54 | // Up Characteristic 55 | //////////////////////////////////////////////////////////////////////////// 56 | Characteristic.UpButton = function () { 57 | Characteristic.call(this, 'Up', Characteristic.UpButton.UUID); 58 | this.setProps({ 59 | format: Characteristic.Formats.BOOL, 60 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 61 | }); 62 | this.value = this.getDefaultValue(); 63 | }; 64 | Characteristic.UpButton.UUID = '3B005F2F-E2AE-4895-A37E-53280E2EA764'; 65 | inherits(Characteristic.UpButton, Characteristic); 66 | 67 | //////////////////////////////////////////////////////////////////////////// 68 | // Down Characteristic 69 | //////////////////////////////////////////////////////////////////////////// 70 | Characteristic.DownButton = function () { 71 | Characteristic.call(this, 'Down', Characteristic.DownButton.UUID); 72 | this.setProps({ 73 | format: Characteristic.Formats.BOOL, 74 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 75 | }); 76 | this.value = this.getDefaultValue(); 77 | }; 78 | Characteristic.DownButton.UUID = 'B17E1EC9-314B-46F1-97D5-0A371B662D2A'; 79 | inherits(Characteristic.DownButton, Characteristic); 80 | 81 | //////////////////////////////////////////////////////////////////////////// 82 | // Left Characteristic 83 | //////////////////////////////////////////////////////////////////////////// 84 | Characteristic.LeftButton = function () { 85 | Characteristic.call(this, 'Left', Characteristic.LeftButton.UUID); 86 | this.setProps({ 87 | format: Characteristic.Formats.BOOL, 88 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 89 | }); 90 | this.value = this.getDefaultValue(); 91 | }; 92 | Characteristic.LeftButton.UUID = '76261837-BFE3-413B-9803-36122EE1D994'; 93 | inherits(Characteristic.LeftButton, Characteristic); 94 | 95 | //////////////////////////////////////////////////////////////////////////// 96 | // Right Characteristic 97 | //////////////////////////////////////////////////////////////////////////// 98 | Characteristic.RightButton = function () { 99 | Characteristic.call(this, 'Right', Characteristic.RightButton.UUID); 100 | this.setProps({ 101 | format: Characteristic.Formats.BOOL, 102 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 103 | }); 104 | this.value = this.getDefaultValue(); 105 | }; 106 | Characteristic.RightButton.UUID = 'A3DECC2A-4852-4347-A548-9972E0490891'; 107 | inherits(Characteristic.RightButton, Characteristic); 108 | 109 | //////////////////////////////////////////////////////////////////////////// 110 | // Input Control Service 111 | //////////////////////////////////////////////////////////////////////////// 112 | Service.InputControlService = function (displayName, subtype) { 113 | Service.call(this, displayName, Service.InputControlService.UUID, subtype); 114 | 115 | // Required Characteristics 116 | this.addCharacteristic(Characteristic.TopMenuButton); 117 | this.addCharacteristic(Characteristic.MenuButton); 118 | this.addCharacteristic(Characteristic.SelectButton); 119 | this.addCharacteristic(Characteristic.UpButton); 120 | this.addCharacteristic(Characteristic.DownButton); 121 | this.addCharacteristic(Characteristic.LeftButton); 122 | this.addCharacteristic(Characteristic.RightButton); 123 | 124 | // Optional Characteristics 125 | this.addOptionalCharacteristic(Characteristic.Name); 126 | }; 127 | 128 | Service.InputControlService.UUID = '5F862E4E-9D42-4636-9F1E-0D4BC5572705'; 129 | inherits(Service.InputControlService, Service); 130 | } 131 | }; -------------------------------------------------------------------------------- /src/hap/MediaSkippingTypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inherits = require('util').inherits; 4 | 5 | module.exports = { 6 | registerWith: function (hap) { 7 | 8 | const Characteristic = hap.Characteristic; 9 | const Service = hap.Service; 10 | 11 | //////////////////////////////////////////////////////////////////////////// 12 | // SkipForward Characteristic 13 | //////////////////////////////////////////////////////////////////////////// 14 | Characteristic.SkipForward = function () { 15 | Characteristic.call(this, 'Skip >', Characteristic.SkipForward.UUID); 16 | this.setProps({ 17 | format: Characteristic.Formats.BOOL, 18 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 19 | }); 20 | this.value = this.getDefaultValue(); 21 | }; 22 | Characteristic.SkipForward.UUID = 'CD56B40B-F98B-4ACA-BF5E-4AD4E9C77D1C'; 23 | inherits(Characteristic.SkipForward, Characteristic); 24 | 25 | //////////////////////////////////////////////////////////////////////////// 26 | // SkipBackward Characteristic 27 | //////////////////////////////////////////////////////////////////////////// 28 | Characteristic.SkipBackward = function () { 29 | Characteristic.call(this, 'Skip <', Characteristic.SkipBackward.UUID); 30 | this.setProps({ 31 | format: Characteristic.Formats.BOOL, 32 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 33 | }); 34 | this.value = this.getDefaultValue(); 35 | }; 36 | Characteristic.SkipBackward.UUID = 'CFFE477D-70C8-4630-B33B-25073F137191'; 37 | inherits(Characteristic.SkipBackward, Characteristic); 38 | 39 | //////////////////////////////////////////////////////////////////////////// 40 | // Now Playing Service 41 | //////////////////////////////////////////////////////////////////////////// 42 | Service.MediaSkippingService = function (displayName, subtype) { 43 | Service.call(this, displayName, Service.MediaSkippingService.UUID, subtype); 44 | 45 | // Required Characteristics 46 | this.addCharacteristic(Characteristic.SkipForward); 47 | this.addCharacteristic(Characteristic.SkipBackward); 48 | 49 | // Optional Characteristics 50 | this.addOptionalCharacteristic(Characteristic.Name); 51 | }; 52 | 53 | Service.MediaSkippingService.UUID = '07163D16-8F0E-4B36-9AC4-18BE183B9EDE'; 54 | inherits(Service.MediaSkippingService, Service); 55 | } 56 | }; -------------------------------------------------------------------------------- /src/hap/NowPlayingTypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inherits = require('util').inherits; 4 | 5 | module.exports = { 6 | registerWith: function (hap) { 7 | 8 | const Characteristic = hap.Characteristic; 9 | const Service = hap.Service; 10 | 11 | //////////////////////////////////////////////////////////////////////////// 12 | // Title Characteristic 13 | //////////////////////////////////////////////////////////////////////////// 14 | Characteristic.Title = function () { 15 | Characteristic.call(this, 'Title', Characteristic.Title.UUID); 16 | this.setProps({ 17 | format: Characteristic.Formats.STRING, 18 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 19 | }); 20 | this.value = this.getDefaultValue(); 21 | }; 22 | Characteristic.Title.UUID = '00003001-0000-1000-8000-135D67EC4377'; 23 | inherits(Characteristic.Title, Characteristic); 24 | 25 | //////////////////////////////////////////////////////////////////////////// 26 | // Album Characteristic 27 | //////////////////////////////////////////////////////////////////////////// 28 | Characteristic.Album = function () { 29 | Characteristic.call(this, 'Album', Characteristic.Album.UUID); 30 | this.setProps({ 31 | format: Characteristic.Formats.STRING, 32 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 33 | }); 34 | this.value = this.getDefaultValue(); 35 | }; 36 | Characteristic.Album.UUID = '00003002-0000-1000-8000-135D67EC4377'; 37 | inherits(Characteristic.Album, Characteristic); 38 | 39 | //////////////////////////////////////////////////////////////////////////// 40 | // Artist Characteristic 41 | //////////////////////////////////////////////////////////////////////////// 42 | Characteristic.Artist = function () { 43 | Characteristic.call(this, 'Artist', Characteristic.Artist.UUID); 44 | this.setProps({ 45 | format: Characteristic.Formats.STRING, 46 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 47 | }); 48 | this.value = this.getDefaultValue(); 49 | }; 50 | Characteristic.Artist.UUID = '00003003-0000-1000-8000-135D67EC4377'; 51 | inherits(Characteristic.Artist, Characteristic); 52 | 53 | //////////////////////////////////////////////////////////////////////////// 54 | // Genre Characteristic 55 | //////////////////////////////////////////////////////////////////////////// 56 | Characteristic.Genre = function () { 57 | Characteristic.call(this, 'Genre', Characteristic.Genre.UUID); 58 | this.setProps({ 59 | format: Characteristic.Formats.STRING, 60 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 61 | }); 62 | this.value = this.getDefaultValue(); 63 | }; 64 | Characteristic.Genre.UUID = '8087750B-8B8C-451E-B907-8E3BAD8DCB1E'; 65 | inherits(Characteristic.Genre, Characteristic); 66 | 67 | //////////////////////////////////////////////////////////////////////////// 68 | // Media Type Characteristic 69 | //////////////////////////////////////////////////////////////////////////// 70 | Characteristic.MediaType = function () { 71 | Characteristic.call(this, 'Media Type', Characteristic.MediaType.UUID); 72 | this.setProps({ 73 | format: Characteristic.Formats.INT, 74 | maxValue: 64, 75 | minValue: 0, 76 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 77 | }); 78 | this.value = this.getDefaultValue(); 79 | }; 80 | Characteristic.MediaType.UUID = '9898982C-7B70-47AD-A81D-211BFE5AFBF2'; 81 | inherits(Characteristic.MediaType, Characteristic); 82 | 83 | //////////////////////////////////////////////////////////////////////////// 84 | // Position Characteristic 85 | //////////////////////////////////////////////////////////////////////////// 86 | Characteristic.MediaCurrentPosition = function () { 87 | Characteristic.call(this, 'Position', Characteristic.MediaCurrentPosition.UUID); 88 | this.setProps({ 89 | format: Characteristic.Formats.STRING, 90 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 91 | }); 92 | this.value = this.getDefaultValue(); 93 | }; 94 | Characteristic.MediaCurrentPosition.UUID = '00002007-0000-1000-8000-135D67EC4377'; 95 | inherits(Characteristic.MediaCurrentPosition, Characteristic); 96 | 97 | //////////////////////////////////////////////////////////////////////////// 98 | // Duration Characteristic 99 | //////////////////////////////////////////////////////////////////////////// 100 | Characteristic.MediaItemDuration = function () { 101 | Characteristic.call(this, 'Duration', Characteristic.MediaItemDuration.UUID); 102 | this.setProps({ 103 | format: Characteristic.Formats.STRING, // In seconds 104 | perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY] 105 | }); 106 | this.value = this.getDefaultValue(); 107 | }; 108 | Characteristic.MediaItemDuration.UUID = '00003005-0000-1000-8000-135D67EC4377'; 109 | inherits(Characteristic.MediaItemDuration, Characteristic); 110 | 111 | //////////////////////////////////////////////////////////////////////////// 112 | // Now Playing Service 113 | //////////////////////////////////////////////////////////////////////////// 114 | Service.NowPlayingService = function (displayName, subtype) { 115 | Service.call(this, displayName, Service.NowPlayingService.UUID, subtype); 116 | 117 | // Required Characteristics 118 | this.addCharacteristic(Characteristic.Title); 119 | 120 | // Optional Characteristics 121 | this.addOptionalCharacteristic(Characteristic.Album); 122 | this.addOptionalCharacteristic(Characteristic.Artist); 123 | this.addOptionalCharacteristic(Characteristic.Genre); 124 | this.addOptionalCharacteristic(Characteristic.MediaType); 125 | this.addOptionalCharacteristic(Characteristic.MediaCurrentPosition); 126 | this.addOptionalCharacteristic(Characteristic.MediaItemDuration); 127 | this.addOptionalCharacteristic(Characteristic.Name); 128 | }; 129 | 130 | Service.NowPlayingService.UUID = 'F7138C87-EABF-420A-BFF0-76FC04DD81CD'; 131 | inherits(Service.NowPlayingService, Service); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /src/hap/PlayerControlsTypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inherits = require('util').inherits; 4 | 5 | module.exports = { 6 | registerWith: function (hap) { 7 | 8 | const Characteristic = hap.Characteristic; 9 | const Service = hap.Service; 10 | 11 | //////////////////////////////////////////////////////////////////////////// 12 | // PlayPause Characteristic 13 | //////////////////////////////////////////////////////////////////////////// 14 | Characteristic.PlayPause = function () { 15 | Characteristic.call(this, 'Play', Characteristic.PlayPause.UUID); 16 | this.setProps({ 17 | format: Characteristic.Formats.BOOL, 18 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 19 | }); 20 | this.value = this.getDefaultValue(); 21 | }; 22 | Characteristic.PlayPause.UUID = 'BA16B86C-DC86-482A-A70C-CC9C924DB842'; 23 | inherits(Characteristic.PlayPause, Characteristic); 24 | 25 | //////////////////////////////////////////////////////////////////////////// 26 | // PlayerControlsService Service 27 | //////////////////////////////////////////////////////////////////////////// 28 | Service.PlayerControlsService = function (displayName, subtype) { 29 | Service.call(this, displayName, Service.PlayerControlsService.UUID, subtype); 30 | 31 | // Required Characteristics 32 | this.addCharacteristic(Characteristic.PlayPause); 33 | 34 | // Optional Characteristics 35 | this.addOptionalCharacteristic(Characteristic.Name); 36 | }; 37 | 38 | Service.PlayerControlsService.UUID = 'EFD51587-6F54-4093-9E8D-FA3975DCDCE6'; 39 | inherits(Service.PlayerControlsService, Service); 40 | } 41 | }; -------------------------------------------------------------------------------- /src/hap/PlaylistTypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inherits = require('util').inherits; 4 | 5 | module.exports = { 6 | registerWith: function (hap) { 7 | 8 | const Characteristic = hap.Characteristic; 9 | const Service = hap.Service; 10 | 11 | //////////////////////////////////////////////////////////////////////////// 12 | // PlayPause Characteristic 13 | //////////////////////////////////////////////////////////////////////////// 14 | Characteristic.StartPlaylist = function (displayName) { 15 | 16 | const uuid = hap.uuid.generate(displayName).toUpperCase(); 17 | Characteristic.call(this, displayName, uuid); 18 | 19 | this.setProps({ 20 | format: Characteristic.Formats.BOOL, 21 | perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] 22 | }); 23 | this.value = this.getDefaultValue(); 24 | }; 25 | inherits(Characteristic.StartPlaylist, Characteristic); 26 | 27 | //////////////////////////////////////////////////////////////////////////// 28 | // PlayerControlsService Service 29 | //////////////////////////////////////////////////////////////////////////// 30 | Service.PlaylistControlService = function (displayName, subtype) { 31 | Service.call(this, displayName, Service.PlaylistControlService.UUID, subtype); 32 | 33 | // Optional Characteristics 34 | this.addOptionalCharacteristic(Characteristic.Name); 35 | }; 36 | 37 | Service.PlaylistControlService.UUID = '24B5B813-8D9C-49C3-ABFB-EDE879A4FF99'; 38 | inherits(Service.PlaylistControlService, Service); 39 | } 40 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const version = require('../package.json').version; 4 | 5 | const isPi = require('detect-rpi'); 6 | 7 | const DacpAccessory = require('./DacpAccessory'); 8 | const DacpBrowser = require('./dacp/DacpBrowser'); 9 | const DacpRemote = require('./dacp/DacpRemote'); 10 | 11 | const MediaSkippingTypes = require('./hap/MediaSkippingTypes'); 12 | const NowPlayingTypes = require('./hap/NowPlayingTypes'); 13 | const PlayerControlTypes = require('./hap/PlayerControlsTypes'); 14 | const PlaylistTypes = require('./hap/PlaylistTypes'); 15 | const InputControlTypes = require('./hap/InputControlTypes'); 16 | 17 | const ArtworkCamera = require('./artwork/ArtworkCamera'); 18 | 19 | const HOMEBRIDGE = { 20 | hap: null, 21 | Accessory: null, 22 | Service: null, 23 | Characteristic: null, 24 | UUIDGen: null 25 | }; 26 | 27 | const platformName = 'homebridge-dacp'; 28 | const platformPrettyName = 'DACP'; 29 | 30 | module.exports = (homebridge) => { 31 | HOMEBRIDGE.hap = homebridge.hap; 32 | HOMEBRIDGE.Accessory = homebridge.platformAccessory; 33 | HOMEBRIDGE.Service = homebridge.hap.Service; 34 | HOMEBRIDGE.Characteristic = homebridge.hap.Characteristic; 35 | HOMEBRIDGE.UUIDGen = homebridge.hap.uuid; 36 | HOMEBRIDGE.homebridge = homebridge; 37 | 38 | homebridge.registerPlatform(platformName, platformPrettyName, DacpPlatform, false); 39 | }; 40 | 41 | const DacpPlatform = class { 42 | constructor(log, config, api) { 43 | this.log = log; 44 | this.log(`DACP Platform Plugin Loaded - Version ${version}`); 45 | this.config = config; 46 | this.api = api; 47 | 48 | this._dacpBrowser = new DacpBrowser(this.log); 49 | this._dacpBrowser.on('serviceUp', this._onServiceUp.bind(this)); 50 | this._dacpBrowser.on('serviceDown', this._onServiceDown.bind(this)); 51 | this._dacpBrowser.on('error', this._onDacpBrowserError.bind(this)); 52 | 53 | this._dacpErrors = 0; 54 | 55 | this._accessories = []; 56 | this._remotes = []; 57 | 58 | this.api.on('didFinishLaunching', this._didFinishLaunching.bind(this)); 59 | 60 | MediaSkippingTypes.registerWith(api.hap); 61 | NowPlayingTypes.registerWith(api.hap); 62 | PlayerControlTypes.registerWith(api.hap); 63 | PlaylistTypes.registerWith(api.hap); 64 | InputControlTypes.registerWith(api.hap); 65 | } 66 | 67 | _didFinishLaunching() { 68 | // Start looking for the controllable accessories 69 | this._dacpBrowser.start(); 70 | 71 | // Enable all artwork cameras (if any) 72 | this._enableArtworkCameras(); 73 | } 74 | 75 | _enableArtworkCameras() { 76 | const configuredAccessories = []; 77 | 78 | this.config.devices.forEach(device => { 79 | 80 | if (!device || !device.features) { 81 | return; 82 | } 83 | 84 | const artwork = device.features['album-artwork']; 85 | if (typeof artwork === 'string') { 86 | const cameraName = `${device.name} Artwork`; 87 | const videoConfig = { 88 | 'binary': 'ffmpeg', 89 | 'vcodec': 'libx264', 90 | 'artworkImageSource': artwork, 91 | 'maxStreams': 2, 92 | 'maxWidth': 600, 93 | 'maxHeight': 600, 94 | 'maxFPS': 2 95 | }; 96 | 97 | if (isPi()) { 98 | videoConfig.vcodec = 'h264_omx'; 99 | } 100 | 101 | const uuid = HOMEBRIDGE.UUIDGen.generate(cameraName); 102 | const artworkCameraAccessory = new HOMEBRIDGE.Accessory(cameraName, uuid, HOMEBRIDGE.hap.Accessory.Categories.CAMERA); 103 | const artworkCamera = new ArtworkCamera(this.log, HOMEBRIDGE.hap, videoConfig); 104 | 105 | artworkCameraAccessory.configureCameraSource(artworkCamera); 106 | configuredAccessories.push(artworkCameraAccessory); 107 | } 108 | }); 109 | 110 | if (configuredAccessories.length > 0) { 111 | this.api.publishCameraAccessories(platformPrettyName, configuredAccessories); 112 | } 113 | } 114 | 115 | _onServiceUp(service) { 116 | // If the browser was down this is also an indication that it's up again. 117 | this._dacpErrors = 0; 118 | 119 | // Update accessory and tell it that it's device is available. 120 | this._accessories.forEach(accessory => { 121 | if (accessory.serviceName && service.name === accessory.serviceName) { 122 | accessory.accessoryUp(service); 123 | } 124 | }); 125 | } 126 | 127 | _onServiceDown(service) { 128 | // Update accessory and tell it that it's device is unavailable. 129 | this._accessories.forEach(accessory => { 130 | if (accessory.serviceName && service.name === accessory.serviceName) { 131 | accessory.accessoryDown(service); 132 | } 133 | }); 134 | } 135 | 136 | _onDacpBrowserError(error) { 137 | 138 | 139 | // How often has this occurred? If less than 5 times within last 10mins, 140 | // keep retrying. We might have a sporadic network disconnect or other reason 141 | // that this has been failing. We want to deal with this gracefully, so we 142 | // need a restart strategy for the DACP browser and the accessories. 143 | 144 | this.log('Fatal error browsing for DACP services:'); 145 | this.log(''); 146 | this.log(` Error: ${JSON.stringify(error)}`); 147 | this.log(''); 148 | 149 | this._dacpErrors++; 150 | this._dacpBrowser.stop(); 151 | 152 | if (this._dacpErrors < 5) { 153 | const timeout = 120000; 154 | this.log(`Restarting MDNS browser for DACP in ${timeout / 1000} seconds.`); 155 | setTimeout(() => this._dacpBrowser.start(), timeout); 156 | } 157 | else { 158 | this.log('There were 5 failures in the past 600s. Giving up.'); 159 | this.log(''); 160 | this.log('Restarting homebridge might fix the problem. If not, file an issue at https://github.com/grover/homebridge-dacp.'); 161 | } 162 | 163 | this.log(''); 164 | 165 | // Mark all accessories as unavailable 166 | this._accessories.forEach(accessory => { 167 | accessory.accessoryDown(); 168 | }); 169 | } 170 | 171 | accessories(callback) { 172 | const { devices } = this.config; 173 | 174 | this._accessories = devices.map(device => { 175 | 176 | this.log(`Found accessory in config: "${device.name}"`); 177 | 178 | if (device.features === undefined) { 179 | device.features = {}; 180 | } 181 | 182 | if (!device.pairing || !device.serviceName) { 183 | const passcode = this._randomBaseString(4, 10); 184 | 185 | this.log(''); 186 | this.log(`Skipping creation of the accessory "${device.name}" because it doesn't have a pairing code or`); 187 | this.log('service name yet. You need to pair the device/iTunes, reconfigure and restart homebridge.'); 188 | this.log(''); 189 | this.log(`Beginning remote control announcements for the accessory "${device.name}".`); 190 | this.log(''); 191 | this.log(`\tUse passcode ${passcode} to pair with this remote control.`); 192 | this.log(''); 193 | 194 | this._createRemote(device, passcode); 195 | return; 196 | } 197 | 198 | device.uuid = HOMEBRIDGE.UUIDGen.generate(platformPrettyName + ':' + device.name); 199 | device.version = version; 200 | 201 | return new DacpAccessory(this.api, this.log, device); 202 | }).filter(a => a !== undefined); 203 | 204 | callback(this._accessories); 205 | } 206 | 207 | _createRemote(remote, passcode) { 208 | 209 | const remoteConfig = { 210 | pair: this._randomBaseString(16, 16).toUpperCase(), 211 | pairPasscode: passcode, 212 | deviceName: remote.name, 213 | deviceType: 'iPad' 214 | }; 215 | const dacpRemote = new DacpRemote(remoteConfig, this.log); 216 | 217 | dacpRemote.on('paired', data => { 218 | this.log(`Completed pairing for "${remote.name}":`); 219 | this.log(''); 220 | this.log('{'); 221 | this.log(` "name": "${remote.name}",`); 222 | this.log(` "pairing": "${remoteConfig.pair}",`); 223 | this.log(` "serviceName": "${data.serviceName}"`); 224 | this.log('}'); 225 | this.log(''); 226 | this.log('Please add the above block to the accessory in your homebridge config.json'); 227 | this.log(''); 228 | this.log('YOU MUST RESTART HOMEBRIDGE AFTER YOU ADDED THE ABOVE LINES OR THE ACCESSORY'); 229 | this.log('WILL NOT WORK.'); 230 | this.log(''); 231 | }); 232 | 233 | this._remotes.push(dacpRemote); 234 | } 235 | 236 | _randomBaseString(length, base) { 237 | var generated = ''; 238 | for (var ctr = 0; ctr < length; ctr++) 239 | generated += Math.floor(Math.random() * base).toString(base); 240 | return generated; 241 | } 242 | }; 243 | --------------------------------------------------------------------------------