@bharper/atv-js 0.2.6 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/dist/index.d.ts +15 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +89 -9
  4. package/dist/index.js.map +1 -1
  5. package/dist/mdns.d.ts.map +1 -1
  6. package/dist/mdns.js +96 -11
  7. package/dist/mdns.js.map +1 -1
  8. package/examples/print-device-json.js +22 -0
  9. package/package.json +2 -3
  10. package/pyatv/.codecov.yml +38 -0
  11. package/pyatv/.github/FUNDING.yml +3 -0
  12. package/pyatv/.github/ISSUE_TEMPLATE/bug_report.yml +80 -0
  13. package/pyatv/.github/ISSUE_TEMPLATE/config.yml +1 -0
  14. package/pyatv/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
  15. package/pyatv/.github/ISSUE_TEMPLATE/implementation-proposal.yml +29 -0
  16. package/pyatv/.github/ISSUE_TEMPLATE/investigation.yml +16 -0
  17. package/pyatv/.github/ISSUE_TEMPLATE/minor-change.yml +10 -0
  18. package/pyatv/.github/ISSUE_TEMPLATE/question-or-idea.yml +11 -0
  19. package/pyatv/.github/dependabot.yml +26 -0
  20. package/pyatv/.github/workflows/codeql-analysis.yml +71 -0
  21. package/pyatv/.github/workflows/release.yml +160 -0
  22. package/pyatv/.github/workflows/tests.yml +104 -0
  23. package/pyatv/.gitpod.yml +23 -0
  24. package/pyatv/CHANGES.md +3708 -0
  25. package/pyatv/CODE_OF_CONDUCT.md +76 -0
  26. package/pyatv/CONTRIBUTING.md +72 -0
  27. package/pyatv/CONTRIBUTORS.md +3 -0
  28. package/pyatv/Dockerfile +15 -0
  29. package/pyatv/LICENSE.md +9 -0
  30. package/pyatv/MANIFEST.in +14 -0
  31. package/pyatv/README.md +111 -0
  32. package/pyatv/base_versions.txt +13 -0
  33. package/pyatv/chickn.yaml +75 -0
  34. package/pyatv/docs/404.html +24 -0
  35. package/pyatv/docs/CNAME +1 -0
  36. package/pyatv/docs/Gemfile +31 -0
  37. package/pyatv/docs/_config.yml +121 -0
  38. package/pyatv/docs/_includes/api +10 -0
  39. package/pyatv/docs/_includes/atvremote_scan +32 -0
  40. package/pyatv/docs/_includes/code +6 -0
  41. package/pyatv/docs/_includes/issue +14 -0
  42. package/pyatv/docs/_includes/pypi +5 -0
  43. package/pyatv/docs/_layouts/template.html +109 -0
  44. package/pyatv/docs/api/pyatv.conf.html +312 -0
  45. package/pyatv/docs/api/pyatv.const.html +974 -0
  46. package/pyatv/docs/api/pyatv.convert.html +106 -0
  47. package/pyatv/docs/api/pyatv.exceptions.html +489 -0
  48. package/pyatv/docs/api/pyatv.helpers.html +102 -0
  49. package/pyatv/docs/api/pyatv.html +120 -0
  50. package/pyatv/docs/api/pyatv.interface.html +2369 -0
  51. package/pyatv/docs/api/pyatv.settings.html +484 -0
  52. package/pyatv/docs/api/pyatv.storage.file_storage.html +102 -0
  53. package/pyatv/docs/api/pyatv.storage.html +186 -0
  54. package/pyatv/docs/api/pyatv.storage.memory_storage.html +83 -0
  55. package/pyatv/docs/assets/css/custom.css +19 -0
  56. package/pyatv/docs/assets/css/hljs.css +1 -0
  57. package/pyatv/docs/assets/css/normalize.css +349 -0
  58. package/pyatv/docs/assets/css/pdoc.css +287 -0
  59. package/pyatv/docs/assets/css/sanitize.css +566 -0
  60. package/pyatv/docs/assets/css/style.scss +9 -0
  61. package/pyatv/docs/assets/img/logo.svg +63 -0
  62. package/pyatv/docs/assets/js/highlight.9.12.0.min.js +3 -0
  63. package/pyatv/docs/assets/js/mermaid.8.9.2.min.js +32 -0
  64. package/pyatv/docs/assets/js/mermaid.min.js.map +1 -0
  65. package/pyatv/docs/development/apps.md +81 -0
  66. package/pyatv/docs/development/audio.md +42 -0
  67. package/pyatv/docs/development/control.md +56 -0
  68. package/pyatv/docs/development/development.md +15 -0
  69. package/pyatv/docs/development/device_info.md +36 -0
  70. package/pyatv/docs/development/examples.md +44 -0
  71. package/pyatv/docs/development/features.md +70 -0
  72. package/pyatv/docs/development/keyboard.md +51 -0
  73. package/pyatv/docs/development/listeners.md +144 -0
  74. package/pyatv/docs/development/logging.md +55 -0
  75. package/pyatv/docs/development/metadata.md +115 -0
  76. package/pyatv/docs/development/power_management.md +53 -0
  77. package/pyatv/docs/development/scan_pair_and_connect.md +331 -0
  78. package/pyatv/docs/development/services.md +9 -0
  79. package/pyatv/docs/development/storage.md +259 -0
  80. package/pyatv/docs/development/stream.md +241 -0
  81. package/pyatv/docs/development/testing.md +9 -0
  82. package/pyatv/docs/documentation/atvlog.md +64 -0
  83. package/pyatv/docs/documentation/atvproxy.md +244 -0
  84. package/pyatv/docs/documentation/atvremote.md +639 -0
  85. package/pyatv/docs/documentation/atvscript.md +275 -0
  86. package/pyatv/docs/documentation/concepts.md +168 -0
  87. package/pyatv/docs/documentation/documentation.md +130 -0
  88. package/pyatv/docs/documentation/getting_started.md +248 -0
  89. package/pyatv/docs/documentation/protocols.md +1959 -0
  90. package/pyatv/docs/documentation/supported_features.md +246 -0
  91. package/pyatv/docs/documentation/tutorial.md +1062 -0
  92. package/pyatv/docs/documentation/workspace.code-workspace +7 -0
  93. package/pyatv/docs/favicon.ico +0 -0
  94. package/pyatv/docs/index.md +109 -0
  95. package/pyatv/docs/internals/design.md +354 -0
  96. package/pyatv/docs/internals/documentation.md +84 -0
  97. package/pyatv/docs/internals/interfaces.md +95 -0
  98. package/pyatv/docs/internals/internals.md +157 -0
  99. package/pyatv/docs/internals/submit_pr.md +56 -0
  100. package/pyatv/docs/internals/testing.md +176 -0
  101. package/pyatv/docs/internals/tools.md +574 -0
  102. package/pyatv/docs/pdoc_templates/config.mako +46 -0
  103. package/pyatv/docs/pdoc_templates/html.mako +454 -0
  104. package/pyatv/docs/support/acknowledgements.md +87 -0
  105. package/pyatv/docs/support/faq.md +214 -0
  106. package/pyatv/docs/support/migration.md +138 -0
  107. package/pyatv/docs/support/scanning_issues.md +110 -0
  108. package/pyatv/docs/support/support.md +18 -0
  109. package/pyatv/docs/support/troubleshooting.md +83 -0
  110. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeMessage.proto +13 -0
  111. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeMessage_pb2.pyi +37 -0
  112. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeResponseMessage.proto +11 -0
  113. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeResponseMessage_pb2.pyi +32 -0
  114. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFormatSettingsMessage.proto +5 -0
  115. package/pyatv/pyatv/protocols/mrp/protobuf/AudioFormatSettingsMessage_pb2.pyi +27 -0
  116. package/pyatv/pyatv/protocols/mrp/protobuf/ClientUpdatesConfigMessage.proto +16 -0
  117. package/pyatv/pyatv/protocols/mrp/protobuf/ClientUpdatesConfigMessage_pb2.pyi +44 -0
  118. package/pyatv/pyatv/protocols/mrp/protobuf/CommandInfo.proto +117 -0
  119. package/pyatv/pyatv/protocols/mrp/protobuf/CommandInfo_pb2.pyi +325 -0
  120. package/pyatv/pyatv/protocols/mrp/protobuf/CommandOptions.proto +36 -0
  121. package/pyatv/pyatv/protocols/mrp/protobuf/CommandOptions_pb2.pyi +115 -0
  122. package/pyatv/pyatv/protocols/mrp/protobuf/Common.proto +79 -0
  123. package/pyatv/pyatv/protocols/mrp/protobuf/Common_pb2.pyi +228 -0
  124. package/pyatv/pyatv/protocols/mrp/protobuf/ConfigureConnectionMessage.proto +11 -0
  125. package/pyatv/pyatv/protocols/mrp/protobuf/ConfigureConnectionMessage_pb2.pyi +32 -0
  126. package/pyatv/pyatv/protocols/mrp/protobuf/ContentItem.proto +27 -0
  127. package/pyatv/pyatv/protocols/mrp/protobuf/ContentItemMetadata.proto +213 -0
  128. package/pyatv/pyatv/protocols/mrp/protobuf/ContentItemMetadata_pb2.pyi +630 -0
  129. package/pyatv/pyatv/protocols/mrp/protobuf/ContentItem_pb2.pyi +94 -0
  130. package/pyatv/pyatv/protocols/mrp/protobuf/CryptoPairingMessage.proto +15 -0
  131. package/pyatv/pyatv/protocols/mrp/protobuf/CryptoPairingMessage_pb2.pyi +46 -0
  132. package/pyatv/pyatv/protocols/mrp/protobuf/DeviceInfoMessage.proto +69 -0
  133. package/pyatv/pyatv/protocols/mrp/protobuf/DeviceInfoMessage_pb2.pyi +226 -0
  134. package/pyatv/pyatv/protocols/mrp/protobuf/GenericMessage.proto +12 -0
  135. package/pyatv/pyatv/protocols/mrp/protobuf/GenericMessage_pb2.pyi +35 -0
  136. package/pyatv/pyatv/protocols/mrp/protobuf/GetKeyboardSessionMessage.proto +11 -0
  137. package/pyatv/pyatv/protocols/mrp/protobuf/GetKeyboardSessionMessage_pb2.pyi +26 -0
  138. package/pyatv/pyatv/protocols/mrp/protobuf/GetRemoteTextInputSessionMessage.proto +10 -0
  139. package/pyatv/pyatv/protocols/mrp/protobuf/GetRemoteTextInputSessionMessage_pb2.pyi +26 -0
  140. package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeMessage.proto +11 -0
  141. package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeMessage_pb2.pyi +32 -0
  142. package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeResultMessage.proto +11 -0
  143. package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeResultMessage_pb2.pyi +32 -0
  144. package/pyatv/pyatv/protocols/mrp/protobuf/KeyboardMessage.proto +88 -0
  145. package/pyatv/pyatv/protocols/mrp/protobuf/KeyboardMessage_pb2.pyi +261 -0
  146. package/pyatv/pyatv/protocols/mrp/protobuf/LanguageOption.proto +9 -0
  147. package/pyatv/pyatv/protocols/mrp/protobuf/LanguageOption_pb2.pyi +42 -0
  148. package/pyatv/pyatv/protocols/mrp/protobuf/ModifyOutputContextRequestMessage.proto +23 -0
  149. package/pyatv/pyatv/protocols/mrp/protobuf/ModifyOutputContextRequestMessage_pb2.pyi +86 -0
  150. package/pyatv/pyatv/protocols/mrp/protobuf/NotificationMessage.proto +12 -0
  151. package/pyatv/pyatv/protocols/mrp/protobuf/NotificationMessage_pb2.pyi +38 -0
  152. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingClient.proto +12 -0
  153. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingClient_pb2.pyi +49 -0
  154. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingInfo.proto +24 -0
  155. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingInfo_pb2.pyi +79 -0
  156. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingPlayer.proto +11 -0
  157. package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingPlayer_pb2.pyi +45 -0
  158. package/pyatv/pyatv/protocols/mrp/protobuf/Origin.proto +17 -0
  159. package/pyatv/pyatv/protocols/mrp/protobuf/OriginClientPropertiesMessage.proto +11 -0
  160. package/pyatv/pyatv/protocols/mrp/protobuf/OriginClientPropertiesMessage_pb2.pyi +32 -0
  161. package/pyatv/pyatv/protocols/mrp/protobuf/Origin_pb2.pyi +63 -0
  162. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueue.proto +15 -0
  163. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueCapabilities.proto +7 -0
  164. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueCapabilities_pb2.pyi +33 -0
  165. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueContext.proto +5 -0
  166. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueContext_pb2.pyi +27 -0
  167. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueRequestMessage.proto +29 -0
  168. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueRequestMessage_pb2.pyi +87 -0
  169. package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueue_pb2.pyi +53 -0
  170. package/pyatv/pyatv/protocols/mrp/protobuf/PlayerClientPropertiesMessage.proto +13 -0
  171. package/pyatv/pyatv/protocols/mrp/protobuf/PlayerClientPropertiesMessage_pb2.pyi +37 -0
  172. package/pyatv/pyatv/protocols/mrp/protobuf/PlayerPath.proto +11 -0
  173. package/pyatv/pyatv/protocols/mrp/protobuf/PlayerPath_pb2.pyi +39 -0
  174. package/pyatv/pyatv/protocols/mrp/protobuf/ProtocolMessage.proto +171 -0
  175. package/pyatv/pyatv/protocols/mrp/protobuf/ProtocolMessage_pb2.pyi +377 -0
  176. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterForGameControllerEventsMessage.proto +18 -0
  177. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterForGameControllerEventsMessage_pb2.pyi +54 -0
  178. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceMessage.proto +12 -0
  179. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceMessage_pb2.pyi +34 -0
  180. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceResultMessage.proto +12 -0
  181. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceResultMessage_pb2.pyi +35 -0
  182. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceMessage.proto +12 -0
  183. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceMessage_pb2.pyi +34 -0
  184. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceResponseMessage.proto +12 -0
  185. package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceResponseMessage_pb2.pyi +35 -0
  186. package/pyatv/pyatv/protocols/mrp/protobuf/RemoteTextInputMessage.proto +13 -0
  187. package/pyatv/pyatv/protocols/mrp/protobuf/RemoteTextInputMessage_pb2.pyi +38 -0
  188. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveClientMessage.proto +12 -0
  189. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveClientMessage_pb2.pyi +34 -0
  190. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveEndpointsMessage.proto +11 -0
  191. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveEndpointsMessage_pb2.pyi +34 -0
  192. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveOutputDevicesMessage.proto +12 -0
  193. package/pyatv/pyatv/protocols/mrp/protobuf/RemoveOutputDevicesMessage_pb2.pyi +38 -0
  194. package/pyatv/pyatv/protocols/mrp/protobuf/RemovePlayerMessage.proto +12 -0
  195. package/pyatv/pyatv/protocols/mrp/protobuf/RemovePlayerMessage_pb2.pyi +34 -0
  196. package/pyatv/pyatv/protocols/mrp/protobuf/SendButtonEventMessage.proto +13 -0
  197. package/pyatv/pyatv/protocols/mrp/protobuf/SendButtonEventMessage_pb2.pyi +38 -0
  198. package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandMessage.proto +16 -0
  199. package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandMessage_pb2.pyi +43 -0
  200. package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandResultMessage.proto +100 -0
  201. package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandResultMessage_pb2.pyi +286 -0
  202. package/pyatv/pyatv/protocols/mrp/protobuf/SendHIDEventMessage.proto +41 -0
  203. package/pyatv/pyatv/protocols/mrp/protobuf/SendHIDEventMessage_pb2.pyi +63 -0
  204. package/pyatv/pyatv/protocols/mrp/protobuf/SendPackedVirtualTouchEventMessage.proto +24 -0
  205. package/pyatv/pyatv/protocols/mrp/protobuf/SendPackedVirtualTouchEventMessage_pb2.pyi +64 -0
  206. package/pyatv/pyatv/protocols/mrp/protobuf/SendVoiceInputMessage.proto +38 -0
  207. package/pyatv/pyatv/protocols/mrp/protobuf/SendVoiceInputMessage_pb2.pyi +134 -0
  208. package/pyatv/pyatv/protocols/mrp/protobuf/SetArtworkMessage.proto +11 -0
  209. package/pyatv/pyatv/protocols/mrp/protobuf/SetArtworkMessage_pb2.pyi +32 -0
  210. package/pyatv/pyatv/protocols/mrp/protobuf/SetConnectionStateMessage.proto +18 -0
  211. package/pyatv/pyatv/protocols/mrp/protobuf/SetConnectionStateMessage_pb2.pyi +54 -0
  212. package/pyatv/pyatv/protocols/mrp/protobuf/SetDefaultSupportedCommandsMessage.proto +28 -0
  213. package/pyatv/pyatv/protocols/mrp/protobuf/SetDefaultSupportedCommandsMessage_pb2.pyi +74 -0
  214. package/pyatv/pyatv/protocols/mrp/protobuf/SetDiscoveryModeMessage.proto +12 -0
  215. package/pyatv/pyatv/protocols/mrp/protobuf/SetDiscoveryModeMessage_pb2.pyi +35 -0
  216. package/pyatv/pyatv/protocols/mrp/protobuf/SetHiliteModeMessage.proto +11 -0
  217. package/pyatv/pyatv/protocols/mrp/protobuf/SetHiliteModeMessage_pb2.pyi +32 -0
  218. package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingClientMessage.proto +12 -0
  219. package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingClientMessage_pb2.pyi +34 -0
  220. package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingPlayerMessage.proto +12 -0
  221. package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingPlayerMessage_pb2.pyi +34 -0
  222. package/pyatv/pyatv/protocols/mrp/protobuf/SetRecordingStateMessage.proto +17 -0
  223. package/pyatv/pyatv/protocols/mrp/protobuf/SetRecordingStateMessage_pb2.pyi +54 -0
  224. package/pyatv/pyatv/protocols/mrp/protobuf/SetStateMessage.proto +27 -0
  225. package/pyatv/pyatv/protocols/mrp/protobuf/SetStateMessage_pb2.pyi +72 -0
  226. package/pyatv/pyatv/protocols/mrp/protobuf/SetVolumeMessage.proto +12 -0
  227. package/pyatv/pyatv/protocols/mrp/protobuf/SetVolumeMessage_pb2.pyi +35 -0
  228. package/pyatv/pyatv/protocols/mrp/protobuf/SupportedCommands.proto +7 -0
  229. package/pyatv/pyatv/protocols/mrp/protobuf/SupportedCommands_pb2.pyi +30 -0
  230. package/pyatv/pyatv/protocols/mrp/protobuf/TextInputMessage.proto +23 -0
  231. package/pyatv/pyatv/protocols/mrp/protobuf/TextInputMessage_pb2.pyi +76 -0
  232. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionKey.proto +6 -0
  233. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionKey_pb2.pyi +30 -0
  234. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionMessage.proto +15 -0
  235. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionMessage_pb2.pyi +42 -0
  236. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPacket.proto +11 -0
  237. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPacket_pb2.pyi +41 -0
  238. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPackets.proto +7 -0
  239. package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPackets_pb2.pyi +30 -0
  240. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateClientMessage.proto +12 -0
  241. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateClientMessage_pb2.pyi +34 -0
  242. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemArtworkMessage.proto +14 -0
  243. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemArtworkMessage_pb2.pyi +41 -0
  244. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemMessage.proto +14 -0
  245. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemMessage_pb2.pyi +41 -0
  246. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateEndPointsMessage.proto +25 -0
  247. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateEndPointsMessage_pb2.pyi +74 -0
  248. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateOutputDeviceMessage.proto +88 -0
  249. package/pyatv/pyatv/protocols/mrp/protobuf/UpdateOutputDeviceMessage_pb2.pyi +277 -0
  250. package/pyatv/pyatv/protocols/mrp/protobuf/UpdatePlayerPath.proto +12 -0
  251. package/pyatv/pyatv/protocols/mrp/protobuf/UpdatePlayerPath_pb2.pyi +34 -0
  252. package/pyatv/pyatv/protocols/mrp/protobuf/VirtualTouchDeviceDescriptorMessage.proto +8 -0
  253. package/pyatv/pyatv/protocols/mrp/protobuf/VirtualTouchDeviceDescriptorMessage_pb2.pyi +36 -0
  254. package/pyatv/pyatv/protocols/mrp/protobuf/VoiceInputDeviceDescriptorMessage.proto +8 -0
  255. package/pyatv/pyatv/protocols/mrp/protobuf/VoiceInputDeviceDescriptorMessage_pb2.pyi +35 -0
  256. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlAvailabilityMessage.proto +23 -0
  257. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlAvailabilityMessage_pb2.pyi +71 -0
  258. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlCapabilitiesDidChangeMessage.proto +14 -0
  259. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlCapabilitiesDidChangeMessage_pb2.pyi +40 -0
  260. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeDidChangeMessage.proto +13 -0
  261. package/pyatv/pyatv/protocols/mrp/protobuf/VolumeDidChangeMessage_pb2.pyi +38 -0
  262. package/pyatv/pyatv/protocols/mrp/protobuf/WakeDeviceMessage.proto +11 -0
  263. package/pyatv/pyatv/protocols/mrp/protobuf/WakeDeviceMessage_pb2.pyi +26 -0
  264. package/pyatv/pyatv/py.typed +0 -0
  265. package/pyatv/pylintrc +49 -0
  266. package/pyatv/pyproject.toml +74 -0
  267. package/pyatv/requirements/requirements.txt +14 -0
  268. package/pyatv/requirements/requirements_docs.txt +2 -0
  269. package/pyatv/requirements/requirements_test.txt +20 -0
  270. package/pyatv/scripts/build_docs.sh +17 -0
  271. package/pyatv/scripts/setup_dev_env.sh +83 -0
  272. package/pyatv/setup.cfg +14 -0
  273. package/pyatv/tests/data/README +23 -0
  274. package/pyatv/tests/data/audio_10_frames.wav +0 -0
  275. package/pyatv/tests/data/audio_1_packet_metadata.wav +0 -0
  276. package/pyatv/tests/data/audio_3_packets.wav +0 -0
  277. package/pyatv/tests/data/only_metadata.wav +0 -0
  278. package/pyatv/tests/data/only_title.wav +0 -0
  279. package/pyatv/tests/data/static_3sec.ogg +0 -0
  280. package/pyatv/tests/data/testfile.txt +1 -0
  281. package/pyatv/tests/support/pyatv.code-workspace +14 -0
  282. package/src/index.ts +122 -8
  283. package/src/mdns.ts +64 -11
