@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.
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -9
- package/dist/index.js.map +1 -1
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +96 -11
- package/dist/mdns.js.map +1 -1
- package/examples/print-device-json.js +22 -0
- package/package.json +2 -3
- package/pyatv/.codecov.yml +38 -0
- package/pyatv/.github/FUNDING.yml +3 -0
- package/pyatv/.github/ISSUE_TEMPLATE/bug_report.yml +80 -0
- package/pyatv/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/pyatv/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
- package/pyatv/.github/ISSUE_TEMPLATE/implementation-proposal.yml +29 -0
- package/pyatv/.github/ISSUE_TEMPLATE/investigation.yml +16 -0
- package/pyatv/.github/ISSUE_TEMPLATE/minor-change.yml +10 -0
- package/pyatv/.github/ISSUE_TEMPLATE/question-or-idea.yml +11 -0
- package/pyatv/.github/dependabot.yml +26 -0
- package/pyatv/.github/workflows/codeql-analysis.yml +71 -0
- package/pyatv/.github/workflows/release.yml +160 -0
- package/pyatv/.github/workflows/tests.yml +104 -0
- package/pyatv/.gitpod.yml +23 -0
- package/pyatv/CHANGES.md +3708 -0
- package/pyatv/CODE_OF_CONDUCT.md +76 -0
- package/pyatv/CONTRIBUTING.md +72 -0
- package/pyatv/CONTRIBUTORS.md +3 -0
- package/pyatv/Dockerfile +15 -0
- package/pyatv/LICENSE.md +9 -0
- package/pyatv/MANIFEST.in +14 -0
- package/pyatv/README.md +111 -0
- package/pyatv/base_versions.txt +13 -0
- package/pyatv/chickn.yaml +75 -0
- package/pyatv/docs/404.html +24 -0
- package/pyatv/docs/CNAME +1 -0
- package/pyatv/docs/Gemfile +31 -0
- package/pyatv/docs/_config.yml +121 -0
- package/pyatv/docs/_includes/api +10 -0
- package/pyatv/docs/_includes/atvremote_scan +32 -0
- package/pyatv/docs/_includes/code +6 -0
- package/pyatv/docs/_includes/issue +14 -0
- package/pyatv/docs/_includes/pypi +5 -0
- package/pyatv/docs/_layouts/template.html +109 -0
- package/pyatv/docs/api/pyatv.conf.html +312 -0
- package/pyatv/docs/api/pyatv.const.html +974 -0
- package/pyatv/docs/api/pyatv.convert.html +106 -0
- package/pyatv/docs/api/pyatv.exceptions.html +489 -0
- package/pyatv/docs/api/pyatv.helpers.html +102 -0
- package/pyatv/docs/api/pyatv.html +120 -0
- package/pyatv/docs/api/pyatv.interface.html +2369 -0
- package/pyatv/docs/api/pyatv.settings.html +484 -0
- package/pyatv/docs/api/pyatv.storage.file_storage.html +102 -0
- package/pyatv/docs/api/pyatv.storage.html +186 -0
- package/pyatv/docs/api/pyatv.storage.memory_storage.html +83 -0
- package/pyatv/docs/assets/css/custom.css +19 -0
- package/pyatv/docs/assets/css/hljs.css +1 -0
- package/pyatv/docs/assets/css/normalize.css +349 -0
- package/pyatv/docs/assets/css/pdoc.css +287 -0
- package/pyatv/docs/assets/css/sanitize.css +566 -0
- package/pyatv/docs/assets/css/style.scss +9 -0
- package/pyatv/docs/assets/img/logo.svg +63 -0
- package/pyatv/docs/assets/js/highlight.9.12.0.min.js +3 -0
- package/pyatv/docs/assets/js/mermaid.8.9.2.min.js +32 -0
- package/pyatv/docs/assets/js/mermaid.min.js.map +1 -0
- package/pyatv/docs/development/apps.md +81 -0
- package/pyatv/docs/development/audio.md +42 -0
- package/pyatv/docs/development/control.md +56 -0
- package/pyatv/docs/development/development.md +15 -0
- package/pyatv/docs/development/device_info.md +36 -0
- package/pyatv/docs/development/examples.md +44 -0
- package/pyatv/docs/development/features.md +70 -0
- package/pyatv/docs/development/keyboard.md +51 -0
- package/pyatv/docs/development/listeners.md +144 -0
- package/pyatv/docs/development/logging.md +55 -0
- package/pyatv/docs/development/metadata.md +115 -0
- package/pyatv/docs/development/power_management.md +53 -0
- package/pyatv/docs/development/scan_pair_and_connect.md +331 -0
- package/pyatv/docs/development/services.md +9 -0
- package/pyatv/docs/development/storage.md +259 -0
- package/pyatv/docs/development/stream.md +241 -0
- package/pyatv/docs/development/testing.md +9 -0
- package/pyatv/docs/documentation/atvlog.md +64 -0
- package/pyatv/docs/documentation/atvproxy.md +244 -0
- package/pyatv/docs/documentation/atvremote.md +639 -0
- package/pyatv/docs/documentation/atvscript.md +275 -0
- package/pyatv/docs/documentation/concepts.md +168 -0
- package/pyatv/docs/documentation/documentation.md +130 -0
- package/pyatv/docs/documentation/getting_started.md +248 -0
- package/pyatv/docs/documentation/protocols.md +1959 -0
- package/pyatv/docs/documentation/supported_features.md +246 -0
- package/pyatv/docs/documentation/tutorial.md +1062 -0
- package/pyatv/docs/documentation/workspace.code-workspace +7 -0
- package/pyatv/docs/favicon.ico +0 -0
- package/pyatv/docs/index.md +109 -0
- package/pyatv/docs/internals/design.md +354 -0
- package/pyatv/docs/internals/documentation.md +84 -0
- package/pyatv/docs/internals/interfaces.md +95 -0
- package/pyatv/docs/internals/internals.md +157 -0
- package/pyatv/docs/internals/submit_pr.md +56 -0
- package/pyatv/docs/internals/testing.md +176 -0
- package/pyatv/docs/internals/tools.md +574 -0
- package/pyatv/docs/pdoc_templates/config.mako +46 -0
- package/pyatv/docs/pdoc_templates/html.mako +454 -0
- package/pyatv/docs/support/acknowledgements.md +87 -0
- package/pyatv/docs/support/faq.md +214 -0
- package/pyatv/docs/support/migration.md +138 -0
- package/pyatv/docs/support/scanning_issues.md +110 -0
- package/pyatv/docs/support/support.md +18 -0
- package/pyatv/docs/support/troubleshooting.md +83 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeMessage.proto +13 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeMessage_pb2.pyi +37 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeResponseMessage.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/AudioFadeResponseMessage_pb2.pyi +32 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/AudioFormatSettingsMessage.proto +5 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/AudioFormatSettingsMessage_pb2.pyi +27 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ClientUpdatesConfigMessage.proto +16 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ClientUpdatesConfigMessage_pb2.pyi +44 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/CommandInfo.proto +117 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/CommandInfo_pb2.pyi +325 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/CommandOptions.proto +36 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/CommandOptions_pb2.pyi +115 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/Common.proto +79 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/Common_pb2.pyi +228 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ConfigureConnectionMessage.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ConfigureConnectionMessage_pb2.pyi +32 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ContentItem.proto +27 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ContentItemMetadata.proto +213 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ContentItemMetadata_pb2.pyi +630 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ContentItem_pb2.pyi +94 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/CryptoPairingMessage.proto +15 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/CryptoPairingMessage_pb2.pyi +46 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/DeviceInfoMessage.proto +69 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/DeviceInfoMessage_pb2.pyi +226 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/GenericMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/GenericMessage_pb2.pyi +35 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/GetKeyboardSessionMessage.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/GetKeyboardSessionMessage_pb2.pyi +26 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/GetRemoteTextInputSessionMessage.proto +10 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/GetRemoteTextInputSessionMessage_pb2.pyi +26 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeMessage.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeMessage_pb2.pyi +32 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeResultMessage.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/GetVolumeResultMessage_pb2.pyi +32 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/KeyboardMessage.proto +88 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/KeyboardMessage_pb2.pyi +261 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/LanguageOption.proto +9 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/LanguageOption_pb2.pyi +42 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ModifyOutputContextRequestMessage.proto +23 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ModifyOutputContextRequestMessage_pb2.pyi +86 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/NotificationMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/NotificationMessage_pb2.pyi +38 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingClient.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingClient_pb2.pyi +49 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingInfo.proto +24 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingInfo_pb2.pyi +79 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingPlayer.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/NowPlayingPlayer_pb2.pyi +45 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/Origin.proto +17 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/OriginClientPropertiesMessage.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/OriginClientPropertiesMessage_pb2.pyi +32 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/Origin_pb2.pyi +63 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueue.proto +15 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueCapabilities.proto +7 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueCapabilities_pb2.pyi +33 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueContext.proto +5 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueContext_pb2.pyi +27 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueRequestMessage.proto +29 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueueRequestMessage_pb2.pyi +87 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlaybackQueue_pb2.pyi +53 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlayerClientPropertiesMessage.proto +13 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlayerClientPropertiesMessage_pb2.pyi +37 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlayerPath.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/PlayerPath_pb2.pyi +39 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ProtocolMessage.proto +171 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/ProtocolMessage_pb2.pyi +377 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RegisterForGameControllerEventsMessage.proto +18 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RegisterForGameControllerEventsMessage_pb2.pyi +54 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceMessage_pb2.pyi +34 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceResultMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RegisterHIDDeviceResultMessage_pb2.pyi +35 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceMessage_pb2.pyi +34 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceResponseMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RegisterVoiceInputDeviceResponseMessage_pb2.pyi +35 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RemoteTextInputMessage.proto +13 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RemoteTextInputMessage_pb2.pyi +38 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RemoveClientMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RemoveClientMessage_pb2.pyi +34 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RemoveEndpointsMessage.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RemoveEndpointsMessage_pb2.pyi +34 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RemoveOutputDevicesMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RemoveOutputDevicesMessage_pb2.pyi +38 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RemovePlayerMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/RemovePlayerMessage_pb2.pyi +34 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendButtonEventMessage.proto +13 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendButtonEventMessage_pb2.pyi +38 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandMessage.proto +16 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandMessage_pb2.pyi +43 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandResultMessage.proto +100 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendCommandResultMessage_pb2.pyi +286 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendHIDEventMessage.proto +41 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendHIDEventMessage_pb2.pyi +63 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendPackedVirtualTouchEventMessage.proto +24 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendPackedVirtualTouchEventMessage_pb2.pyi +64 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendVoiceInputMessage.proto +38 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SendVoiceInputMessage_pb2.pyi +134 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetArtworkMessage.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetArtworkMessage_pb2.pyi +32 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetConnectionStateMessage.proto +18 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetConnectionStateMessage_pb2.pyi +54 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetDefaultSupportedCommandsMessage.proto +28 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetDefaultSupportedCommandsMessage_pb2.pyi +74 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetDiscoveryModeMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetDiscoveryModeMessage_pb2.pyi +35 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetHiliteModeMessage.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetHiliteModeMessage_pb2.pyi +32 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingClientMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingClientMessage_pb2.pyi +34 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingPlayerMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetNowPlayingPlayerMessage_pb2.pyi +34 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetRecordingStateMessage.proto +17 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetRecordingStateMessage_pb2.pyi +54 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetStateMessage.proto +27 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetStateMessage_pb2.pyi +72 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetVolumeMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SetVolumeMessage_pb2.pyi +35 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SupportedCommands.proto +7 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/SupportedCommands_pb2.pyi +30 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/TextInputMessage.proto +23 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/TextInputMessage_pb2.pyi +76 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/TransactionKey.proto +6 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/TransactionKey_pb2.pyi +30 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/TransactionMessage.proto +15 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/TransactionMessage_pb2.pyi +42 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPacket.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPacket_pb2.pyi +41 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPackets.proto +7 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/TransactionPackets_pb2.pyi +30 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdateClientMessage.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdateClientMessage_pb2.pyi +34 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemArtworkMessage.proto +14 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemArtworkMessage_pb2.pyi +41 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemMessage.proto +14 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdateContentItemMessage_pb2.pyi +41 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdateEndPointsMessage.proto +25 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdateEndPointsMessage_pb2.pyi +74 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdateOutputDeviceMessage.proto +88 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdateOutputDeviceMessage_pb2.pyi +277 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdatePlayerPath.proto +12 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/UpdatePlayerPath_pb2.pyi +34 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/VirtualTouchDeviceDescriptorMessage.proto +8 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/VirtualTouchDeviceDescriptorMessage_pb2.pyi +36 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/VoiceInputDeviceDescriptorMessage.proto +8 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/VoiceInputDeviceDescriptorMessage_pb2.pyi +35 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlAvailabilityMessage.proto +23 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlAvailabilityMessage_pb2.pyi +71 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlCapabilitiesDidChangeMessage.proto +14 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/VolumeControlCapabilitiesDidChangeMessage_pb2.pyi +40 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/VolumeDidChangeMessage.proto +13 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/VolumeDidChangeMessage_pb2.pyi +38 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/WakeDeviceMessage.proto +11 -0
- package/pyatv/pyatv/protocols/mrp/protobuf/WakeDeviceMessage_pb2.pyi +26 -0
- package/pyatv/pyatv/py.typed +0 -0
- package/pyatv/pylintrc +49 -0
- package/pyatv/pyproject.toml +74 -0
- package/pyatv/requirements/requirements.txt +14 -0
- package/pyatv/requirements/requirements_docs.txt +2 -0
- package/pyatv/requirements/requirements_test.txt +20 -0
- package/pyatv/scripts/build_docs.sh +17 -0
- package/pyatv/scripts/setup_dev_env.sh +83 -0
- package/pyatv/setup.cfg +14 -0
- package/pyatv/tests/data/README +23 -0
- package/pyatv/tests/data/audio_10_frames.wav +0 -0
- package/pyatv/tests/data/audio_1_packet_metadata.wav +0 -0
- package/pyatv/tests/data/audio_3_packets.wav +0 -0
- package/pyatv/tests/data/only_metadata.wav +0 -0
- package/pyatv/tests/data/only_title.wav +0 -0
- package/pyatv/tests/data/static_3sec.ogg +0 -0
- package/pyatv/tests/data/testfile.txt +1 -0
- package/pyatv/tests/support/pyatv.code-workspace +14 -0
- package/src/index.ts +122 -8
- 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.
|