@@ -0,0 +1,1959 @@
1
+ ---
2
+ layout: template
3
+ title: Protocols
4
+ permalink: /documentation/protocols/
5
+ link_group: documentation
6
+ ---
7
+ # Table of Contents
8
+ {:.no_toc}
9
+ * TOC
10
+ {:toc}
11
+
12
+
13
+ # Protocols
14
+
15
+ If you want to extend pyatv, a basic understanding of the used protocols helps a lot. This
16
+ page aims to give a summary of the protocols and how they work (to the extent we know, since
17
+ they are reverse engineered). Focus are on the parts that are relevant and implemented in
18
+ pyatv.
19
+
20
+ # Digital Media Access Protocol (DMAP)
21
+
22
+ DMAP covers the suite of protocols used by various Apple software (e.g. iTunes)
23
+ to share, for instance, music. There are already a bunch of sites and libraries describing
24
+ and implementing these protocols. Please see the reference further down. This section will
25
+ focus on the technical aspects used to implement DMAP/DACP/DAAP in pyatv.
26
+
27
+ At its core, DMAP is basically a HTTP server (running on port 3689) that responds to specific
28
+ commands and streams events back to the client. Data is requested using GET and POST methods with
29
+ special URLs. Data in the responses is usually in a specific binary format, whose format can depend on
30
+ the request (like a PNG file for artwork). The
31
+ binary protocol will be explained first, as that makes it easier to understand
32
+ the requests.
33
+
34
+ ## DMAP Binary Format
35
+
36
+ The binary format is basically [type-length-value](https://en.wikipedia.org/wiki/Type–length–value)
37
+ (TLV) data where the tag (or key) is a 4 byte ASCII-string,
38
+ the length is a four byte unsigned integer and the data is, well, data. Type
39
+ and meaning of a specific TLV are derived from the tag. So one must know which
40
+ tags are used, how large they are and what they mean. Please note that Length
41
+ is length of the data, so key and length are not included in this size.
42
+
43
+ A TLV looks like this:
44
+
45
+ | Key (4 bytes) | Length (4 bytes) | Data (Length) bytes |
46
+
47
+ Multiple TLVs are usually embedded in one DMAP data stream and TLVs may also
48
+ be nested, to form a tree:
49
+
50
+ TLV1
51
+ |
52
+ +---TLV2
53
+ | |
54
+ | + TLV3
55
+ |
56
+ +---TLV4
57
+ |
58
+ + TLV5
59
+
60
+ As stated earlier, we must already know if a tag is a "container" (that
61
+ contains other TLVs) or not. It cannot easily be seen on the data itself.
62
+ A container usually has more resemblance to an array than a dictionary
63
+ since multiple TLVs with the same key often occur.
64
+
65
+ All tags currently known by pyatv are defined in `pyatv.dmap.tag_definitions`.
66
+
67
+ ## Decoding Example
68
+
69
+ Lets assume that we know the following three keys:
70
+
71
+
72
+ | Key | Type | Meaning |
73
+ | ---- | --------- | ------------------- |
74
+ | cmst | Container | dmcp.playstatus |
75
+ | mstt | uint32 | dmap.status |
76
+ | cmsr | uint32 | dmcp.serverrevision |
77
+
78
+ Now, let us try to decode the following binary data with the table above:
79
+
80
+ 636d7374000000186d73747400000004000000c8636d73720000000400000019
81
+
82
+ We know that key and length fields are always four bytes, so lets split the
83
+ TLV so we more easily can see what is happening:
84
+
85
+ 636d7374 00000018 6d73747400000004000000c8636d73720000000400000019
86
+
87
+ How nice, 0x636d7374 corresponds to *cmst* in ASCII and we happen to know
88
+ what that is. We can also see that the data is 0x18 = 24 bytes long which so
89
+ happens to be the remaining data. All the following TLVs are thus children
90
+ to *cmst* since that is a container. Lets continue and split the remaining
91
+ data:
92
+
93
+ 6d737474 00000004 000000c8636d73720000000400000019
94
+
95
+ Again, we can see that the key 0x6d737474 is *mstt* in ASCII. This is a uint32
96
+ which means that the size is four bytes and the we should interpret the four
97
+ following bytes a uint32:
98
+
99
+ 000000c8 = 200
100
+
101
+ Since we have data remaining, that should be another TLV and we have to
102
+ continue decoding that one as well. Same procedure:
103
+
104
+ 636d7372 00000004 00000019
105
+
106
+ The tag is 0x636d7372 = *cmsr*, size is four bytes (uint32) and the decoded
107
+ value is 25. The final decoding looks like this:
108
+
109
+ + cmst:
110
+ |
111
+ +- mstt: 200
112
+ |
113
+ +- cmsr: 25
114
+
115
+ Note that *mstt* and *cmsr* are part of the *cmst* container. This is a typical
116
+ response that the Apple TV responds with when doing a "playstatusupdate" request
117
+ and nothing is currently playing. Other keys and values are included when
118
+ you for instance are playing video or music.
119
+
120
+ ## Request URLs
121
+
122
+ Since DAAP is sent over HTTP, requests can be made with any HTTP client. However,
123
+ some special headers must be included. These have been extracted with Wireshark
124
+ when using the Remote app on an iPhone and covers `GET`-requests:
125
+
126
+ | Header | Value |
127
+ | ----------------------------- | -------------------------------------------- |
128
+ | Accept | */* |
129
+ | Accept-Encoding | gzip |
130
+ | Client-DAAP-Version | 3.13 |
131
+ | Client-ATV-Sharing-Version | 1.2 |
132
+ | Client-iTunes-Sharing-Version | 3.15 |
133
+ | User-Agent | Remote/1021 |
134
+ | Viewer-Only-Client | 1 |
135
+
136
+ For `POST`-request, the following header must be present as well:
137
+
138
+ | Header | Value |
139
+ | ------------ | --------------------------------- |
140
+ | Content-Type | application/x-www-form-urlencoded |
141
+
142
+ There are a lot of different requests that can be sent and this library
143
+ implements far from all of them. There is actually support for things that
144
+ aren't implemented by the native Remote app, like scrubbing (changing absolute
145
+ position in the stream). Since it's the same commands as used by iTunes, we can
146
+ probably assume that it's the same software implementation used in both
147
+ products. Enough on that matter... All the requests that are used by this
148
+ library is described in their own chapter a bit further down.
149
+
150
+ ## Authentication
151
+
152
+ Some commands can be queried freely by anyone on the same network as the Apple TV,
153
+ like the server-info command. But most commands require a "session id". The
154
+ session id is obtained by doing a login and extracting the `mlid` key. Session id
155
+ is then included in all requests, e.g.
156
+
157
+ ctrl-int/1/playstatusupdate?session-id=<session id>&revision-number=0
158
+
159
+ The device will respond with an error (503?) if the authentication fails.
160
+
161
+ ## Supported Requests
162
+
163
+ This list only covers the requests performed by pyatv and is incomplete.
164
+
165
+ ### server-info
166
+
167
+ **Type:** GET
168
+
169
+ **URL:** server-info
170
+
171
+ **Authentication:** None
172
+
173
+ Returns various information about a device. Here is an example:
174
+
175
+ msrv: [container, dmap.serverinforesponse]
176
+ mstt: 200 [uint, dmap.status]
177
+ mpro: 131082 [uint, dmap.protocolversion]
178
+ minm: Apple TV [str, dmap.itemname]
179
+ apro: 196620 [uint, daap.protocolversion]
180
+ aeSV: 196618 [uint, com.apple.itunes.music-sharing-version]
181
+ mstm: 1800 [uint, dmap.timeoutinterval]
182
+ msdc: 1 [uint, dmap.databasescount]
183
+ aeFP: 2 [uint, com.apple.itunes.req-fplay]
184
+ aeFR: 100 [uint, unknown tag]
185
+ mslr: True [bool, dmap.loginrequired]
186
+ msal: True [bool, dmap.supportsautologout]
187
+ mstc: 1485803565 [uint, dmap.utctime]
188
+ msto: 3600 [uint, dmap.utcoffset]
189
+ atSV: 65541 [uint, unknown tag]
190
+ ated: True [bool, daap.supportsextradata]
191
+ asgr: 3 [uint, com.apple.itunes.gapless-resy]
192
+ asse: 7341056 [uint, unknown tag]
193
+ aeSX: 3 [uint, unknown tag]
194
+ msed: True [bool, dmap.supportsedit]
195
+ msup: True [bool, dmap.supportsupdate]
196
+ mspi: True [bool, dmap.supportspersistentids]
197
+ msex: True [bool, dmap.supportsextensions]
198
+ msbr: True [bool, dmap.supportsbrowse]
199
+ msqy: True [bool, dmap.supportsquery]
200
+ msix: True [bool, dmap.supportsindex]
201
+ mscu: 101 [uint, unknown tag]
202
+
203
+ ### login
204
+
205
+ **Type:** GET
206
+
207
+ **URL:** login?hsgid=<hsgid>&hasFP=1
208
+
209
+ **URL:** login?pairing-guid=<PAIRING GUID>&hasFP=1
210
+
211
+ **Authentication:** HSGID or PAIRING GUID
212
+
213
+ Used to login and get a `session id`, that is needed for most commands.
214
+ Example response from device:
215
+
216
+ mlog: [container, dmap.loginresponse]
217
+ mstt: 200 [uint, dmap.status]
218
+ mlid: 1739004399 [uint, dmap.sessionid]
219
+
220
+ Expected format for HSGID and PAIRING GUID respectively:
221
+
222
+ * HSGID: `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX`
223
+ * PAIRING GUID: `0xXXXXXXXXXXXXXXXX`
224
+
225
+ Where `X` corresponds to a hex digit (0-F).
226
+
227
+ ### playstatusupdate
228
+
229
+ **Type:** GET
230
+
231
+ **URL:** ctrl-int/1/playstatusupdate?session-id=<session id>&revision-number=<revision number>
232
+
233
+ **Authentication:** Session ID
234
+
235
+ The response contains information about what is currently playing. Example
236
+ response:
237
+
238
+ cmst: [container, dmcp.playstatus]
239
+ mstt: 200 [uint, dmap.status]
240
+ cmsr: 159 [uint, dmcp.serverrevision]
241
+ caps: 4 [uint, dacp.playstatus]
242
+ cash: 0 [uint, dacp.shufflestate]
243
+ carp: 0 [uint, dacp.repeatstate]
244
+ cafs: 0 [uint, dacp.fullscreen]
245
+ cavs: 0 [uint, dacp.visualizer]
246
+ cavc: False [bool, dacp.volumecontrollable]
247
+ caas: 1 [uint, dacp.albumshuffle]
248
+ caar: 1 [uint, dacp.albumrepeat]
249
+ cafe: False [bool, dacp.fullscreenenabled]
250
+ cave: False [bool, dacp.dacpvisualizerenabled]
251
+ ceQA: 0 [uint, unknown tag]
252
+ cann: Call On Me - Ryan Riback Remix [str, daap.nowplayingtrack]
253
+ cana: Starley [str, daap.nowplayingartist]
254
+ canl: Call On Me (Remixes) [str, daap.nowplayingalbum]
255
+ ceSD: b'...' [raw, unknown tag]
256
+ casc: 1 [uint, unknown tag]
257
+ caks: 6 [uint, unknown tag]
258
+ cant: 214005 [uint, dacp.remainingtime]
259
+ cast: 222000 [uint, dacp.tracklength]
260
+ casu: 0 [uint, dacp.su]
261
+
262
+ The field `cmsr` (dmcp.serverrevision) is used to realize "push updates".
263
+ By setting `<revision number>` to this number, the GET-request will block
264
+ until something happens on the device. This number will increase for each
265
+ update, so the next time it will be 160, 161, and so on. Using revision
266
+ number 0 will never block and can be used to poll current playstatus.
267
+
268
+ ### nowplayingartwork
269
+
270
+ **Type:** GET
271
+
272
+ **URL:** ctrl-int/1/nowplayingartwork?mw=1024&mh=576&session-id=<session id>
273
+
274
+ **Authentication:** Session ID
275
+
276
+ Returns a PNG image for what is currently playing, like a poster or album art.
277
+ If not present, an empty response is returned. Width and height of image can be
278
+ altered with `mw` and `mh`, but will be ignored if the available image is smaller
279
+ than the requested size.
280
+
281
+ ### ctrl-int
282
+
283
+ **Type:** POST
284
+
285
+ **URL:** ctrl-int/1/<command>?session-id=<session id>&prompt-id=0
286
+
287
+ **Authentication:** Session ID
288
+
289
+ <command> corresponds to the command to execute. Can be any of `play`, `pause`,
290
+ `nextitem` or `previtem`.
291
+
292
+ ### controlpromptentry
293
+
294
+ **Type:** POST
295
+
296
+ **URL:** ctrl-int/1/controlpromptentry?session-id=<session id>&prompt-id=0
297
+
298
+ **Authentication:** Session ID
299
+
300
+ Used to trigger various buttons, like menu or select. Must contain the
301
+ following binary DMAP data:
302
+
303
+ cmbe: <command> [string]
304
+ cmcc: 0 [string]
305
+
306
+ No external container is used. <command> can be either `select`, `menu` or
307
+ `topmenu`.
308
+
309
+ ### setproperty
310
+
311
+ **Type:** POST:
312
+
313
+ **URL:** ctrl-int/1/setproperty?<key>=<value>&session-id=<session id>&prompt-id=0
314
+
315
+ **Authentication:** Session ID
316
+
317
+ Changes a property for something.
318
+
319
+ Summary of supported properties:
320
+
321
+ | Key | Type | Value |
322
+ | --------------------- | ---- | ----------------------------------- |
323
+ | dacp.playingtime | uint | Time in seconds |
324
+ | dacp.shufflestate | bool | Shuffle state on/off |
325
+ | dacp.repeatstate | uint | Repeat mode (0=Off, 1=Track, 2=All) |
326
+
327
+
328
+ ## References
329
+
330
+ Https://en.wikipedia.org/wiki/Digital_Media_Access_Protocol
331
+
332
+ https://github.com/benumc/Apple-TV-Basic-IP/blob/master/apple_apple%20tv%20(ip).xml
333
+
334
+ https://nto.github.io/AirPlay.html
335
+
336
+ http://stackoverflow.com/questions/35355807/has-anyone-reversed-engineered-the-protocol-used-by-apples-ios-remote-app-for-c
337
+
338
+ # Media Remote Protocol (MRP)
339
+
340
+ The Media Remote Protocol (MRP) was introduced some time when Apple TV 4
341
+ and tvOS was launched. It is the protocol used by the Remote App as well as the Control
342
+ Center widget in iOS before iOS13. It is also the reason devices not running tvOS (e.g. Apple TV 3)
343
+ cannot be controlled from Control Center.
344
+
345
+ From a protocol point-of-view, it is based on Protocol Buffers
346
+ [(protobuf)](https://developers.google.com/protocol-buffers), developed by Google.
347
+ Every message is prefixed with a variant (in protobuf terminology), since protobuf
348
+ messages don't have lengths themselves. Service discovery is done with Zeroconf
349
+ using service `_mediaremotetv._tcp.local.`. The service contains some basic information,
350
+ like device name, but also which port is used for communication. The port can
351
+ change at any time (e.g. after reboot, but also at more random times) and usually
352
+ start with 49152 - the first ephemeral port.
353
+
354
+ ## Implementation
355
+
356
+ This is currently TBD, but you can can the code under `pyatv/mrp`.
357
+
358
+ ## References
359
+
360
+ In order not to duplicate information, please read more about the protocol
361
+ [here](https://github.com/jeanregisser/mediaremotetv-protocol).
362
+
363
+ # Companion Link
364
+
365
+ The Companion Link protocol is yet another protocol used to communicate between Apple
366
+ devices. Its purpose is not yet fully understood, so what is written here is
367
+ mostly speculation and guesses. If you feel that something is wrong or have more details,
368
+ please let me know.
369
+
370
+ The main driver for reverse engineering this protocol was to be able to launch apps in the
371
+ same way as the Shortcuts app, which was introduced in iOS 13. In iOS 13 Apple also
372
+ decided to switch from MRP to Companion Link in the remote widget found in action center.
373
+ Adding server-side support for Companion Link to the proxy would be a nice feature.
374
+ Guesses are that Continuity and Handoff are also built on top of this protocol, but that
375
+ is so far just speculation.
376
+
377
+ ## Service Discovery
378
+
379
+ Like with most Apple services, Zeroconf is used for service discovery. More precisely,
380
+ `_companion-link._tcp.local.` is the used service type. Here's a list of the properties
381
+ included in this service and typical values:
382
+
383
+ | Property | Example Value | Meaning |
384
+ | -------- | ------------- | ------- |
385
+ | rpHA | 45efecc5211 | HomeKit AuthTag
386
+ | rpHN | 86d44e4f11ff | Discovery Nonce
387
+ | rpVr | 195.2 | Likely protocol version
388
+ | rpMd | AppleTV6,2 | Device model name
389
+ | rpFl | 0x36782 | Some status flags (or supported features)
390
+ | rpAD | cc5011ae31ee | Bonjour Auth Tag
391
+ | rpHI | ffb855e34e31 | HomeKit rotating ID
392
+ | rpBA | E1:B2:E3:BB:11:FF | Bluetooth Address (can rotate)
393
+
394
+ Most values (except for rpVr, rpMd and rpFl) change every now and then (rotating encryption
395
+ scheme), likely for privacy reasons. It is still not known how these values are consumed.
396
+
397
+ ## Binary Format
398
+
399
+ The binary format is quite simple as it only consists of a message type, payload length
400
+ and the actual payload:
401
+
402
+ | Frame Type (1 byte) | Length (3 bytes) | Payload |
403
+
404
+ Since the message type is called "frame type", one message will be referred to as a
405
+ frame. The following frame types are currently known:
406
+
407
+ | Id | Name | Note |
408
+ | ---- | ---- | ---- |
409
+ | 0x00 | Unknown |
410
+ | 0x01 | NoOp |
411
+ | 0x03 | PS\_Start | Pair-Setup initial measage
412
+ | 0x04 | PS\_Next | Pair-Setup following messages
413
+ | 0x05 | PV\_Start | Pair-Verify initial message
414
+ | 0x06 | PV\_Next | Pair-Verify following measages
415
+ | 0x07 | U_OPACK |
416
+ | 0x08 | E_OPACK | This is used when launching apps
417
+ | 0x09 | P_OPACK |
418
+ | 0x0A | PA\_Req |
419
+ | 0x0B | PA\_Rsp |
420
+ | 0x10 | SessionStartRequest |
421
+ | 0x11 | SessionStartResponse |
422
+ | 0x12 | SessionData |
423
+ | 0x20 | FamilyIdentityRequest |
424
+ | 0x21 | FamilyIdentityResponse |
425
+ | 0x22 | FamilyIdentityUpdate |
426
+
427
+ The length field determines the size of the following payload in bytes (stored as
428
+ big endian). So far, only responses with frame type `E_OPACK` has been seen. The payload
429
+ in these frames is encoded with OPACK (described below), which should also be the
430
+ case for `U_OPACK` and `P_OPACK`.
431
+
432
+ ## OPACK
433
+
434
+ OPACK is an Apple internal serialization format found in the CoreUtils private framework.
435
+ It can serialize basic data types, like integers, strings, lists and dictionaries
436
+ in an efficient way. In some instances (like booleans and small numbers), a single
437
+ byte is sufficient. In other cases dynamic length fields are used to encode data size. Data is encoded using little endian where applicable and unless stated otherwise.
438
+
439
+ Most parts of this format have been reverse engineered, but it's not complete or
440
+ verified to be correct. If any discrepancies are found, please report them or open a PR.
441
+
442
+ An object is encoded or decoded according to this table:
443
+
444
+ | Bytes | Kind of Data | Example (python-esque) |
445
+ | ----- | ------------ | ---------------------- |
446
+ | 0x00 | Invalid | Reserved
447
+ | 0x01 | true | 0x01 = True
448
+ | 0x02 | false | 0x02 = False
449
+ | 0x03 | termination | 0xEF4163416403 = {"a": "b"} (See [Endless Collections](#endless-collections))
450
+ | 0x04 | null | 0x04 = None
451
+ | 0x05 | UUID4 (16 bytes) big-endian | 0x0512345678123456781234567812345678 = 12345678-1234-5678-1234-567812345678
452
+ | 0x06 | absolute mach time little-endian | 0x0000000000000000 = ?
453
+ | 0x07 | -1 (decimal) | 0x07 = -1 (decimal)
454
+ | 0x08-0x2F | 0-39 (decimal) | 0x17 = 15 (decimal)
455
+ | 0x30 | int32 1 byte length | 0x3020 = 32 (decimal)
456
+ | 0x31 | int32 2 byte length | 0x310020 = 32 (decimal)
457
+ | 0x32 | int32 4 byte length | 0x3200000020 = 32 (decimal)
458
+ | 0x33 | int32 8 byte length | 0x330000000000000020 = 32 (decimal)
459
+ | 0x34 | int32 16 byte length |
460
+ | 0x35 | float32 | 0x35xxxxxxxx = xxxxxxxx (signed, single precision)
461
+ | 0x36 | float64 | 0x36xxxxxxxxxxxxxxxx = xxxxxxxxxxxxxxxx (signed, double precision)
462
+ | 0x40-0x60 | string (0-32 chars) | 0x43666F6F = "foo"
463
+ | 0x61 | string 1 byte length | 0x6103666F6F = "foo"
464
+ | 0x62 | string 2 byte length | 0x620300666F6F = "foo"
465
+ | 0x63 | string 3 byte length | 0x62030000666F6F = "foo"
466
+ | 0x64 | string 4 byte length | 0x6303000000666F6F = "foo"
467
+ | 0x6F | null terminated string | 0x6F666F6F00 = "foo"
468
+ | 0x70-0x90 | raw bytes (0-32 bytes) | 0x72AABB = b"\xAA\xBB"
469
+ | 0x91 | data 1 byte length | 0x9102AABB = b"\xAA\xBB"
470
+ | 0x92 | data 2 byte length | 0x920200AABB = b"\xAA\xBB"
471
+ | 0x93 | data 3 byte length | 0x93020000AABB = b"\xAA\xBB"
472
+ | 0x94 | data 4 byte length | 0x9402000000AABB = b"\xAA\xBB"
473
+ | 0xA0-0xC0 | pointer | 0xD443666F6F43626172A0A1 = ["foo", "bar", "foo", "bar"] (see [Pointers](#pointers))
474
+ | 0xC1 | pointer 1 bytes length | 0xC102 = 2 (see [Pointers](#pointers))
475
+ | 0xC2 | pointer 2 bytes length | 0xC20002 = 2 (see [Pointers](#pointers))
476
+ | 0xC2 | pointer 3 bytes length | 0xC3000002 = 2 (see [Pointers](#pointers))
477
+ | 0xC4 | pointer 4 bytes length | 0xC400000003 = 2 (see [Pointers](#pointers))
478
+ | 0xDv | array with *v* elements | 0xD2016103666F6F = [True, "foo"]
479
+ | 0xEv | dictionary with *v* entries | 0xE16103666F6F0x17 = {"foo": 15}
480
+
481
+ ### Endless Collections
482
+
483
+ Dictionaries and lists support up to 14 elements when including number of elements in a single byte, e.g. `0xE3` corresponds to a
484
+ dictionary with three elements. It is however possible to represent lists, dictionaries and data objects with an endless amount of items
485
+ using `F` as count, i.e. `0xDF`, `0xEF` or `0x9F`. A byte with value `0x03` indicates end of a list, dictionary or data object.
486
+
487
+ A simple example with just one element, e.g. ["a"] looks like this:
488
+
489
+ ```raw
490
+ 0xDF416103
491
+ ```
492
+
493
+ Decoded form:
494
+
495
+ ```raw
496
+ DF : Endless list
497
+ 41 61 : "a"
498
+ 03 : Terminates previous list (or dict)
499
+ ```
500
+
501
+ ### Pointers
502
+
503
+ To save space, a *pointer* can be used to refer to an already defined object. A pointer is an index referring to the object order in the
504
+ byte stream, i.e. if three strings are placed in a list, index 0 would refer to the first string, index 1 to the second and so on. Lists and
505
+ dictionary bytes are ignored as well as other types represented by a single byte (e.g. a bool) as no space would be saved by a pointer.
506
+
507
+ The index table can be constructed by appending every new decoded object (excluding ignored types) to list. When a pointer byte is found,
508
+ subtract `0xA0` and use the obtained value as index in the list.
509
+
510
+ Here is a simple example to illustrate:
511
+
512
+ ```yaml
513
+ {
514
+ "a": False,
515
+ "b": "test",
516
+ "c": "test
517
+ }
518
+ ```
519
+
520
+ The above data structure would serialize to:
521
+
522
+ ```raw
523
+ E3416102416244746573744163A2
524
+ ```
525
+
526
+ Break-down of the data:
527
+
528
+ ```raw
529
+ E3 : Dictionary with three items
530
+ 41 61 : "a"
531
+ 02 : False
532
+ 41 62 : "b"
533
+ 44 74657374 : "test"
534
+ 41 63 : "c"
535
+ A2 : Pointer, index=2
536
+
537
+ ```
538
+
539
+ As single byte objects are ignored, the constructed index list looks
540
+ like `[a, b, test, c]`. Index 2 translates to `"test"` and `0xA2` is simply
541
+ replaced by that value.
542
+
543
+ The range `0xA0-0xC0` can be used to reference an object using a single byte.
544
+ It is also possible to use `0xC1-0xC4` to address objects beyond that. The lower
545
+ nibble (1-4) indicates how many bytes are used for the index.
546
+
547
+ ### Reference Decoding
548
+ To play around with various OPACK input, this example application can be used (only on macOS):
549
+
550
+ ```objectivec
551
+ #import <Foundation/Foundation.h>
552
+ #import <Foundation/NSJSONSerialization.h>
553
+
554
+ CFMutableDataRef OPACKEncoderCreateData(NSObject *obj, int32_t flags, int32_t *error);
555
+ NSObject* OPACKDecodeBytes(const void *ptr, size_t length, int32_t flags, int32_t *error);
556
+
557
+ int main(int argc, const char * argv[]) {
558
+ @autoreleasepool {
559
+ NSError *e = nil;
560
+ NSFileHandle *stdInFh = [NSFileHandle fileHandleWithStandardInput];
561
+ NSData *stdin = [stdInFh readDataToEndOfFile];
562
+
563
+ int decode_error = 0;
564
+ NSObject *decoded = OPACKDecodeBytes([stdin bytes], [stdin length], 0, &decode_error);
565
+ if (decode_error) {
566
+ NSLog(@"Failed to decode: %d", decode_error);
567
+ return -1;
568
+ }
569
+
570
+ NSLog(@"Decoded: %@", decoded);
571
+ }
572
+ return 0;
573
+ }
574
+ ```
575
+
576
+ Compile with:
577
+ ```shell
578
+ xcrun clang -fobjc-arc -fmodules -mmacosx-version-min=10.6 -F /System/Library/PrivateFrameworks/ -framework CoreUtils decode.m -o decode
579
+ ```
580
+
581
+ Then pass hex data to it like this:
582
+
583
+ ```shell
584
+ $ echo E3416102416244746573744163A2 | xxd -r -p | ./decode
585
+ 2021-04-19 21:14:57.243 decode[59438:2193666] decoded: {
586
+ a = 0;
587
+ b = test;
588
+ c = test;
589
+ }
590
+ ```
591
+
592
+ This excellent example comes straight from [fabianfreyer/opack-tools](https://github.com/fabianfreyer/opack-tools).
593
+
594
+ ## Authentication
595
+
596
+ Devices are paired and data encrypted according to HAP (HomeKit). You can refer to that specification
597
+ for further details (available [here](https://developer.apple.com/homekit/specification/),
598
+ but it requires an Apple ID, except for the Non-Commercial version, free to download).
599
+
600
+ Messages are presented in hex and a decoded format, based on the implementation in
601
+ pyatv. So beware that it will be somewhat python-oriented.
602
+
603
+ ### Pairing
604
+
605
+ The pairing sequence is initiated by the client sending a frame with type `PA_Start`. The following messages always use `PA_Next` as frame type. A typical flow looks like this (details below):
606
+
607
+ <code class="diagram">
608
+ sequenceDiagram
609
+ autonumber
610
+ Client->>ATV: M1: Pair-Setup Start (0x03)
611
+ Note over Client,ATV: _pd: Method=0x00, State=M1<br/>_pwTy: 1 (PIN Code)
612
+ ATV->>Client: M2: Pair-Setup Next (0x04)
613
+ Note over ATV,Client: _pd: State=M2, Salt, Pubkey, 0x1B (Unknown)
614
+ Note over Client,ATV: PIN Code is displayed on screen
615
+ Client->>ATV: M3: Pair-Setup Next (0x04)
616
+ Note over Client,ATV: _pd: State=M3, Pubkey, Proof<br/>_pwTy: 1 (PIN Code)
617
+ ATV->>Client: M4: Pair-Setup Next (0x04)
618
+ Note over ATV,Client: _pd: State=M4, Proof
619
+ Client->>ATV: M5: Pair-Setup Next (0x04)
620
+ Note over Client,ATV: _pd: State=M5, Encrypted Data<br/>_pwTy: 1 (PIN Code)
621
+ ATV->>Client: M6: Pair-Setup Next (0x04)
622
+ Note over ATV,Client: _pd: State=M6, Encrypted Data
623
+ </code>
624
+
625
+ The content of each frame is OPACK data containing a dictionary. The `_pd` key (*pairing data*) is TLV8 data according to HAP, and should be decoded according to that specification. Next follows more details for each message.
626
+
627
+ #### Client -> ATV: M1: Pair-Setup Start (0x03)
628
+ A client initiates a pairing request by sending a `PS_Start` message (M1).
629
+
630
+ Example data:
631
+ ```raw
632
+ Hex:
633
+ 03000013e2435f706476000100060101455f7077547909
634
+
635
+ Decoded:
636
+ frame_type=<FrameType.PS_Start: 3>, length=19, data={'_pd': {0: b'\x00', 6: b'\x01'}, '_pwTy': 1}
637
+ ```
638
+
639
+ #### ATV -> Client: M2: Pair-Setup Next (0x04)
640
+ When the ATV receives a `PS_Start` (M1), it will respond with `PS_Next` (M2) containing its public key (0x03) and salt (0x02). At this stage, a PIN code is displayed on screen which the client needs to generate a proof (0x04) sent in M3.
641
+
642
+ Example data:
643
+ ```raw
644
+ Hex:
645
+ 040001a4e1435f7064929c0106010202102558953b4496aecea0a367bafb29e98503ff6c33b53ca685062f6b8953f303bc30a01f0edeb64ed0cffaf570cc1b3aa9de5a7482d854671a8f72a9f72e3b5cbc60631499e292b4d749d9f0f69d47de657e63517753e342fbddea38d99cd69794847487accecd07993fabc60dcda50a25850c37357f1962c7eef91042381d951d9897030e57e7b12823c24ee183cc901e41d4f2dbf9de1e673574aedfaeaa86a5c37eaeccba1e112e3f650aa69389ac73c00dd405bbf0e7b204167974cf77295a1acde14a437f58fa9555de4b00b3d88e82ee375042ae54b7473303aa5a7091cd88f5e4a1fb63c2d80005f743e2484d4a1636509356f295dab6726410670ae2b514f68300c92643960e79963223b4809e69038194fab97b932b168a7962f3db8be188a418e25506c04c50aab80c2b42dfc108cedc7c5f0a9cbe23c9d34417a7840ec321071d32ca113a0fa2c7bbe3660efe21129eb407143e89a6ff5e655ae9c95dd735cb4130aadf46943653af001a4a981d32b12bf04f06dd85788c8e8401e5f4b544a72ddf8e58193f5873d9cfcdd3415393101b0101
646
+
647
+ Decoded:
648
+ frame_type=<FrameType.PS_Next: 4>, length=420, data={'_pd': {6: b'\x02', 2: b'%X\x95;D\x96\xae\xce\xa0\xa3g\xba\xfb)\xe9\x85', 3: b'l3\xb5<\xa6\x85\x06/k\x89S\xf3\x03\xbc0\xa0\x1f\x0e\xde\xb6N\xd0\xcf\xfa\xf5p\xcc\x1b:\xa9\xdeZt\x82\xd8Tg\x1a\x8fr\xa9\xf7.;\\\xbc`c\x14\x99\xe2\x92\xb4\xd7I\xd9\xf0\xf6\x9dG\xdee~cQwS\xe3B\xfb\xdd\xea8\xd9\x9c\xd6\x97\x94\x84t\x87\xac\xce\xcd\x07\x99?\xab\xc6\r\xcd\xa5\n%\x85\x0c75\x7f\x19b\xc7\xee\xf9\x10B8\x1d\x95\x1d\x98\x97\x03\x0eW\xe7\xb1(#\xc2N\xe1\x83\xcc\x90\x1eA\xd4\xf2\xdb\xf9\xde\x1eg5t\xae\xdf\xae\xaa\x86\xa5\xc3~\xae\xcc\xba\x1e\x11.?e\n\xa6\x93\x89\xacs\xc0\r\xd4\x05\xbb\xf0\xe7\xb2\x04\x16yt\xcfw)Z\x1a\xcd\xe1JC\x7fX\xfa\x95U\xdeK\x00\xb3\xd8\x8e\x82\xee7PB\xaeT\xb7G3\x03\xaaZp\x91\xcd\x88\xf5\xe4\xa1\xfbc\xc2\xd8\x00\x05\xf7C\xe2HMJ\x166P\x93V\xf2\x95\xda\xb6rd\x10g\n\xe2\xb5\x14\xf6\x83\x00\xc9&C\x96\x0ey\x962#\xb4\x80\x9ei\x94\xfa\xb9{\x93+\x16\x8ayb\xf3\xdb\x8b\xe1\x88\xa4\x18\xe2U\x06\xc0LP\xaa\xb8\x0c+B\xdf\xc1\x08\xce\xdc|_\n\x9c\xbe#\xc9\xd3D\x17\xa7\x84\x0e\xc3!\x07\x1d2\xca\x11:\x0f\xa2\xc7\xbb\xe3f\x0e\xfe!\x12\x9e\xb4\x07\x14>\x89\xa6\xff^eZ\xe9\xc9]\xd75\xcbA0\xaa\xdfF\x946S\xaf\x00\x1aJ\x98\x1d2\xb1+\xf0O\x06\xdd\x85x\x8c\x8e\x84\x01\xe5\xf4\xb5D\xa7-\xdf\x8eX\x19?Xs\xd9\xcf\xcd\xd3AS\x93\x10', 27: b'\x01'}}
649
+ ```
650
+
651
+ #### Client -> ATV: M3: Pair-Setup Next (0x04)
652
+ The client uses the PIN code to generate a proof (0x04) and sends it together with its public key in M3.
653
+
654
+ Example data:
655
+ ```raw
656
+ Hex:
657
+ 040001d8e2435f706492c90106010303ff992fcaa1f49bc6563e84fe283b34ba5efcf82b561dafdfcfa8dbffaa0e85fad1715b451586319cf3ec90b4961e8f793bfed6da9ab5a9b5c0fc11cb109ac91c0601801f1b150197198c44d1db67a1a0347c44db40bea50762089ea6a18896c2e161a6e80a2241e67ee8ac2cdf94c8899b09cccb310a681db44029248131dbc21ccfbdffae63d1c46e9a9ce77f309db673535dd8873100d917ee5fe13ac9a5490036cb4611ffacd0bb5389cf72aa2fbdd07227a98e83085bddd5851f459b0321a19a793ab03b5a972a0444f5a4c1e079666101b8699a9cd296d716bd87be2fcc81af4333267897ce74d4f072d8846c9d133270bae8b51bb15d0a856f06642ac903817497b588839a8ce1b4c89470cb8f5aaa647ac4387e08068c2074d42e89172bc3604a9140bba7e10404c2fecde3c02456a401c31f46ca35bf3a607e771987540607034793f42bce0685dffab35e6ff6871d9d85b3eee86d0b4069c90f024010659035a9b29adb3d6be996181eb088eb10e2706bccbc85900fca338533a891894c3c0440e4be1e32d5ba274436f38c40bc1ebbd3697b3de27e3a0908b73d7a81cdb196cdde02ed84140bae66b1149c57c62680a7d92ca503fd1a70e2d0a138800dc85324455f7077547909
658
+
659
+ Decoded:
660
+ frame_type=<FrameType.PS_Next: 4>, length=472, data={'_pd': {6: b'\x03', 3: b'\x99/\xca\xa1\xf4\x9b\xc6V>\x84\xfe(;4\xba^\xfc\xf8+V\x1d\xaf\xdf\xcf\xa8\xdb\xff\xaa\x0e\x85\xfa\xd1q[E\x15\x861\x9c\xf3\xec\x90\xb4\x96\x1e\x8fy;\xfe\xd6\xda\x9a\xb5\xa9\xb5\xc0\xfc\x11\xcb\x10\x9a\xc9\x1c\x06\x01\x80\x1f\x1b\x15\x01\x97\x19\x8cD\xd1\xdbg\xa1\xa04|D\xdb@\xbe\xa5\x07b\x08\x9e\xa6\xa1\x88\x96\xc2\xe1a\xa6\xe8\n"A\xe6~\xe8\xac,\xdf\x94\xc8\x89\x9b\t\xcc\xcb1\nh\x1d\xb4@)$\x811\xdb\xc2\x1c\xcf\xbd\xff\xaec\xd1\xc4n\x9a\x9c\xe7\x7f0\x9d\xb6sS]\xd8\x871\x00\xd9\x17\xee_\xe1:\xc9\xa5I\x006\xcbF\x11\xff\xac\xd0\xbbS\x89\xcfr\xaa/\xbd\xd0r\'\xa9\x8e\x83\x08[\xdd\xd5\x85\x1fE\x9b\x03!\xa1\x9ay:\xb0;Z\x97*\x04D\xf5\xa4\xc1\xe0yfa\x01\xb8i\x9a\x9c\xd2\x96\xd7\x16\xbd\x87\xbe/\xcc\x81\xafC3&x\x97\xcet\xd4\xf0r\xd8\x84l\x9d\x132p\xba\xe8\xb5\x1b\xb1]\n\x85o\x06d*\xc9t\x97\xb5\x88\x83\x9a\x8c\xe1\xb4\xc8\x94p\xcb\x8fZ\xaadz\xc48~\x08\x06\x8c t\xd4.\x89\x17+\xc3`J\x91@\xbb\xa7\xe1\x04\x04\xc2\xfe\xcd\xe3\xc0$V\xa4\x01\xc3\x1fF\xca5\xbf:`~w\x19\x87T\x06\x07\x03G\x93\xf4+\xce\x06\x85\xdf\xfa\xb3^o\xf6\x87\x1d\x9d\x85\xb3\xee\xe8m\x0b@i\xc9\x0f\x02@\x10e\x905\xa9\xb2\x9a\xdb=k\xe9\x96\x18\x1e\xb0\x88\xeb\x10\xe2pk\xcc\xbc\x85\x90\x0f\xca3\x853\xa8\x91\x89L<', 4: b"\xe4\xbe\x1e2\xd5\xba'D6\xf3\x8c@\xbc\x1e\xbb\xd3i{=\xe2~:\t\x08\xb7=z\x81\xcd\xb1\x96\xcd\xde\x02\xed\x84\x14\x0b\xaef\xb1\x14\x9cW\xc6&\x80\xa7\xd9,\xa5\x03\xfd\x1ap\xe2\xd0\xa18\x80\r\xc8S$"}, '_pwTy': 1}
661
+ ```
662
+
663
+ #### ATV -> Client: M4: Pair-Setup Next (0x04)
664
+ The ATV also generates a proof (0x04) and sends it back to the client in M4.
665
+
666
+ Example data:
667
+ ```raw
668
+ Hex:
669
+ 0400004ce1435f7064914506010404402598bf58f5e3f944b63df0c1e389f59b2dff2a97e2e25d86013a1a9e18c2c69ec1960d9ca2020c1a22b656d2fbb96d390df65604f94bef0ba8cc37bbcc2eca11
670
+
671
+ Decoded:
672
+ frame_type=<FrameType.PS_Next: 4>, length=76, data={'_pd': {6: b'\x04', 4: b'%\x98\xbfX\xf5\xe3\xf9D\xb6=\xf0\xc1\xe3\x89\xf5\x9b-\xff*\x97\xe2\xe2]\x86\x01:\x1a\x9e\x18\xc2\xc6\x9e\xc1\x96\r\x9c\xa2\x02\x0c\x1a"\xb6V\xd2\xfb\xb9m9\r\xf6V\x04\xf9K\xef\x0b\xa8\xcc7\xbb\xcc.\xca\x11'}}
673
+ ```
674
+
675
+ #### Client -> ATV: M5: Pair-Setup Next (0x04)
676
+ At this stage, both devices should have proved themselves to one another. The client will
677
+ create a certain payload and encrypt it with a session key and send it in M5 to the ATV.
678
+
679
+ The content of encrypted data is TLV8 encoded and contains an identifier (0x01), the clients
680
+ public key (0x03) and a signature (0x0A) according to HAP. It also contains an additional
681
+ item with data specific to the Companion protocol. It uses tag 17 and the content is encoded
682
+ with OPACK. An example of the payload looks like this (illustrative values):
683
+
684
+ ```python
685
+ {
686
+ "altIRK": b"-\x54\xe0\x7a\x88*en\x11\xab\x82v-'%\xc5",
687
+ "accountID": "DC6A7CB6-CA1A-4BF4-880D-A61B717814DB",
688
+ "model": "iPhone10,6",
689
+ "wifiMAC": b"@\xff\xa1\x8f\xa1\xb9",
690
+ "name": "Pierres iPhone",
691
+ "mac": b"@\xc4\xff\x8f\xb1\x99"
692
+ }
693
+ ```
694
+
695
+ Example data:
696
+ ```
697
+ Hex:
698
+ 040000ade2435f7064919f060105059af10dc2be3a537a73d7a89dd5d6a3114a6c9adbaf46a2b3a389b33381cf470de62d837f44da190266cfd4eb5c8f42350e2d4dec03e9354384be770e8f17fbf726cb21049589b912fdb88ba416dde56e033fd077e64c272f5cca2fd4c42d9143a9811f8897a81f5847fdc14f78e1bfba06005d3dc243e0ecb5af734348d7099ec1b252c64a04e04f1d146a90ad49da95f6a38e6d2755b41bc2d1b6455f7077547909
699
+
700
+ Decoded:
701
+ frame_type=<FrameType.PS_Next: 4>, length=2782, data={'_pd': {6: b'\x05', 5: b"\xf1\r\xc2\xbe:Szs\xd7\xa8\x9d\xd5\xd6\xa3\x11Jl\x9a\xdb\xafF\xa2\xb3\xa3\x89\xb33\x81\xcfG\r\xe6-\x83\x7fD\xda\x19\x02f\xcf\xd4\xeb\\\x8fB5\x0e-M\xec\x03\xe95C\x84\xbew\x0e\x8f\x17\xfb\xf7&\xcb!\x04\x95\x89\xb9\x12\xfd\xb8\x8b\xa4\x16\xdd\xe5n\x03?\xd0w\xe6L'/\\\xca/\xd4\xc4-\x91C\xa9\x81\x1f\x88\x97\xa8\x1fXG\xfd\xc1Ox\xe1\xbf\xba\x06\x00]=\xc2C\xe0\xec\xb5\xafsCH\xd7\t\x9e\xc1\xb2R\xc6J\x04\xe0O\x1d\x14j\x90\xadI\xda\x95\xf6\xa3\x8em'U\xb4\x1b\xc2\xd1\xb6"}, '_pwTy': 1}
702
+ ```
703
+
704
+ #### ATV -> Client: M6: Pair-Setup Next (0x04)
705
+ The concept here is the same as M5 (same kind of encrypted data).
706
+
707
+ Example data:
708
+ ```raw
709
+ Hex:
710
+ 0400012fe1435f706492270105ff8efc56bf0641a0fa53f00ae8da07a4ec5e929f5ec697e8692c8e833f175ecae4e381a8ced11097c76152031374926558cc8e64a0330097a241e76580c69d5d5a5017da1c393cee663be525ac1cc47229e491b3c1834a0d32ffc121d78e2d65bbc0efb5858615f49d6d43457a7c827f5c15bfc8a9da1f75839d24dbc8ddbbf2b658d3ded2848d9e1b92e8a7f4dd09f7f81b2108cf85be3910bfbb2045043d3cf3aa9619b63ba923acdae14e3cbc5a9b16c83b9a4e33e3d88d1af6c4154973ffaa8ca08a48f964056413a62551ff4628329c3bc836dfc14873b597f223ff4c4b6e17cc062cd66b34c475b3e272ecf47a8866457eb462fb2116f9134d443369540521dcaaed3b1a4622fec7806be71d4739a8f46327e8f41cc148f23a437dafb56575c3060106
711
+
712
+ Decoded:
713
+ frame_type=<FrameType.PS_Next: 4>, length=303, data={'_pd': {5: b'\x8e\xfcV\xbf\x06A\xa0\xfaS\xf0\n\xe8\xda\x07\xa4\xec^\x92\x9f^\xc6\x97\xe8i,\x8e\x83?\x17^\xca\xe4\xe3\x81\xa8\xce\xd1\x10\x97\xc7aR\x03\x13t\x92eX\xcc\x8ed\xa03\x00\x97\xa2A\xe7e\x80\xc6\x9d]ZP\x17\xda\x1c9<\xeef;\xe5%\xac\x1c\xc4r)\xe4\x91\xb3\xc1\x83J\r2\xff\xc1!\xd7\x8e-e\xbb\xc0\xef\xb5\x85\x86\x15\xf4\x9dmCEz|\x82\x7f\\\x15\xbf\xc8\xa9\xda\x1fu\x83\x9d$\xdb\xc8\xdd\xbb\xf2\xb6X\xd3\xde\xd2\x84\x8d\x9e\x1b\x92\xe8\xa7\xf4\xdd\t\xf7\xf8\x1b!\x08\xcf\x85\xbe9\x10\xbf\xbb E\x04=<\xf3\xaa\x96\x19\xb6;\xa9#\xac\xda\xe1N<\xbcZ\x9b\x16\xc8;\x9aN3\xe3\xd8\x8d\x1a\xf6\xc4\x15Is\xff\xaa\x8c\xa0\x8aH\xf9d\x05d\x13\xa6%Q\xffF(2\x9c;\xc86\xdf\xc1Hs\xb5\x97\xf2#\xffLKn\x17\xcc\x06,\xd6k4\xc4u\xb3\xe2r\xec\xf4z\x88fE~\xb4b\xfb!\x16\xf9\x13MD3iT\xdc\xaa\xed;\x1aF"\xfe\xc7\x80k\xe7\x1dG9\xa8\xf4c\'\xe8\xf4\x1c\xc1H\xf2:C}\xaf\xb5eu\xc3', 6: b'\x06'}})
714
+ ```
715
+
716
+ ### Verification
717
+
718
+ The verification sequence is initiated by the client by sending a frame with type `PV_Start`. The following messages always use `PV_Next` as frame type. A typical flow looks like this (details below):
719
+
720
+ <code class="diagram">
721
+ sequenceDiagram
722
+ autonumber
723
+ Client->>ATV: M1: Pair-Verify Start (0x04)
724
+ Note over Client,ATV: _pd: State=M1, Pubkey
725
+ ATV->>Client: M2: Pair-Verify Next (0x05)
726
+ Note over ATV,Client: _pd: State=M2, Pubkey, EncryptedData
727
+ Client->>ATV: M3: Pair-Verify Next (0x05)
728
+ Note over Client,ATV: _pd: State=M3, EncryptedData
729
+ ATV->>Client: M4: Pair-Verify Next (0x05)
730
+ Note over ATV,Client: _pd: State=M4
731
+ </code>
732
+
733
+ #### Client -> ATV: M1: Pair-Verify Start (0x05)
734
+ A client initiates a verification request by sending a `PV_Start` message (M1) containing
735
+ a public key for the new session.
736
+
737
+ Example data:
738
+ ```raw
739
+ Hex:
740
+ 05000033E2435F7064912506010103206665D845056F6D32584C8D213EB2E8B365F569084D5006268FDD9B818028FB23455F617554790C
741
+
742
+ Decoded:
743
+ frame_type=<FrameType.PV_Start: 5>, length=51, data={'_pd': b'\x06\x01\x01\x03 fe\xd8E\x05om2XL\x8d!>\xb2\xe8\xb3e\xf5i\x08MP\x06&\x8f\xdd\x9b\x81\x80(\xfb#', '_auTy': 4}
744
+ ```
745
+
746
+ #### ATV -> Client: M2: Pair-Verify Next (0x06)
747
+ When the Apple TV receives `M1`, it will respond with its session public key as well as
748
+ encrypted data used by the client to perform client verification in `M2`.
749
+
750
+ Example data:
751
+ ```raw
752
+ Hex:
753
+ 060000a6e1435f7064919f0578b5ecac3ecc240c38ac4c46c6b532bec01ffbb24390c45c19eabf5742bb0ad231983b8f7b42ae849494159e1240784c7d90edcf93fbe341bb3a36c66689a7cd690fbe5f0d7bcef2475c3510fb97da70452c61cf92af9e81d1549e28d56092720db5dce884c7739edaa0558c90078a286ae64d388215293b2e0601020320452357b145e149d20d91cd11f29475be78659279c67d4f9a1f04e0d56542de6b
754
+
755
+ Decoded:
756
+ frame_type=<FrameType.PV_Next: 6>, length=166, data={'_pd': b'\x05x\xb5\xec\xac>\xcc$\x0c8\xacLF\xc6\xb52\xbe\xc0\x1f\xfb\xb2C\x90\xc4\\\x19\xea\xbfWB\xbb\n\xd21\x98;\x8f{B\xae\x84\x94\x94\x15\x9e\x12@xL}\x90\xed\xcf\x93\xfb\xe3A\xbb:6\xc6f\x89\xa7\xcdi\x0f\xbe_\r{\xce\xf2G\\5\x10\xfb\x97\xdapE,a\xcf\x92\xaf\x9e\x81\xd1T\x9e(\xd5`\x92r\r\xb5\xdc\xe8\x84\xc7s\x9e\xda\xa0U\x8c\x90\x07\x8a(j\xe6M8\x82\x15);.\x06\x01\x02\x03 E#W\xb1E\xe1I\xd2\r\x91\xcd\x11\xf2\x94u\xbexe\x92y\xc6}O\x9a\x1f\x04\xe0\xd5eB\xdek'}
757
+ ```
758
+
759
+ #### Client -> ATV: M3: Pair-Verify Next (0x06)
760
+ The client verifies the identity of the Apple TV based on the encrypted data and responds with
761
+ corresponding data in `M3` back to the Apple TV.
762
+
763
+ Example data:
764
+ ```raw
765
+ Hex:
766
+ 06000084E1435F7064917D06010305786A89ECD933472C940493C34A6AD36E936B6AB49741390864E9EFCF029BCB0EFC599EA61E5FD5A55BA6D274D6DF0F1AB6ADCB9520DAC43645E8B757175E1BBF6F032D611918B8E18639703CFACD2FB2A330745EC09DD7F91235E2AA17A58D08C5E7FB52ADE66B170627C3490F517882C833E85127087C4D1A
767
+
768
+ Decoded:
769
+ frame_type=<FrameType.PV_Next: 6>, length=132, data={'_pd': b"\x06\x01\x03\x05xj\x89\xec\xd93G,\x94\x04\x93\xc3Jj\xd3n\x93kj\xb4\x97A9\x08d\xe9\xef\xcf\x02\x9b\xcb\x0e\xfcY\x9e\xa6\x1e_\xd5\xa5[\xa6\xd2t\xd6\xdf\x0f\x1a\xb6\xad\xcb\x95 \xda\xc46E\xe8\xb7W\x17^\x1b\xbfo\x03-a\x19\x18\xb8\xe1\x869p<\xfa\xcd/\xb2\xa30t^\xc0\x9d\xd7\xf9\x125\xe2\xaa\x17\xa5\x8d\x08\xc5\xe7\xfbR\xad\xe6k\x17\x06'\xc3I\x0fQx\x82\xc83\xe8Q'\x08|M\x1a"}
770
+ ```
771
+
772
+ #### ATV -> Client: M4: Pair-Verify Next (0x06)
773
+ If the client is verified properly, `M4` is sent back without an error code.
774
+
775
+ Example data:
776
+ ```raw
777
+ Hex:
778
+ Data=06000009e1435f706473060104
779
+
780
+ Decoded:
781
+ frame_type=<FrameType.PV_Next: 6>, length=9, data={'_pd': b'\x06\x01\x04'}
782
+ ```
783
+
784
+ ### Encryption
785
+
786
+ After verification has finished, all following messages are encrypted using the derived shared
787
+ key. Chacha20Poly1305 is used for encryption (just like HAP) with the following attributes:
788
+
789
+ * Salt: *empty string*
790
+ * Info: `ServerEncrypt-main` for decrypting (incoming), `ClientEncrypt-main` for encrypting (outgoing)
791
+
792
+ Sequence number (starting from zero) is used as nonce, incremented by one for each sent or
793
+ received message and encoded as little endian (12 bytes). Individual counters are used for each
794
+ direction. AAD should be set to the frame header. Do note that encrypting data will add a 16 byte
795
+ authentication tag at the end, increasing the size by 16 bytes. The AAD for three bytes of data
796
+ with `E_OPACK` as frame type would yield `0x08000013` as AAD for both encryption and decryption.
797
+
798
+ ### E_OPACK
799
+
800
+ Several types of data can be carried over the Companion protocol, but the one called `E_OPACK`
801
+ seems to be the one of interest for pyatv. It carries information for both the Apple TV remote
802
+ widget in Action Center as well as the Shortcuts app. So far, not much is known about the format
803
+ used by `E_PACK`, but what is known is documented here.
804
+
805
+ Lets start with a typical message (most data obfuscated or left out):
806
+
807
+ ```raw
808
+ "Send OPACK":{
809
+ "_i":"_systemInfo",
810
+ "_x":1499315511,
811
+ "_btHP":false,
812
+ "_c":{
813
+ "_pubID":"11:89:AA:A7:C9:F2",
814
+ "_sv":"230.1",
815
+ "_bf":0,
816
+ "_siriInfo":{
817
+ "collectorElectionVersion":1.0,
818
+ "deviceCapabilities":{
819
+ "seymourEnabled":1,
820
+ "voiceTriggerEnabled":2
821
+ },
822
+ "sharedDataProtoBuf":"..."
823
+ },
824
+ "_stA":[
825
+ "com.apple.LiveAudio",
826
+ "com.apple.siri.wakeup",
827
+ "com.apple.Seymour",
828
+ "com.apple.announce",
829
+ "com.apple.coreduet.sync",
830
+ "com.apple.SeymourSession"
831
+ ],
832
+ "_sigHKU":"",
833
+ "_clFl":128,
834
+ "_idsID":"5EFE874C-9681-4BFE-BB7B-E9B90776730A",
835
+ "_hkUID":[
836
+ "0ADF154C-A2D6-4641-90F0-F4F851A52111"
837
+ ],
838
+ "_dC":"1",
839
+ "_sigRP":"...",
840
+ "_sf":256,
841
+ "model":"iPhone10,6",
842
+ "name":"Pierres iPhone",
843
+ "_idHKU":"F9E5990A-F2A6-4E6D-A340-6D40BFF6BF87"
844
+ },
845
+ "_t":2
846
+ }
847
+ ```
848
+
849
+ There's a lot of information stuffed in there, but the main elements are these ones:
850
+
851
+ | **Tag** | **Name** | **Description** |
852
+ | _i | ID | Identifier for the message request or event, e.g. `_systemInfo` or `_launchApp`. |
853
+ | _c | Content | Additional data/arguments passed to whatever is specified in `_i`. |
854
+ | _t | Type | Type of message: 1=event, 2=request, 3=response |
855
+ | _x | XID | Likely "transfer ID". The response will contain the same XID as was specified in the request. Not used by all frame types (e.g. not by authentication frames). Integer with unknown range. |
856
+ | _sid | Session ID | Identifier used by sessions. |
857
+
858
+ Most messages seems to include the tags above. Here are a few other tags seen as well:
859
+
860
+ | **Tag** | **Name** | **Description** |
861
+ | _em | Error message | In case of error, e.g. `No request handler` if no handler exists for `_i` (i.e. invalid value for `_i`).
862
+ | _ec | Error code | In case of error, e.g. 58822 |
863
+ | _ed | Error domain | In case of error, e.g. RPErrorDomain |
864
+
865
+ #### Sessions (_sessionStart, _sessionStop)
866
+
867
+ When a client connects, it can establish a new session by sending `_sessionStart`. It
868
+ includes a 32 bit session ID called `_sid` (assumed to be randomized by the client) and a
869
+ service type called `_srvT` (endpoint the client wants to talk to):
870
+
871
+ ```javascript
872
+ {
873
+ '_i': '_sessionStart',
874
+ '_x': 123,
875
+ '_t': '2',
876
+ '_c': {
877
+ '_srvT': 'com.apple.tvremoteservices',
878
+ 'sid': 123456
879
+ }
880
+ }
881
+ ```
882
+
883
+ The server will respond with a remote `_sid` upon success:
884
+
885
+ ```javascript
886
+ {
887
+ '_c': {
888
+ '_sid': 1443773422
889
+ },
890
+ '_t': 3,
891
+ '_x': 123
892
+ }
893
+ ```
894
+
895
+ A final 64 bit session ID is then created by shifting up the received `_sid` 32 bits
896
+ and OR'ing it with the randomized `_sid`:
897
+
898
+ ```python
899
+ (1443773422 << 32) | 123456 = 6200959630324130368 = 0x560E3BEE0001E240
900
+ ```
901
+
902
+ This identifier is then used in further requests where `_sid` is required, e.g. when stopping
903
+ the session:
904
+
905
+ ```javascript
906
+ // Request
907
+ {
908
+ '_i': '_sessionStop',
909
+ '_x': 123,
910
+ '_t': '2',
911
+ '_c': {
912
+ '_sid': 6200959630324130368
913
+ }
914
+ }
915
+
916
+ // Response
917
+ {
918
+ '_c': {},
919
+ '_t': 3,
920
+ '_x': 123
921
+ }
922
+ ```
923
+
924
+ Combining both endpoint session ids into a single identifier is likely for convenience
925
+ reasons.
926
+
927
+ Some commands will not work until a session has been started. One example is `_launchApp`,
928
+ which won't work after the Apple TV has been restarted until the app list has been requested
929
+ by, e.g., the shortcuts app. The theory is that the `rapportd` process (implementing
930
+ the Companion protocol) acts like a proxy between clients and processes on the system.
931
+ When a client wants to call a function (e.g. `_launchApp`) handled by another process,
932
+ `_sessionStart` will make sure that function is available to call by setting up a session
933
+ to the process handling the function and relaying messages back and forth:
934
+
935
+ <code class="diagram">
936
+ sequenceDiagram
937
+ Client->>rapportd: _startSession: {_srvT=com.apple.tvremoteservices, _sid=123456}
938
+ rect rgb(0, 0, 255, 0.1)
939
+ Note over rapportd,tvremoteservices: Only if no previous session?
940
+ rapportd->>tvremoteservices: Start new session
941
+ tvremoteservices->>rapportd: {_sid: 1443773422}
942
+ end
943
+ rapportd->>Client: {_sid: 1443773422}
944
+ note over Client, rapportd: Interaction
945
+ Client->>rapportd: _stopSession: {_sid=6200959630324130368}
946
+ rapportd->>Client: {}
947
+ </code>
948
+
949
+ Once a command has been called, it will be cached making it possible to call it without
950
+ sending `_sessionStart` again. This is probably why `_launchApp` keeps working after
951
+ requesting the list from Shortcuts (as it will set up a new session).
952
+
953
+ #### Events
954
+
955
+ It is possible to subscribe to events using `_interest`:
956
+
957
+
958
+ ```javascript
959
+ {
960
+ '_i': '_interest',
961
+ '_x': 123,
962
+ '_t': '1,
963
+ '_c': {
964
+ '_regEvents: ['_iMC']
965
+ }
966
+ }
967
+ ```
968
+
969
+ No explicit response is sent to the request, other than an event update. So far `_iMC`
970
+ (Media Control) is the only known event type. An event update might look like this:
971
+
972
+ ```javascript
973
+ {
974
+ '_i': '_iMC',
975
+ '_x': 123,
976
+ '_c': {
977
+ '_mcF': 256
978
+ },
979
+ '_t': 1}
980
+ ```
981
+
982
+ The Media Control Flags (`mcF`) chunk is a bitmask with the following bits (not fully reversed
983
+ yet):
984
+
985
+ | Bitmask | Purpose |
986
+ | ------- | ------- |
987
+ | 0x0001 | Play
988
+ | 0x0002 | Pause
989
+ | 0x0004 | Previous track
990
+ | 0x0008 | Next track
991
+ | 0x0010 | Fast forward
992
+ | 0x0020 | Rewind
993
+ | 0x0040 | ?
994
+ | 0x0080 | ?
995
+ | 0x0100 | Volume
996
+ | 0x0200 | Skip forward (e.g. 30 seconds, defined by player)
997
+ | 0x0400 | Skip backward (e.g. 30 seconds, defined by player)
998
+
999
+ To unsubscribe, instead use `_deregEvents`:
1000
+
1001
+ ```javascript
1002
+ {
1003
+ '_i': '_interest',
1004
+ '_x': 123,
1005
+ '_t': 1,
1006
+ '_c': {
1007
+ '_deregEvents': ['_iMC']
1008
+ }
1009
+ }
1010
+ ```
1011
+
1012
+ #### Launch Application (_launchApp)
1013
+
1014
+ ```javascript
1015
+ // Request
1016
+ {'_i': '_launchApp', '_x': 123, '_t': '2', '_c': {'_bundleID': 'com.netflix.Netflix'}}
1017
+
1018
+ // Response
1019
+ {'_c': {}, '_t': 3, '_x': 123}
1020
+ ```
1021
+
1022
+ #### Fetch Application List (FetchLaunchableApplicationsEvent)
1023
+
1024
+ ```javascript
1025
+ // Request
1026
+ {'_i': 'FetchLaunchableApplicationsEvent', '_x': 123, '_t': '2', '_c': {}}
1027
+
1028
+ // Response
1029
+ {'_c': {'com.apple.podcasts': 'Podcaster', 'com.apple.TVMovies': 'Filmer', 'com.apple.TVWatchList': 'TV', 'com.apple.TVPhotos': 'Bilder', 'com.apple.TVAppStore': 'App\xa0Store', 'se.cmore.CMore2': 'C More', 'com.apple.Arcade': 'Arcade', 'com.apple.TVSearch': 'Sök', 'emby.media.emby-tvos': 'Emby', 'se.tv4.tv4play': 'TV4 Play', 'com.apple.TVHomeSharing': 'Datorer', 'com.google.ios.youtube': 'YouTube', 'se.svtplay.mobil': 'SVT Play', 'com.plexapp.plex': 'Plex', 'com.MTGx.ViaFree.se': 'Viafree', 'com.apple.TVSettings': 'Inställningar', 'com.apple.appleevents': 'Apple Events', 'com.kanal5.play': 'discovery+', 'com.netflix.Netflix': 'Netflix', 'se.harbourfront.viasatondemand': 'Viaplay', 'com.apple.TVMusic': 'Musik'}, '_t': 3, '_x': 123}
1030
+ ```
1031
+
1032
+ #### Buttons/Commands (_hidC)
1033
+
1034
+ Identifier shall be set to *_hidC* and content (*_c*), to the following:
1035
+
1036
+ | **Tag** | **Name** | **Value** |
1037
+ | _hBtS | Button state | 1=Down/pressed, 2=Up/released |
1038
+ | _hidC | Command | 1=Up<br/>2=Down<br/>3=Left<br/>4=Right<br/>5=Menu<br/>6=Select<br/>7=Home<br/>8=Volume up<br/>9=Volume down<br/>10=Siri<br/>11=Screensaver<br/>12=Sleep<br/>13=Wake<br/>14=PlayPause<br/>15=Channel Increment<br/>16=Channel Decrement<br/>17=Guide<br/>18=Page Up<br/>19=Page Down
1039
+
1040
+ Example: Put device to sleep:
1041
+
1042
+ ```javascript
1043
+ // Request
1044
+ {'_i': '_hidC', '_x': 123, '_t': '2', '_c': {'_hBtS': 2, '_hidC': 12}}
1045
+
1046
+ // Response
1047
+ {'_c': {}, '_t': 3, '_x': 123}
1048
+ ```
1049
+
1050
+ #### Touch gestures
1051
+
1052
+ Additional information about slide gestures : slide gestures are handled with a succession of events with about 20ms between each requests
1053
+
1054
+ 1. First event with _tPh=1 (press mode)
1055
+ 2. N requests with _tPh = 3 (where N*100 ms = duration of the gesture), with _cx and _cy coordinates changing at each request
1056
+ 3. One last request with _tPh=4 when released
1057
+
1058
+ To be noted :
1059
+ - _cx and _cy coordinates must be in the range [0,1000] which is set in a startup event _touchStart :
1060
+ ```javascript
1061
+ // Received during initialization
1062
+ {'_i': '_touchStart', '_x': 1865081428, '_btHP': False, '_c': {'_height': 1000.0, '_tFl': 0, '_width': 1000.0}, '_t': 2}
1063
+ ```
1064
+ - _ns = timestamp in nanoseconds (probably based on device boot time)
1065
+ - when reaching the edge of the touch area, a release event should be sent with a new press event otherwise the cursor will move in the opposite way
1066
+
1067
+
1068
+ #### Single tap
1069
+
1070
+ 3 requests have to be sent to simulate tap gesture on the touch pad : 2 commands requests (_hidC) and 1 event request (_hidT)
1071
+ - 2 requests with _i = _hidC and in the additional arguments structure _C :
1072
+ - the button pressed : _hidC = 6 for touch pad click
1073
+ - the action mode : _hBtS = 1 for press, and the second request with _hBtS = 2 for release
1074
+ - 1 event with additional arguments as followed :
1075
+ - _cx and _cy (x,y coordinates) set to max : 1000
1076
+ - _tPh : action mode of the touchpad set to 5
1077
+
1078
+ ```javascript
1079
+ // Request
1080
+ {'_i': '_hidC', '_x': 1984212224, '_btHP': False, '_c': {'_hBtS': 1, '_hidC': 6}, '_t': 2}
1081
+ {'_i': '_hidC', '_x': 1984212225, '_btHP': False, '_c': {'_hBtS': 2, '_hidC': 6}, '_t': 2}
1082
+ {'_i': '_hidT', '_x': 1984212226, '_c': {'_ns': 713243707438041, '_tFg': 1, '_cx': 1000, '_tPh': 5, '_cy': 1000}, '_t': 1}
1083
+
1084
+ // Response
1085
+ R: {'_c': {}, '_t': 3, '_x': 1984212224}
1086
+ R: {'_c': {}, '_t': 3, '_x': 1984212225}
1087
+ ```
1088
+
1089
+
1090
+ #### Gestures
1091
+
1092
+ Touch gestures are a series of events (_i = "_hidT", _t = 1) sent every few milliseconds (~20ms) with updated x,y coordinates
1093
+ - 1 start event with _tPh = 1 (pressed event)
1094
+ - N events with _tPh = 3 (hold)
1095
+ - 1 end event with _tPh = 4 (released)
1096
+
1097
+ ```javascript
1098
+ // Request
1099
+ {'_i': '_hidT', '_x': 588648840, '_c': {'_ns': 713018028759791, '_tFg': 1, '_cx': 500, '_tPh': 1, '_cy': 800}, '_t': 1}
1100
+ {'_i': '_hidT', '_x': 588648841, '_c': {'_ns': 713018124653791, '_tFg': 1, '_cx': 506, '_tPh': 3, '_cy': 789}, '_t': 1}
1101
+ {'_i': '_hidT', '_x': 588648842, '_c': {'_ns': 713018141323791, '_tFg': 1, '_cx': 520, '_tPh': 3, '_cy': 767}, '_t': 1}
1102
+ {'_i': '_hidT', '_x': 588648843, '_c': {'_ns': 713018157994791, '_tFg': 1, '_cx': 520, '_tPh': 3, '_cy': 758}, '_t': 1}
1103
+ ...
1104
+ {'_i': '_hidT', '_x': 588648930, '_c': {'_ns': 713019612448791, '_tFg': 1, '_cx': 587, '_tPh': 3, '_cy': 128}, '_t': 1}
1105
+ {'_i': '_hidT', '_x': 588648931, '_c': {'_ns': 713019616615791, '_tFg': 1, '_cx': 593, '_tPh': 4, '_cy': 134}, '_t': 1}
1106
+ ```
1107
+
1108
+
1109
+ #### System Status
1110
+
1111
+ A system can be in one of the following states:
1112
+
1113
+ | **ID** | **State** | **Note** |
1114
+ | 0x01 | Asleep | Device is sleeping/in standby.
1115
+ | 0x02 | Screensaver | Screensaver is shown on screen.
1116
+ | 0x03 | Awake | Device is awake.
1117
+ | 0x04 | Idle | This state has not been seen, but is likely present. Not sure what difference is compare to `Awake`.
1118
+
1119
+ Current system state can be fetch using `FetchAttentionState`:
1120
+
1121
+ ```javascript
1122
+ // Request
1123
+ {'_i': 'FetchAttentionState', '_t': 2, '_c': {}, '_x': 38571}
1124
+
1125
+ // Response
1126
+ {'_c': {'state': 1}, '_t': 3, '_x': 38571}
1127
+ ```
1128
+
1129
+ `state` in the response maps to **ID** in the table above.
1130
+
1131
+ Updates to the state is announced via the `SystemStatus` event:
1132
+
1133
+ ```javascript
1134
+ // Register to event
1135
+ {'_i': '_interest', '_t': 1, '_c': {'_regEvents': ['SystemStatus']}, '_x': 38573}
1136
+
1137
+ // Example of an event
1138
+ {'_i': 'SystemStatus', '_x': 798413326, '_c': {'state': 3}, '_t': 1}
1139
+ ```
1140
+
1141
+ # AirPlay
1142
+
1143
+ The AirPlay protocol suite is used to stream media from a sender to a receiver. Two protocols
1144
+ are used: AirTunes and "AirPlay". The former is used for audio streaming and is based on
1145
+ *Real-Time Streaming Protocol*. The latter adds video and image capabilities to the stack,
1146
+ allowing video streaming, screen mirroring and image sharing.
1147
+
1148
+ There's quite a history behind the AirPlay stack and I haven't fully grasped it yet. But I
1149
+ *think* it looks something like this:
1150
+
1151
+ <code class="diagram">
1152
+ graph LR
1153
+ AT[AirTunes, 2004] --> AT2(AirTunes v2, 2010)
1154
+ AT2 --> APS1
1155
+ AP1[AirPlay, 2010] --> APS1
1156
+ APS1[AirPlay v1, 2010] --> APS2
1157
+ APS2[AirPlay v2, 2018]
1158
+ </code>
1159
+
1160
+ AirTunes (i.e. Airplay v1) is announced as *Remote Audio Output Protocol*, e.g. when looking at Zeroconf
1161
+ services. That's also what it will be referred to here.
1162
+
1163
+ As the AirPlay protocol is covered a lot elsewhere, I will update here when I'm bored. Please
1164
+ refer to the references for more details on the protocol.
1165
+
1166
+ ## Service Discovery
1167
+
1168
+ AirPlay uses two services, one for audio and one for video. They are described here.
1169
+
1170
+ ### RAOP
1171
+
1172
+ | **Property** | **Example value** | **Meaning** |
1173
+ | ------------ | ----------------- | ----------- |
1174
+ | et | 0,4 | Encryption type: 0=unencrypted, 1=RSA (AirPort Express), 3=FairPlay, 4=MFiSAP, 5=FairPlay SAPv2.5
1175
+ | da | true | Digest Authentication
1176
+ | ss | 16 | Audio sample size in bits
1177
+ | am | AppleTV6,2 | Apple (device) Model
1178
+ | tp | TCP,UDP | Supported transport protocols for media streams
1179
+ | pw | false | Password protected
1180
+ | fv | s8927.1096.0 | Firmware version (non-Apple)
1181
+ | txtvers | 1 | TXT record version 1
1182
+ | vn | 65537 | Version Number (uint16.uint16, e.g. 1.1 = 65537)
1183
+ | md | 0,1,2 | Supported metadata: 0=text, 1=artwork, 2=progress (only for pre iOS7 senders)
1184
+ | vs | 103.2 | Server version
1185
+ | sv | false | Software Volume, whether receiver needs sender to adjust their volume.
1186
+ | sm | false | Software Mute, (as above).
1187
+ | ch | 2 | Number of audio channels
1188
+ | sr | 44100 | Audio sample rate
1189
+ | cn | 0,1 | Audio codecs: 0=PCM, 1=AppleLossless (ALAC), 2=AAC, 3=AAC ELD, 4=OPUS
1190
+ | ov | 8.4.4 | Operating system version? (seen on ATV 3)
1191
+ | pk | 38fd7e... | Public key
1192
+ | pw | false | Whether password (PIN) auth is required. Triggers Method POST Path /pair-pin-start from sender
1193
+ | ft | 0x8074... | Supported Features (hex integer bitmask)
1194
+ | sf | 0x387e... | System Flags (hex integer bitmask)
1195
+
1196
+
1197
+
1198
+ ### AirPlay
1199
+
1200
+ | **Property** | **Example value** | **Meaning** |
1201
+ | ------------ | --------------------- | ----------- |
1202
+ | features | 0x4A7FDFD5,0x3C155FDE | Features supported by device, see [here](https://openairplay.github.io/airplay-spec/features.html)
1203
+ | igl | 1 | Is Group Leader
1204
+ | model | AppleTV6,2 | Model name
1205
+ | osvers | 14.5 | Operating system version
1206
+ | pi | UUID4 | Group ID
1207
+ | vv | 2 | ?
1208
+ | srcvers | 540.31.41 | AirPlay version
1209
+ | psi | UUID4 | Public AirPlay Pairing Identifier
1210
+ | gid | UUID4 | Group UUID
1211
+ | pk | UUID4 | Public key
1212
+ | acl | 0 | Access Control Level
1213
+ | deviceid | AA:BB:CC:DD:EE:FF | Device identifier, typically MAC address
1214
+ | protovers | Protocol version
1215
+ | fex | 1d9/St5fFTw | ?
1216
+ | gcgl | 1 | Group Contains Group Leader
1217
+ | flags | 0x244 | Status flags, see [here](https://openairplay.github.io/airplay-spec/status_flags.html)
1218
+ | btaddr | AA:BB:CC:DD:EE:FF | Bluetooth address
1219
+
1220
+ ## RAOP
1221
+
1222
+ This section covers the audio streaming part of AirPlay, i.e. AirTunes/RAOP. TBD
1223
+
1224
+ ### RTSP
1225
+
1226
+ Streaming sessions are set up using the RTSP protocol. This section covers the basics of how
1227
+ that is done.
1228
+
1229
+ #### OPTIONS
1230
+
1231
+ Sender asks receiver what methods it supports:
1232
+
1233
+ **Sender -> Receiver:**
1234
+ ```raw
1235
+ OPTIONS * RTSP/1.0
1236
+ CSeq: 0
1237
+ nUser-Agent: AirPlay/540.31
1238
+ DACP-ID: A851074254310A45
1239
+ Active-Remote: 4019753970
1240
+ Client-Instance: A851074254310A45
1241
+ ```
1242
+
1243
+ **Receiver -> Sender:**
1244
+ ```raw
1245
+ RTSP/1.0 200 OK
1246
+ Date: Tue, 11 May 2021 17:35:10 GMT
1247
+ Content-Length: 0
1248
+ Public: ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, POST, GET, PUT
1249
+ Server: AirTunes/540.31.41
1250
+ CSeq: 0
1251
+ ```
1252
+
1253
+ #### ANNOUNCE
1254
+
1255
+ Sender tells the receiver about properties for an upcoming stream.
1256
+
1257
+ **Sender -> Receiver:**
1258
+ ```raw
1259
+ ANNOUNCE rtsp://10.0.10.254/4018537194 RTSP/1.0
1260
+ CSeq: 0
1261
+ User-Agent: AirPlay/540.31
1262
+ DACP-ID: 9D881F7AED72DB4A
1263
+ Active-Remote: 3630929274
1264
+ Client-Instance: 9D881F7AED72DB4A
1265
+ Content-Type: application/sdp
1266
+ Content-Length: 179
1267
+
1268
+ v=0
1269
+ o=iTunes 4018537194 0 IN IP4 10.0.10.254
1270
+ s=iTunes
1271
+ c=IN IP4 10.0.10.84
1272
+ t=0 0
1273
+ m=audio 0 RTP/AVP 96
1274
+ a=rtpmap:96 AppleLossless
1275
+ a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100
1276
+ ```
1277
+
1278
+ Some observations (might not be true):
1279
+
1280
+ * ID in `o=` property (`4018537194`) seems to match what is used for rtsp endpoint (`rtsp://xxx/4018537194`)
1281
+ * Address in `o=` corresponds to IP address of the sender
1282
+ * Address in `c=` is address of the receiver
1283
+ * Configuration for ALAC is used here. Format for `fmtp` is `a=fmtp:96 <frames per packet> <ALAC version, 0> <ALAC sample size> <ALAC history mult> <ALAC initial history> <ALAC rice limit> <ALAC channel count> <ALAC max run> <ALAC max coded frame size, 0 for auto/unknown> <ALAC average bitrate, 0 for auto> <ALAC sample rate>`
1284
+
1285
+ **Receiver -> Sender:**
1286
+ ```raw
1287
+ RTSP/1.0 200 OK
1288
+ Date: Tue, 11 May 2021 17:25:54 GMT
1289
+ Content-Length: 0
1290
+ Server: AirTunes/540.31.41
1291
+ CSeq: 0
1292
+ ```
1293
+
1294
+ #### SETUP
1295
+
1296
+ Sender requests initialization of a (Airplay v1) session (but does not start it). Sets up three different UDP channels:
1297
+
1298
+ | Channel | Description |
1299
+ | ------- | ----------- |
1300
+ | server | audio
1301
+ | control | sync and retransmission of lost frames
1302
+ | timing | sync of common master clock
1303
+
1304
+ **Sender -> Receiver:**
1305
+ ```raw
1306
+ SETUP rtsp://10.0.10.254/1085946124 RTSP/1.0
1307
+ CSeq: 2
1308
+ User-Agent: AirPlay/540.31
1309
+ DACP-ID: A851074254310A45
1310
+ Active-Remote: 4019753970
1311
+ Client-Instance: A851074254310A45
1312
+ Transport: RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;control_port=55433;timing_port=55081
1313
+ ```
1314
+
1315
+ **Receiver -> Sender:**
1316
+ ```raw
1317
+ RTSP/1.0 200 OK
1318
+ Date: Tue, 11 May 2021 17:35:11 GMT
1319
+ Content-Length: 0
1320
+ Transport: RTP/AVP/UDP;unicast;mode=record;server_port=55801;control_port=50367;timing_port=0
1321
+ Session: 1
1322
+ Audio-Jack-Status: connected
1323
+ Server: AirTunes/540.31.41
1324
+ CSeq: 2
1325
+ ```
1326
+
1327
+ #### SETPEERS
1328
+
1329
+ Describes PTP timing peers to the receiver.
1330
+
1331
+ ```raw
1332
+ ...
1333
+ Content-Type: /peer-list-changed
1334
+
1335
+ Contains [] array of IP{4|6}addrs e.g.:
1336
+ ['::',
1337
+ '::',
1338
+ '127.0.0.1']
1339
+ ```
1340
+
1341
+ #### RECORD
1342
+
1343
+ Requests to start the stream at a particular point. Initially, a sequence (16bit) number and start time (32bit) are included in `RTP-Info` which correspond to those in the first RTP packet. These values are
1344
+ randomized.
1345
+
1346
+ **Sender -> Receiver:**
1347
+ ```raw
1348
+ RECORD rtsp://10.0.10.254/1085946124 RTSP/1.0
1349
+ CSeq: 6
1350
+ User-Agent: AirPlay/540.31
1351
+ DACP-ID: A851074254310A45
1352
+ Active-Remote: 4019753970
1353
+ Client-Instance: A851074254310A45
1354
+ Range: npt=0-
1355
+ Session: 1
1356
+ RTP-Info: seq=15432;rtptime=66150
1357
+ ```
1358
+
1359
+ **Receiver -> Sender:**
1360
+ ```raw
1361
+ RTSP/1.0 200 OK
1362
+ Date: Tue, 11 May 2021 07:35:11 GMT
1363
+ Content-Length: 0
1364
+ Audio-Latency: 3035
1365
+ Server: AirTunes/540.31.41
1366
+ CSeq: 6
1367
+ ```
1368
+
1369
+ #### FLUSH
1370
+
1371
+ Requests to flush the receivers buffer and pause/stop what is playing.
1372
+
1373
+ **Sender -> Receiver:**
1374
+ ```raw
1375
+ FLUSH rtsp://10.0.10.254/1085946124 RTSP/1.0
1376
+ CSeq: 7
1377
+ User-Agent: AirPlay/540.31
1378
+ DACP-ID: A851074254310A45
1379
+ Active-Remote: 4019753970
1380
+ Client-Instance: A851074254310A45
1381
+ ```
1382
+
1383
+ **Receiver -> Sender:**
1384
+ ```raw
1385
+ RTSP/1.0 200 OK
1386
+ Date: Tue, 11 May 2021 17:35:11 GMT
1387
+ Content-Length: 0
1388
+ Server: AirTunes/540.31.41
1389
+ CSeq: 7
1390
+ ```
1391
+
1392
+ #### TEARDOWN
1393
+
1394
+ End the active session.
1395
+
1396
+ **Sender -> Receiver:**
1397
+ ```raw
1398
+ TEARDOWN rtsp://10.0.10.254/1085946124 RTSP/1.0
1399
+ CSeq: 8
1400
+ User-Agent: AirPlay/540.31
1401
+ DACP-ID: A851074254310A45
1402
+ Active-Remote: 4019753970
1403
+ Client-Instance: A851074254310A45
1404
+ ```
1405
+
1406
+ **Receiver -> Sender:**
1407
+ ```raw
1408
+ RTSP/1.0 200 OK
1409
+ Date: Tue, 11 May 2021 17:35:19 GMT
1410
+ Content-Length: 0
1411
+ Server: AirTunes/540.31.41
1412
+ CSeq: 8
1413
+ ```
1414
+
1415
+ #### SET_PARAMETER
1416
+
1417
+ Change a parameter, e.g. metadata or progress, on the receiver.
1418
+
1419
+ **Sender -> Receiver:**
1420
+ ```raw
1421
+ SET_PARAMETER rtsp://10.0.10.254/1085946124 RTSP/1.0
1422
+ CSeq: 3
1423
+ User-Agent: AirPlay/540.31
1424
+ DACP-ID: A851074254310A45
1425
+ Active-Remote: 4019753970
1426
+ Client-Instance: A851074254310A45
1427
+ Content-Type: text/parameters
1428
+ Content-Length: 11
1429
+
1430
+ volume: -20
1431
+ ```
1432
+
1433
+ **Receiver -> Sender:**
1434
+ ```raw
1435
+ RTSP/1.0 200 OK
1436
+ Date: Tue, 11 May 2021 17:35:11 GMT
1437
+ Content-Length: 0
1438
+ Server: AirTunes/540.31.41
1439
+ CSeq: 3
1440
+ ```
1441
+
1442
+ ## AirPlay
1443
+
1444
+ This section deals with the "video part" of AirPlay. TBD
1445
+
1446
+ ### Commands
1447
+
1448
+ #### /auth-setup
1449
+
1450
+ Devices supporting MFi authentication (e.g. has `et=4`) might require an authentication step
1451
+ initiated by `/auth-setup`. This is always the case for AirPlay 2. More details
1452
+ [here](https://openairplay.github.io/airplay-spec/audio/rtsp_requests/post_auth_setup.html).
1453
+
1454
+ *TODO: document more*
1455
+
1456
+ The request consists of one byte encryption type (0x01: unencrypted,
1457
+ 0x02: MFi-SAP-encrypted AES key) and 32 bytes Curve25519 public key. Normally this step is used
1458
+ to verify MFi authenticity, but no further action needs to be taken (i.e. just send request
1459
+ and ignore response) for devices requiring this step. Implementation in `pyatv` has been stolen
1460
+ from owntone [here](https://github.com/owntone/owntone-server/blob/c1db4d914f5cd8e7dbe6c1b6478d68a4c14824af/src/outputs/raop.c#L1568).
1461
+
1462
+ **Sender -> Receiver:**
1463
+ ```raw
1464
+ POST /auth-setup RTSP/1.0
1465
+ CSeq: 0
1466
+ User-Agent: AirPlay/540.31
1467
+ DACP-ID: BFAA2A9155BD093C
1468
+ Active-Remote: 347218209
1469
+ Client-Instance: BFAA2A9155BD093C
1470
+ Content-Type: application/octet-stream
1471
+ Content-Length: 33
1472
+
1473
+ 015902ede90d4ef2bd4cb68a6330038207a94dbd50d8aa465b5d8c012a0c7e1d4e27
1474
+ ```
1475
+
1476
+ **Receiver -> Sender:**
1477
+ ```raw
1478
+ RTSP/1.0 200 OK
1479
+ Content-Length: 1076
1480
+ Content-Type: application/octet-stream
1481
+ Server: AirTunes/366.0
1482
+ CSeq: 0
1483
+
1484
+ 97a02c0d0a31486316de944d8404f4e01f93b05dde4543cc022a5727e8a352330000038c3082038806092a864886f70d010702a0820379308203750201013100300b06092a864886f70d010701a082035d3082035930820241a003020102020f1212aa121127aa00aa8023aa238776300d06092a864886f70d0101050500308183310b300906035504061302555331133011060355040a130a4170706c6520496e632e31263024060355040b131d4170706c652043657274696669636174696f6e20417574686f72697479313730350603550403132e4170706c652069506f64204163636573736f726965732043657274696669636174696f6e20417574686f72697479301e170d3132313132373138323135305a170d3230313132373138323135305a3070310b300906035504061302555331133011060355040a0c0a4170706c6520496e632e311f301d060355040b0c164170706c652069506f64204163636573736f72696573312b302906035504030c224950415f31323132414131323131323741413030414138303233414132333837373630819f300d06092a864886f70d010101050003818d003081890281810097e302c45e7b6f387dd390201b0dd902b19dc30d72a93a8b9f1313c6108e90ee93daff24177526736e4f1f58a2c2382cf4b7f7359bb1b1a3a7595850d489f335557a48653d96e9407ccc05eba6c867716e446b31d2bdc9c5122af4c213e7d7f0635b74e323094483a900bd3f93ce8833785b2fd14d88fb2dd4c581e1189b38250203010001a360305e301d0603551d0e04160414d74ea8b90475ee5140d2be7d2f9258931c7543cb300c0603551d130101ff04023000301f0603551d23041830168014ff4b1a439af51996ab18002b61c9ee409d8ec704300e0603551d0f0101ff0404030203b8300d06092a864886f70d0101050500038201010012e8b29b1e1b81e7a14b6435b92a9c58f0a28e6bcb645edd223969b77a70dda3ddc280562f53cb87e3ccd5fea213ccc9c2a4f005c3aa4447b84a895df649f74e9f6612d6cc69eeb7561706fa718f5e1d80b0554affe911c6fa3f99ca06bcf4debf03b64449bde16058392c830be55ae33273d24eecaf0f4aef6f6c46bed87192e2773e5ae092098b32563a532164df5eecd3fc299c8b267cf555b516b02a013920242f4162e6cb5d8d555356d3999c989860ed8c4ea2a0f34af4bcc74b864a07c6d952115dd28b0cc5d8bc780567dcaafc721e678391a048b00cf8664d5c0ad1949b57165a7c98144480ac0510a1887e27821d966b14478c901f6c7548f8563e310000000080121b14309c641bc593196f886c633d19986c11ca9cb4be2fdad1f2ec1427eeb8da23aaeaf7a713f2b8e05a6942db364e3dd408d5a1eeb1525baadc5ccb46614dadef1bfa565c65f46a54f576802209faa39ac442ac7cd43995be833f7794d0517fd93218e86c0228b30b036d3055476114d926de2875bed7cef4970492df58a3
1485
+ ```
1486
+
1487
+ ## References
1488
+
1489
+ [RAOP-Player](https://github.com/philippe44/RAOP-Player)
1490
+
1491
+ [owntone-server](https://github.com/owntone/owntone-server)
1492
+
1493
+ [Unofficial AirPlay Specification](https://openairplay.github.io/airplay-spec/introduction.html)
1494
+
1495
+ [AirPlay 2 Internals](https://emanuelecozzi.net/docs/airplay2)
1496
+
1497
+ [Using raw in ALAC frames (Stackoverflow)](https://stackoverflow.com/questions/34584522/airplay-protocol-how-to-use-raw-pcm-instead-of-alac)
1498
+
1499
+ [Unofficial AirPlay Protocol Specification](https://nto.github.io/AirPlay.html)
1500
+
1501
+ [AirTunes v2](https://git.zx2c4.com/Airtunes2/about/)
1502
+
1503
+ [AirPlayAuth](https://github.com/funtax/AirPlayAuth)
1504
+
1505
+ # AirPlay 2
1506
+
1507
+ In reality, AirPlay 2 has a lot in common with its predecessor, but a lot also differs
1508
+ so it deserves its own chapter.
1509
+
1510
+ For now, the main focus here is to describe how AirPlay can be set up for remote control
1511
+ only, i.e. how to get metadata for what is playing. Other parts will be added later.
1512
+
1513
+ ## Service Discovery
1514
+
1515
+ TBD
1516
+
1517
+ ## Authentication
1518
+
1519
+ There are multiple ways to authenticate a device, all based on HAP just like Companion
1520
+ and MRP. Implementations of regular pairing and transient pairing (written in C) can
1521
+ be found [here](https://github.com/ejurgensen/pair_ap).
1522
+
1523
+ *TBD: Add details regarding pairing and encryption.*
1524
+
1525
+ ### Encryption
1526
+ All channels described here are encrypted using Chacha20Poly1305. The session key is always
1527
+ derived (with HKDF) from the shared secret agreed upon during authentication. Salt and info
1528
+ values vary depending on channel.
1529
+
1530
+ Data is encrypted in blocks, with a two byte (little-endian) size field prepended to it
1531
+ as well as a 16 byte auth tag appended:
1532
+
1533
+ | **Size (2 bytes)** | **Data (n bytes)** | **Auth tag (16 bytes)** |
1534
+
1535
+ HomeKit madates a maximum block size of 1024 bytes. This is not the case here as any block
1536
+ size (that fits length tag) can be used.
1537
+
1538
+ This is also described in the HAP specification, section 5.2.2 (Release R1).
1539
+
1540
+ ## Remote Control
1541
+
1542
+ *NB: This is a WIP. Far from everything is understood yet, so take this part with a pinch
1543
+ of salt.*
1544
+
1545
+ Setting up a remote control session works more or less like setting up a regular audio
1546
+ stream, but it has a different type and the data channel will carry control messages.
1547
+ Below is a sequence diagram outlining the setup flow on a higher level. Three channels
1548
+ are used: control, event and data. You should interpret *Control Sender* as the control
1549
+ channel on the "sender" side, e.g. an iPhone.
1550
+
1551
+ <code class="diagram">
1552
+ sequenceDiagram
1553
+ participant CS as Control Sender
1554
+ participant CR as Control Receiver
1555
+ participant ES as Event Sender
1556
+ participant ER as Event Receiver
1557
+ participant DS as Data Sender
1558
+ participant DR as Data Receiver
1559
+ CS->>CR: SETUP {isRemoteControlOnly: True}
1560
+ CR->>CS: OK {eventPort}
1561
+ CS-->>ES: Start Event Sender
1562
+ ES->>ER: Connect (eventPort)
1563
+ ER-->>ES:
1564
+ CS->>CR: RECORD
1565
+ CR->>CS: OK
1566
+ ER->>ES: System info update
1567
+ CS->>CR: SETUP (stream)
1568
+ CR->>CS: OK {dataPort}
1569
+ CS-->>DS: Start Data Sender
1570
+ DS->>DR: Connect (dataPort)
1571
+ DR-->>DS:
1572
+ note over DS,DR: Exchange messages here
1573
+ loop Every two seconds
1574
+ CS->>CR: POST /feedback
1575
+ CR->>CS: OK
1576
+ end
1577
+ </code>
1578
+
1579
+ ### Control Channel (RTSP)
1580
+ This section describes how a remote control session is set up on the main (control) channel.
1581
+ An event and data channel will be set up in parallel, so be prepared to skip back and forth
1582
+ between chapters a bit to understand what's going on. Keep the sequence diagram above ready
1583
+ at hand.
1584
+
1585
+ The sender starts by setting up a new session, requesting a remote control session via
1586
+ `isRemoteControlOnly`:
1587
+
1588
+ **Sender -> Receiver:**
1589
+ ```raw
1590
+ SETUP rtsp://10.0.10.254/14511846595692938970 RTSP/1.0
1591
+ Content-Length: 376
1592
+ Content-Type: application/x-apple-binary-plist
1593
+ CSeq: 7
1594
+ User-Agent: AirPlay/550.10
1595
+
1596
+ bplist00\xdb\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16_\x10\x13isRemoteControlOnlyVosName]sourceVersion^timingProtocolUmodelXdeviceIDYosVersion^osBuildVersionZmacAddress[sessionUUIDTname\tYiPhone OSV550.10TNoneZiPhone10,6_\x10\x11FF:EE:DD:CC:BB:AAV14.7.1U18G82_\x10\x11AA:BB:CC:DD:EE:FF_\x10$C9646F97-7B3D-46DA-9F92-332ED10EC258^Pierres iPhone\x00\x08\x00\x1f\x005\x00<\x00J\x00Y\x00_\x00h\x00r\x00\x81\x00\x8c\x00\x98\x00\x9d\x00\x9e\x00\xa8\x00\xaf\x00\xb4\x00\xbf\x00\xd3\x00\xda\x00\xe0\x00\xf4\x01\x1b\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01*
1597
+ ```
1598
+
1599
+ The decoded content looks like this:
1600
+
1601
+ ```javascript
1602
+ {'isRemoteControlOnly': True, 'osName': 'iPhone OS', 'sourceVersion': '550.10', 'timingProtocol': 'None', 'model': 'iPhone10,6', 'deviceID': 'FF:EE:DD:CC:BB:AA', 'osVersion': '14.7.1', 'osBuildVersion': '18G82', 'macAddress': 'AA:BB:CC:DD:EE:FF', 'sessionUUID': 'C9646F97-7B3D-46DA-9F92-332ED10EC258', 'name': 'Pierres iPhone'}
1603
+ ```
1604
+
1605
+ **Receiver -> Sender:**
1606
+ ```raw
1607
+ RTSP/1.0 200 OK
1608
+ Date: Fri, 20 Aug 2021 17:09:58 GMT
1609
+ Content-Length: 59
1610
+ Content-Type: application/x-apple-binary-plist
1611
+ Server: AirTunes/550.10
1612
+ CSeq: 7
1613
+
1614
+ bplist00\xd1\x01\x02YeventPort\x11\xc0\xba\x08\x0b\x15\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18'
1615
+ ```
1616
+
1617
+ Which decodes to:
1618
+
1619
+ ```javascript
1620
+ {'eventPort': 49338}
1621
+ ```
1622
+
1623
+ At this stage, the receiver expects the sender to establish a TCP connection to the port specified
1624
+ by `eventPort`. This connection will be used for regular AirPlay events, i.e. RTSP messages
1625
+ are exchanged here. See the next chapter for more details.
1626
+
1627
+ After the sender has connected to the event port, it shall start the stream using `RECORD`
1628
+ (iOS seems to request `/info` before doing this, that's why CSeq 8 is skipped):
1629
+
1630
+ **Sender -> Receiver:**
1631
+ ```raw
1632
+ RECORD rtsp://10.0.10.254/14511846595692938970 RTSP/1.0
1633
+ CSeq: 9
1634
+ User-Agent: AirPlay/550.10
1635
+ ```
1636
+
1637
+ The receiver will respond with:
1638
+
1639
+ **Receiver -> Sender:**
1640
+ ```raw
1641
+ RTSP/1.0 200 OK
1642
+ Date: Thu, 19 Aug 2021 20:17:58 GMT
1643
+ Content-Length: 0
1644
+ Audio-Latency: 0
1645
+ Server: AirTunes/550.10
1646
+ CSeq: 9
1647
+ ```
1648
+
1649
+ Now the receiver will send a "system info update" on the event channel.
1650
+
1651
+ The next step is to set up a channel for the actual remote control messages:
1652
+
1653
+ **Sender -> Receiver:**
1654
+ ```raw
1655
+ SETUP rtsp://10.0.10.254/14511846595692938970 RTSP/1.0
1656
+ Content-Length: 298
1657
+ Content-Type: application/x-apple-binary-plist
1658
+ CSeq: 10
1659
+ User-Agent: AirPlay/550.10
1660
+
1661
+ bplist00\xd1\x01\x02Wstreams\xa1\x03\xd7\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11[controlTypeYchannelIDTseedZclientUUIDTtype_\x10\x14wantsDedicatedSocket^clientTypeUUID\x10\x02_\x10$DA6501B1-1452-4417-AE27-ED8E309DEBCE\x13\xd0_\x18\xd7\x13\xcbh\xd6_\x10$11F965B7-8653-4A25-B82E-D9416C05FE68\x10\x82\t_\x10$1910A70F-DBC0-4242-AF95-115DB30604E1\x08\x0b\x13\x15$0:?JOfuw\x9e\xa7\xce\xd0\xd1\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf8
1662
+ ```
1663
+
1664
+ Which decodes to:
1665
+
1666
+ ```javascript
1667
+ {'streams': [{'controlType': 2, 'channelID': 'DA6501B1-1452-4417-AE27-ED8E309DEBCE', 'seed': -3431997079003895594, 'clientUUID': '11F965B7-8653-4A25-B82E-D9416C05FE68', 'type': 130, 'wantsDedicatedSocket': True, 'clientTypeUUID': '1910A70F-DBC0-4242-AF95-115DB30604E1'}]}
1668
+ ```
1669
+
1670
+ Some things to note here:
1671
+
1672
+ * `channelID` and `clientUUID` change each time, so they are likely randomized
1673
+ * `clientTypeUUID` of `1910A70F-DBC0-4242-AF95-115DB30604E1` means Media Remote (there
1674
+ are a few other values used under other circumstances, like `8186BE43-A39A-4C42-9D0E-60BDB9CE1FE3`).
1675
+ * `type` 130 represents the Remote Control type
1676
+ * `seed` is used when deriving encryption keys for the channel, if encryption is mandated.
1677
+
1678
+ The response looks like this:
1679
+
1680
+ **Receiver -> Sender:**
1681
+ ```raw
1682
+ RTSP/1.0 200 OK
1683
+ Date: Thu, 19 Aug 2021 20:17:58 GMT
1684
+ Content-Length: 100
1685
+ Content-Type: application/x-apple-binary-plist
1686
+ Server: AirTunes/550.10
1687
+ CSeq: 10
1688
+
1689
+ bplist00\xd1\x01\x02Wstreams\xa1\x03\xd3\x04\x05\x06\x07\x08\tTtypeXstreamIDXdataPort\x10\x82\x10\x01\x11\xc0\xae\x08\x0b\x13\x15\x1c!*357\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00:
1690
+ ```
1691
+
1692
+ Which decodes to:
1693
+
1694
+ ```javascript
1695
+ {'streams': [{'type': 130, 'streamID': 1, 'dataPort': 49326}]}
1696
+ ```
1697
+
1698
+ Like with the event channel, the sender is expected to establish a TCP connection to
1699
+ `dataPort` and enable encryption. Remote control messages should from now on be exchanged on the
1700
+ data channel.
1701
+
1702
+ The sender shall continuously send feedback updates to the receiver on the control channel:
1703
+
1704
+ **Sender -> Receiver:**
1705
+ ```raw
1706
+ POST /feedback RTSP/1.0
1707
+ CSeq: 12
1708
+ User-Agent: AirPlay/550.10
1709
+ ```
1710
+
1711
+ **Receiver -> Sender:**
1712
+ ```raw
1713
+ RTSP/1.0 200 OK
1714
+ Date: Thu, 19 Aug 2021 20:18:08 GMT
1715
+ Content-Length: 55
1716
+ Content-Type: application/x-apple-binary-plist
1717
+ Server: AirTunes/550.10
1718
+ CSeq: 12
1719
+
1720
+ bplist00\xd1\x01\x02Wstreams\xa0\x08\x0b\x13\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14
1721
+ ````
1722
+
1723
+ Which decodes to this:
1724
+
1725
+ ```javascript
1726
+ {'streams': []}
1727
+ ```
1728
+
1729
+ iOS sends this every two seconds as a keep-alive.
1730
+
1731
+ **Now it's done!**
1732
+
1733
+ ### Event Channel
1734
+ The event channel is initiated by the sender to the port returned in the response to `SETUP`.
1735
+ After `SETUP` has finished, the sender shall connect to this port (TCP) and enable encryption.
1736
+ The following parameters are used to derive encryption keys:
1737
+
1738
+ | Direction | Salt | Info |
1739
+ | --------- | ---- | ---- |
1740
+ | Output | Events-Salt | Events-Write-Encryption-Key |
1741
+ | Input | Events-Salt | Events-Read-Encryption-Key |
1742
+
1743
+ Even though the channel setup is initiated by the sender, the channel should be treated as
1744
+ originating from the receiver. This means that input and output keys shall be reversed on
1745
+ the sender side (use `Output` as `Input` and `Input` as `Output` to SRP).
1746
+
1747
+ After the stream has been started using `RECORD`, the receiver will send a "system info update",
1748
+ which is basically what is returned when requesting `/info`:
1749
+
1750
+ **Receiver -> Sender:**
1751
+ ```raw
1752
+ POST /command RTSP/1.0
1753
+ CSeq: 0
1754
+ Content-Length: 1386
1755
+ Content-Type: application/x-apple-binary-plist
1756
+
1757
+ bplist00\xd2\x01\x02\x03\x04TtypeUvalueZupdateInfo\xdf\x10\x18\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f&\'#)*+,-.&0123;#=>?@ASpsiRvv_\x10\x14playbackCapabilities_\x10\x15canRecordScreenStream[statusFlags_\x10\x18keepAliveSendStatsAsBodyTname_\x10\x0fprotocolVersion_\x10\x11volumeControlType]senderAddressXdeviceIDRpi^screenDemoMode]initialVolumeZfeaturesExZtxtAirPlay_\x10\x10supportedFormats]sourceVersion_\x10\x16hasUDPMirroringSupportUmodelRpkZmacAddress_\x10\x15receiverHDRCapabilityXfeatures_\x10$6EE2C905-874B-4B4B-A50B-0F06B1800A17\x10\x02\xd3 !"###_\x10\x15supportsInterstitials_\x10\x15supportsFPSSecureStop_\x10\x1dsupportsUIForAudioOnlyContent\t\t\t\x08\x11\x02D\tZVardagsrumS1.1\x10\x04_\x10\x1110.0.10.254:46164_\x10\x11AA:BB:CC:DD:EE:FF_\x10$de7562c4-7bd2-4005-a8e4-d584bf63161a\x08#\xc04\x00\x00\x00\x00\x00\x00[1d9/St5fFTwO\x11\x01\x7f\x05acl=0\x18btaddr=FF:EE:DD:CC:BB:AA\x1adeviceid=AA:BB:CC:DD:EE:FF\x0ffex=1d9/St5fFTw\x1efeatures=0x4A7FDFD5,0x3C155FDE\x0bflags=0x244(gid=4D826039-0F40-4605-AD11-A6516183BAA6\x05igl=1\x06gcgl=1\x10model=AppleTV6,2\rprotovers=1.1\'pi=de7562c4-7bd2-4005-a8e4-d584bf63161a(psi=6EE2C905-874B-4B4B-A50B-0F06B1800A17Cpk=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x0esrcvers=550.10\x0bosvers=14.7\x04vv=2\xd44567899:_\x10\x15lowLatencyAudioStream\\screenStream[audioStream\\bufferStream\x10\x00\x12\x01D\x08\x00\x12\x00\xe0\x00\x00V550.10\tZAppleTV6,2O\x10 \xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa_\x10\x11AA:BB:CC:DD:EE:FFT4k30\x13<\x15_\xdeJ\x7f\xdf\xd5\x00\x08\x00\r\x00\x12\x00\x18\x00#\x00V\x00Z\x00]\x00t\x00\x8c\x00\x98\x00\xb3\x00\xb8\x00\xca\x00\xde\x00\xec\x00\xf5\x00\xf8\x01\x07\x01\x15\x01 \x01+\x01>\x01L\x01e\x01k\x01n\x01y\x01\x91\x01\x9a\x01\xc1\x01\xc3\x01\xca\x01\xe2\x01\xfa\x02\x1a\x02\x1b\x02\x1c\x02\x1d\x02\x1e\x02!\x02"\x02-\x021\x023\x02G\x02[\x02\x82\x02\x83\x02\x8c\x02\x98\x04\x1b\x04$\x04<\x04I\x04U\x04b\x04d\x04i\x04n\x04u\x04v\x04\x81\x04\xa4\x04\xb8\x04\xbd\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x00B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\xc6
1758
+ ```
1759
+
1760
+ Which decodes to this (identifiers replaced with random values):
1761
+
1762
+ ```javascript
1763
+ {'type': 'updateInfo', 'value': {'psi': '6EE2C905-874B-4B4B-A50B-0F06B1800A17', 'vv': 2, 'playbackCapabilities': {'supportsInterstitials': True, 'supportsFPSSecureStop': True, 'supportsUIForAudioOnlyContent': True}, 'canRecordScreenStream': False, 'statusFlags': 580, 'keepAliveSendStatsAsBody': True, 'name': 'Vardagsrum', 'protocolVersion': '1.1', 'volumeControlType': 4, 'senderAddress': '10.0.10.254:46164', 'deviceID': 'AA:BB:CC:DD:EE:FF', 'pi': 'de7562c4-7bd2-4005-a8e4-d584bf63161a', 'screenDemoMode': False, 'initialVolume': -20.0, 'featuresEx': '1d9/St5fFTw', 'txtAirPlay': b"\x05acl=0\x18btaddr=FF:EE:DD:CC:BB:AA\x1adeviceid=AA:BB:CC:DD:EE:FF\x0ffex=1d9/St5fFTw\x1efeatures=0x4A7FDFD5,0x3C155FDE\x0bflags=0x244(gid=4D826039-0F40-4605-AD11-A6516183BAA6\x05igl=1\x06gcgl=1\x10model=AppleTV6,2\rprotovers=1.1'pi=de7562c4-7bd2-4005-a8e4-d584bf63161a(psi=6EE2C905-874B-4B4B-A50B-0F06B1800A17Cpk=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x0esrcvers=550.10\x0bosvers=14.7\x04vv=2", 'supportedFormats': {'lowLatencyAudioStream': 0, 'screenStream': 21235712, 'audioStream': 21235712, 'bufferStream': 14680064}, 'sourceVersion': '550.10', 'hasUDPMirroringSupport': True, 'model': 'AppleTV6,2', 'pk': b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa', 'macAddress': 'AA:BB:CC:DD:EE:FF', 'receiverHDRCapability': '4k30', 'features': 4329472025123872725}}
1764
+ ```
1765
+
1766
+ It is important to send a response to this request, otherwise the connection will time out after
1767
+ 30 seconds and be closed by the receiver:
1768
+
1769
+ ```raw
1770
+ RTSP/1.0 200 OK
1771
+ Content-Length:0
1772
+ Audio-Latency: 0
1773
+ Server: AirTunes/550.10
1774
+ CSeq: 0
1775
+ ```
1776
+
1777
+ No other message has been seen on this channel with regards to remote control support.
1778
+
1779
+ ### Data Channel
1780
+ The data channel carries messages related to the remote control. In reality, it's MRP messages
1781
+ (so protobuf). The same message definitions used by Media Remote Protocol are also valid
1782
+ here.
1783
+
1784
+ #### Encryption
1785
+
1786
+ The following parameters are used to derive encryption keys:
1787
+
1788
+ | Direction | Salt | Info |
1789
+ | --------- | ---- | ---- |
1790
+ | Output | DataStream-Salt*X* | DataStream-Output-Encryption-Key |
1791
+ | Input | DataStream-Salt*X* | DataStream-Input-Encryption-Key |
1792
+
1793
+ Where *X* is `seed` (64 bit) from the response to `SETUP`. It shall always be
1794
+ treated as an unsigned integer (`%llu`), so `-3431997079003895594` would be
1795
+ `15014746994705656022`. The salt value in this case would then be
1796
+ `DataStream-Salt15014746994705656022`.
1797
+
1798
+ #### Message format
1799
+
1800
+ The format of the data sent on the data channel is still unclear, so this is mostly
1801
+ educated guesses. Each message includes a 32 byte header where the first four bytes are
1802
+ the message size. Messages can be segmented over several packets, so it's necessary to
1803
+ put data in a buffer and decode from that. The message format looks like this:
1804
+
1805
+ | Field | Size | Comment |
1806
+ | ----- | ---- | ------- |
1807
+ | Size | 4 byte | Size of message, including size field and all other headers
1808
+ | Message Type | 12 byte | Either `sync` for a request or `rply` for a response. Remaining bytes are padded with zeroes (binary, not the digit 0). Might be padding or other unused fields(?).
1809
+ | Command | 4 byte | Only `comm` and `cmnd` seen so far. If *Message Type* is `rply`, this field is zero.
1810
+ | Sequence number | 8 byte | This is either one or two individual fields, it's not entirely clear. A `rply` message will always contain the same value here as it's corresponding `sync` message. If *Command* is `comm`, then the value is always the same for all requests. Upper four bytes seems to be 1 and the lower four bytes random (generated at session start). If *Command* is `cmnd`, then each request has a random value.
1811
+ | Padding | 4 byte | Seems to always be zeroes, maybe has other purpose?
1812
+ | Payload | *Size* - 32 | Any payload included in the message, always a binary plist(?).
1813
+
1814
+ Some real world examples should make it more clear. Here is one without payload:
1815
+
1816
+ ```raw
1817
+ size=32 sync cmnd sequence number padding
1818
+ 00000020 73796e630000000000000000 636d6e64 cf493446 9b4941ae 00000000
1819
+
1820
+ size=32 rply sequence number padding
1821
+ 00000020 72706c790000000000000000 00000000 cf493446 9b4941ae 00000000
1822
+ ```
1823
+
1824
+ And here's one with payload:
1825
+
1826
+ ```raw
1827
+ size=157 sync comm sequence number padding payload
1828
+ 0000009d 73796e630000000000000000 636f6d6d 00000001 6155c3e0 00000000 62706c6973743030d1010256706172616d73d1030454646174614f103b3a08102000aa010c080110001801200028013000aa052436423031354543352d313941412d344534412d394345442d304439343742383144393635080b12151a0000000000000101000000000000000500000000000000000000000000000058
1829
+
1830
+ size=74 sync sequence number padding payload
1831
+ 0000004a 72706c790000000000000000 00000000 00000001 6155c3e0 00000000 62706c6973743030d0080000000000000101000000000000000100000000000000000000000000000009
1832
+ ```
1833
+
1834
+ If payload is included with a message, it's serialized as a binary property list and has
1835
+ this format (others might exist, but have not been seen yet):
1836
+
1837
+ ```javascript
1838
+ {"params": {"data": xxx}}
1839
+ ```
1840
+
1841
+ Where `xxx` is one or more transported messages. Each message is prepended with the message
1842
+ size encoded as a [varint](https://developers.google.com/protocol-buffers/docs/encoding#varints),
1843
+
1844
+ One example looks like this:
1845
+
1846
+ ```javascript
1847
+ {'params': {'data': b'0\x08& \x00\xd2\x02\x02\x08\x02\xaa\x05$E66952D1-F8F3-4F58-8914-4B507443B321'}}
1848
+ ```
1849
+
1850
+ The first byte (in this case), `0x30` decodes to 48 which happens to be the size of
1851
+ the remaining data. Decoding that data as a protobuf message yields:
1852
+
1853
+ ```raw
1854
+ type: SET_CONNECTION_STATE_MESSAGE
1855
+ errorCode: NoError
1856
+ [setConnectionStateMessage] {
1857
+ state: Connected
1858
+ }
1859
+ uniqueIdentifier: "E66952D1-F8F3-4F58-8914-4B507443B321"
1860
+ ```
1861
+
1862
+ #### General message flow
1863
+
1864
+ From here on, protobuf messages are exchanged in a similar manner to how
1865
+ the MRP protocol works. They are encapsulated as previously described. One
1866
+ thing to note is that the sender sets *Command* to `comm`. The receiver on
1867
+ the other hand sets *Command* to `cmnd`. The reason or importance of this
1868
+ is not yet known.
1869
+
1870
+ As an example, here is the initial `DEVICE_INFO_MESSAGE` message sent by
1871
+ the sender and the answer:
1872
+
1873
+ **Sender -> Receiver:**
1874
+ ```raw
1875
+ Hex:
1876
+ 000001ae73796e630000000000000000
1877
+ 636f6d6d000000016155c3e000000000
1878
+ 62706c6973743030d101025670617261
1879
+ 6d73d1030454646174614f110146c402
1880
+ 080f122430433236323835302d463145
1881
+ 382d344637462d383844462d33463331
1882
+ 39324231413031392000a201ef010a24
1883
+ 39334543443531352d453735422d3442
1884
+ 32332d394237312d3845453730384134
1885
+ 32423132120e50696572726573206950
1886
+ 686f6e651a066950686f6e6522053138
1887
+ 4738322a16636f6d2e6170706c652e6d
1888
+ 6564696172656d6f7465643801406c48
1889
+ 015001620f636f6d2e6170706c652e4d
1890
+ 7573696368017001880103a201116161
1891
+ 3a62623a63633a64643a65653a6666a8
1892
+ 0101b00101c00101e80101f00100fa01
1893
+ 12636f6d2e6170706c652e706f646361
1894
+ 73747382022439444244433031352d32
1895
+ 3038342d343930352d394139442d3234
1896
+ 34333544314345363137a80200b00201
1897
+ ba020a6950686f6e6531302c36aa0524
1898
+ 30334246453834342d353037412d3430
1899
+ 45382d383938362d3633464446383237
1900
+ 393130330008000b00120015001a0000
1901
+ 00000000020100000000000000050000
1902
+ 0000000000000000000000000164
1903
+
1904
+ Binary:
1905
+ \x00\x00\x01\xaesync\x00\x00\x00\x00\x00\x00\x00\x00comm\x00\x00\x00\x01aU\xc3\xe0\x00\x00\x00\x00bplist00\xd1\x01\x02Vparams\xd1\x03\x04TdataO\x11\x01F\xc4\x02\x08\x0f\x12$0C262850-F1E8-4F7F-88DF-3F3192B1A019 \x00\xa2\x01\xef\x01\n$93ECD515-E75B-4B23-9B71-8EE708A42B12\x12\x0ePierres iPhone\x1a\x06iPhone"\x0518G82*\x16com.apple.mediaremoted8\x01@lH\x01P\x01b\x0fcom.apple.Musich\x01p\x01\x88\x01\x03\xa2\x01\x11aa:bb:cc:dd:ee:ff\xa8\x01\x01\xb0\x01\x01\xc0\x01\x01\xe8\x01\x01\xf0\x01\x00\xfa\x01\x12com.apple.podcasts\x82\x02$9DBDC015-2084-4905-9A9D-24435D1CE617\xa8\x02\x00\xb0\x02\x01\xba\x02\niPhone10,6\xaa\x05$03BFE844-507A-40E8-8986-63FDF8279103\x00\x08\x00\x0b\x00\x12\x00\x15\x00\x1a\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01d
1906
+ ```
1907
+
1908
+ Which decodes to:
1909
+
1910
+ ```raw
1911
+ type: DEVICE_INFO_MESSAGE
1912
+ identifier: "0C262850-F1E8-4F7F-88DF-3F3192B1A019"
1913
+ errorCode: NoError
1914
+ [deviceInfoMessage] {
1915
+ uniqueIdentifier: "93ECD515-E75B-4B23-9B71-8EE708A42B12"
1916
+ name: "Pierres iPhone"
1917
+ localizedModelName: "iPhone"
1918
+ systemBuildVersion: "18G82"
1919
+ applicationBundleIdentifier: "com.apple.mediaremoted"
1920
+ protocolVersion: 1
1921
+ lastSupportedMessageType: 108
1922
+ supportsSystemPairing: true
1923
+ allowsPairing: true
1924
+ systemMediaApplication: "com.apple.Music"
1925
+ supportsACL: true
1926
+ supportsSharedQueue: true
1927
+ sharedQueueVersion: 3
1928
+ managedConfigDeviceID: "aa:bb:cc:dd:ee:ff"
1929
+ deviceClass: iPhone
1930
+ logicalDeviceCount: 1
1931
+ isProxyGroupPlayer: true
1932
+ isGroupLeader: true
1933
+ isAirplayActive: false
1934
+ systemPodcastApplication: "com.apple.podcasts"
1935
+ enderDefaultGroupUID: "9DBDC015-2084-4905-9A9D-24435D1CE617"
1936
+ clusterType: 0
1937
+ isClusterAware: true
1938
+ modelID: "iPhone10,6"
1939
+ }
1940
+ uniqueIdentifier: "03BFE844-507A-40E8-8986-63FDF8279103"
1941
+ ```
1942
+
1943
+ **Receiver -> Sender:**
1944
+ ```raw
1945
+ Hex:
1946
+ 0000004a72706c790000000000000000
1947
+ 00000000000000016155c3e000000000
1948
+ 62706c6973743030d008000000000000
1949
+ 01010000000000000001000000000000
1950
+ 00000000000000000009
1951
+
1952
+ Bytes:
1953
+ \x00\x00\x00Jrply\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01aU\xc3\xe0\x00\x00\x00\x00bplist00\xd0\x08\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t
1954
+ ```
1955
+
1956
+ Which decodes to an empty dict (`{}`).
1957
+
1958
+ It is important to include `uniqueIdentifier` in the "envelope message" (`ProtocolMessage`) as
1959
+ the device doesn't seem to respond otherwise. It shall be set to a random UUID4 string.