@ciphore/radiocli 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/CONTRIBUTING.md +28 -0
  3. package/README.md +108 -253
  4. package/SECURITY.md +23 -1
  5. package/dist/agent/alarm-service.js +10 -7
  6. package/dist/agent/cli.js +16 -1
  7. package/dist/agent/command-schema.js +47 -0
  8. package/dist/agent/headless-host.js +226 -81
  9. package/dist/agent/launcher.js +2 -1
  10. package/dist/agent/mcp-server.js +10 -5
  11. package/dist/agent/service.js +75 -40
  12. package/dist/agent/session.js +214 -24
  13. package/dist/agent/tui-session.js +213 -0
  14. package/dist/alarms/cli.js +19 -3
  15. package/dist/alarms/occurrence-cache.js +25 -0
  16. package/dist/alarms/runner.js +87 -23
  17. package/dist/alarms/schedule.js +29 -1
  18. package/dist/alarms/scheduler.js +11 -3
  19. package/dist/alarms/system-volume-linux.js +160 -0
  20. package/dist/alarms/system-volume-ownership.js +76 -8
  21. package/dist/alarms/system-volume.js +44 -23
  22. package/dist/cli/arguments.js +212 -0
  23. package/dist/cli/completion.js +44 -0
  24. package/dist/cli/doctor.js +156 -0
  25. package/dist/cli/interactive.js +79 -0
  26. package/dist/cli/listener.js +227 -0
  27. package/dist/cli/output.js +121 -0
  28. package/dist/cli/render-tui.js +34 -0
  29. package/dist/cli.js +182 -199
  30. package/dist/platform/capabilities.js +8 -1
  31. package/dist/platform/desktop-media.js +61 -0
  32. package/dist/platform/diagnostics.js +273 -0
  33. package/dist/platform/json-worker.js +111 -0
  34. package/dist/platform/launch-command.js +9 -6
  35. package/dist/platform/mpris.js +356 -0
  36. package/dist/platform/network.js +69 -1
  37. package/dist/platform/omarchy.js +147 -0
  38. package/dist/platform/packages.js +7 -1
  39. package/dist/platform/runtime.js +1 -1
  40. package/dist/platform/terminals.js +24 -4
  41. package/dist/player/airplay-discovery.js +144 -61
  42. package/dist/player/airplay-policy.js +15 -0
  43. package/dist/player/airplay-process.js +63 -0
  44. package/dist/player/airplay-readiness.js +42 -0
  45. package/dist/player/airplay-sender-health.js +49 -49
  46. package/dist/player/airplay-sender-package.js +5 -0
  47. package/dist/player/airplay-sender-patch.js +102 -4
  48. package/dist/player/airplay-setup.js +51 -0
  49. package/dist/player/airplay-worker.js +97 -41
  50. package/dist/player/backend-install.js +9 -6
  51. package/dist/player/ffmpeg-input.js +37 -0
  52. package/dist/player/mpv-desktop-media.js +194 -0
  53. package/dist/player/mpv-ipc-client.js +51 -2
  54. package/dist/player/player-controller.js +390 -73
  55. package/dist/player/radio-engine.js +282 -0
  56. package/dist/playlists/playlist.js +18 -16
  57. package/dist/providers/cache.js +291 -75
  58. package/dist/providers/discovery-session.js +111 -0
  59. package/dist/providers/provider-manager.js +106 -28
  60. package/dist/providers/radio-browser.js +272 -41
  61. package/dist/providers/radio-garden.js +4 -2
  62. package/dist/setup-playback.js +152 -0
  63. package/dist/setup.js +88 -21
  64. package/dist/storage/file-lock.js +130 -0
  65. package/dist/storage/read-settings.js +33 -0
  66. package/dist/storage/replace-file.js +20 -0
  67. package/dist/storage/repository.js +247 -0
  68. package/dist/storage/station-identity.js +21 -0
  69. package/dist/storage/station-schema.js +27 -0
  70. package/dist/storage/store.js +314 -76
  71. package/dist/types.js +22 -1
  72. package/dist/ui/AdaptiveContent.js +21 -24
  73. package/dist/ui/App.js +708 -454
  74. package/dist/ui/AppContent.js +5 -5
  75. package/dist/ui/airplay-settings.js +12 -5
  76. package/dist/ui/bidi-ranges.js +45 -0
  77. package/dist/ui/components/AdaptiveMarquee.js +16 -9
  78. package/dist/ui/components/ReceiverVisualRow.js +39 -0
  79. package/dist/ui/format.js +37 -4
  80. package/dist/ui/help-content.js +5 -2
  81. package/dist/ui/rtl-output.js +137 -0
  82. package/dist/ui/screen-items.js +10 -4
  83. package/dist/ui/screens/AirPlaySettingsScreen.js +3 -3
  84. package/dist/ui/screens/AlarmsScreen.js +5 -2
  85. package/dist/ui/screens/HelpScreen.js +3 -3
  86. package/dist/ui/screens/NowPlayingScreen.js +2 -21
  87. package/dist/ui/screens/SettingsScreen.js +16 -16
  88. package/dist/ui/screens/StatsScreen.js +1 -1
  89. package/dist/ui/text-direction.js +9 -0
  90. package/dist/ui/theme.js +46 -0
  91. package/dist/ui/use-alarm-tui.js +24 -24
  92. package/dist/ui/use-app-input.js +42 -8
  93. package/dist/ui/use-command-executor.js +21 -14
  94. package/dist/ui/visualizers/boat-lake-kernel.js +325 -0
  95. package/dist/ui/visualizers/boat-lake.js +161 -0
  96. package/dist/ui/visualizers/gallop.js +2 -2
  97. package/dist/ui/visualizers/receiver-style-registry.js +3 -1
  98. package/dist/ui/visualizers/receiver-visualizers.js +6 -108
  99. package/dist/ui/visualizers/sumi-ocean.js +183 -0
  100. package/dist/ui/visualizers/terminal-cell.js +45 -0
  101. package/dist/update-check.js +2 -1
  102. package/docs/THIRD_PARTY_NOTICES.md +6 -4
  103. package/docs/boat-lake.md +88 -0
  104. package/docs/sumi-ocean.md +80 -0
  105. package/package.json +36 -12
  106. package/scripts/build-airplay-native.mjs +22 -0
  107. package/vendor/node-airtunes2/LICENSE +208 -0
  108. package/vendor/node-airtunes2/RADIOCLI_PATCH.md +29 -0
  109. package/vendor/node-airtunes2/README.md +225 -0
  110. package/vendor/node-airtunes2/UPSTREAM.json +50 -0
  111. package/vendor/node-airtunes2/binding.gyp +41 -0
  112. package/vendor/node-airtunes2/lib/ap2_test.js +4 -0
  113. package/vendor/node-airtunes2/lib/atv.js +307 -0
  114. package/vendor/node-airtunes2/lib/atvAuthenticator.js +136 -0
  115. package/vendor/node-airtunes2/lib/audio_out.js +69 -0
  116. package/vendor/node-airtunes2/lib/circular_buffer.js +133 -0
  117. package/vendor/node-airtunes2/lib/config.js +26 -0
  118. package/vendor/node-airtunes2/lib/device_airtunes.js +532 -0
  119. package/vendor/node-airtunes2/lib/devices.js +266 -0
  120. package/vendor/node-airtunes2/lib/ed25519.js +26 -0
  121. package/vendor/node-airtunes2/lib/homekit/credentials.js +86 -0
  122. package/vendor/node-airtunes2/lib/homekit/encryption.js +69 -0
  123. package/vendor/node-airtunes2/lib/homekit/number.js +44 -0
  124. package/vendor/node-airtunes2/lib/homekit/tlv.js +95 -0
  125. package/vendor/node-airtunes2/lib/http.js +189 -0
  126. package/vendor/node-airtunes2/lib/index.d.ts +141 -0
  127. package/vendor/node-airtunes2/lib/index.js +127 -0
  128. package/vendor/node-airtunes2/lib/ntp.js +37 -0
  129. package/vendor/node-airtunes2/lib/num_util.js +21 -0
  130. package/vendor/node-airtunes2/lib/packet_pool.js +43 -0
  131. package/vendor/node-airtunes2/lib/rtsp.js +1807 -0
  132. package/vendor/node-airtunes2/lib/srp.js +158 -0
  133. package/vendor/node-airtunes2/lib/udp_servers.js +214 -0
  134. package/vendor/node-airtunes2/lib/util.js +12 -0
  135. package/vendor/node-airtunes2/package.json +31 -0
  136. package/vendor/node-airtunes2/src/CADebugMacros.h +257 -0
  137. package/vendor/node-airtunes2/src/CAHostTimeBase.cpp +105 -0
  138. package/vendor/node-airtunes2/src/CAHostTimeBase.h +224 -0
  139. package/vendor/node-airtunes2/src/aes_utils.c +864 -0
  140. package/vendor/node-airtunes2/src/aes_utils.h +24 -0
  141. package/vendor/node-airtunes2/src/base64.c +136 -0
  142. package/vendor/node-airtunes2/src/base64.h +42 -0
  143. package/vendor/node-airtunes2/src/bindings.cc +29 -0
  144. package/vendor/node-airtunes2/src/codec.cc +170 -0
  145. package/vendor/node-airtunes2/src/coreaudio.cc +332 -0
@@ -0,0 +1,50 @@
1
+ {
2
+ "url": "https://registry.npmjs.org/node-airtunes2/-/node-airtunes2-2.5.0.tgz",
3
+ "integrity": "sha512-OMbxDCTwakRwTEZ/PKA6HJOOHD1FiqYnw98RtGx/X3lFsNXyKPUMLyR43t9lQArRCYGsGgLuPwDv9Ehkw4mhHQ==",
4
+ "sha256": "2cee1d86fcd2e5fb4f56f12e23fd1abbdbd0b71a9bc5e1ffe1dcbb885cec1d71",
5
+ "unmodifiedFiles": {
6
+ ".editorconfig": "4e40c0a34e2c5d50eeaeb682bb5fd3f72f79e5cbd744729d309bf3bc5f1a7742",
7
+ ".nvmrc": "f14b4987904bcb5814e4459a057ed4d20f58a633152288a761214dcd28780b56",
8
+ "LICENSE": "2c0434c24a2fd80ef3c0dcca3c12a7618f3b0194b6bf9dc545695be86dc6c8e0",
9
+ "README.md": "230de9b984355778dcd8524725abd408bd3981ff6d419372135e6eab9a765a00",
10
+ "binding.gyp": "53cb9eb3fb6ccf3d35f0db61446dbe549d38a592644ba671c25b247a047f2a38",
11
+ "eslint.config.mjs": "b737b0a39d662188d2183fd06588821a996ddd920bce57d7fed758d52a6dd2e2",
12
+ "example_execlient.js": "950d83e8069032fbec39ddc6cd5d1ac8a5241dd5742e44b797f46d40fe95d3d0",
13
+ "lib/ap2_test.js": "9d902c2ccf7ec6ba45800ddf2db46fe5e8c2ad959d60aed2feb151c6fe4f6804",
14
+ "lib/atv.js": "a46235ad8b4b02a4d3b8abca64eba6d36af00e1b7d66f3669f34c65efa2f5ddd",
15
+ "lib/audio_out.js": "0481f988ec3e76ce33a5fe771eeb20107183850e22310325a7cb4799b72f8cc6",
16
+ "lib/circular_buffer.js": "e5521fc371be58bf6aa61b0bbdb906a48da4fd90c012e6db45ce4979a85c7719",
17
+ "lib/config.js": "18a2e5bcc6c41804945c28af3c14ed3a306e19c48aad4f4624ff30a7c9e14e49",
18
+ "lib/device_airtunes.js": "4782a671f6fa0924e8a48119a69b6ff93764d2265aedb6e293f284c773261a9a",
19
+ "lib/devices.js": "90b40223384c81f5635bad7b27bbe0d2061a2653c70053aed53ba6ecb4be6852",
20
+ "lib/homekit/credentials.js": "aa74c977d9831f064164ef30a8a12fd2a9eb06ce4d787fc28d8a2c57bfc2c1a7",
21
+ "lib/homekit/encryption.js": "e1ce0ef009309797d91b70d5f27f8234ef9fda1bb325ecb78a6fd296d87e03db",
22
+ "lib/homekit/number.js": "fd34215e79e61f75fefab06188ff3041982b933d4c406c7638ccb90630f1c6cf",
23
+ "lib/homekit/tlv.js": "447d1544d6befbe2281d50027a419cdecf4c2c9ee9e226ba216161046545e678",
24
+ "lib/http.js": "c7c32939c30e214c6e64de5f63eb0f3764122e988a76d85e606b72d86492effa",
25
+ "lib/index.d.ts": "365cc6d2375896b8141c1f68b76d3e0940ede807f34b3dcbd428e4f45674d674",
26
+ "lib/index.js": "b1c112d4e7b8e378b9ec9fa21cf4b5147cd773f887c99062f54a6a2afb410e63",
27
+ "lib/ntp.js": "208491f53fb4dc9e872eec1953d13aebf7f3c6d042a32020faf402a92008a8df",
28
+ "lib/num_util.js": "6a9585f05cd0fdb709dfb7eb63d554de377831e8f37425cd01028f8fa1d85f13",
29
+ "lib/packet_pool.js": "aef0dc8c9fe4be88a212134b2703f3e6551454e991c8e04d869f3a2cddbb2960",
30
+ "lib/rtsp.js": "99f5fa32e4cb29817ee57d79dd103f8f3bee34217a8032e92df3b5c8b3daa3ca",
31
+ "lib/srp.js": "878ca193826b216edaff1f428cd7645d85bcc0d2a8c10de4e5145454a0f0d631",
32
+ "lib/udp_servers.js": "fcd41ba9ba2b25db8aa74cdb3606f8a8f0201c99cb6c26bbc30a344ade36380d",
33
+ "lib/util.js": "6aeffbe069d052157079bc4fd508ab616de80e08b219dff2a6f30ac4c8303c65",
34
+ "src/CADebugMacros.h": "bc139e5ceb89790191b71ce61038f532f755707608f123a8f4783eb6f98e6872",
35
+ "src/CAHostTimeBase.cpp": "e3db4379731e39ae6d5699aa9625dcf286cfc9ffacafa769c87b746d663186ce",
36
+ "src/CAHostTimeBase.h": "27d74cbf90e7b4e74f44b5dda11b18ebed962a77cc530c6a4eb4456cdad0fda2",
37
+ "src/aes_utils.c": "2d72155b2d9319abc1d4db695fdabfbbdbbaad157286df75a0500653352053ad",
38
+ "src/aes_utils.h": "500146ef43ea263263e15e39c91330deb979db13c85bb0c809f2c1b7243f08f0",
39
+ "src/base64.c": "79d33c06704f52ce09062deb4c9659a98f826db43625f5f1840a1d8250f16d9d",
40
+ "src/base64.h": "ed283d2c2b5e6593d683a97621c8a7994e590529ec6576811da39eaa9ee02146",
41
+ "src/bindings.cc": "38da78b95543422006a1d7c87118ee966cc970b2c353d24d751c43d0d44e0090",
42
+ "src/codec.cc": "ba2f0ab6071e09410daed8aa6ee4fcec20ed23c627b4b46cd7078ee70b74f109",
43
+ "src/coreaudio.cc": "487fcf7e7430be3faf30a9338080cc9147014c1c64c7e6debd3bede117e2a615"
44
+ },
45
+ "excludedFiles": [
46
+ "build/",
47
+ "SECURITY.md",
48
+ "exe_api.md"
49
+ ]
50
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ 'targets': [
3
+ {
4
+ 'target_name': 'airtunes',
5
+ 'sources': [
6
+ 'src/codec.cc', 'src/bindings.cc',
7
+ 'src/aes_utils.c', 'src/base64.c',
8
+ ],
9
+ 'conditions': [
10
+ ['OS=="mac"', {
11
+ 'include_dirs':
12
+ ['/System/Library/Frameworks/Kernel.framework/Versions/A/Headers/sys'
13
+ ],
14
+
15
+ 'sources': [
16
+ ],
17
+ 'xcode_settings': {
18
+ 'CLANG_CXX_LANGUAGE_STANDARD': 'c++20'
19
+ }
20
+ }],
21
+ ['OS=="linux"', {
22
+ 'sources': [
23
+ ],
24
+ 'cflags_cc': [
25
+ "-std=c++20"
26
+ ]
27
+ }],
28
+ ['OS=="win"', {
29
+ 'include_dirs': ['C:\\Program Files\\OpenSSL-Win64\\include\\'],
30
+ "msbuild_settings": {
31
+ "ClCompile": {
32
+ "LanguageStandard": "stdcpp20"
33
+ }
34
+ },
35
+ 'sources': [
36
+ ],
37
+ }],
38
+ ]
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,4 @@
1
+ const number = require('./homekit/number').default
2
+
3
+ const u = number.UInt16toBufferBE(287)
4
+ console.log(Buffer.concat([u.slice(1), u.slice(0, 1)]))
@@ -0,0 +1,307 @@
1
+ const crypto = require('crypto')
2
+ const fs = require('fs')
3
+ const bplistCreator = require('bplist-creator')
4
+ const bplistParser = require('bplist-parser')
5
+ const SRP = require('./srp')
6
+ const ATVAuthenticator = require('./atvAuthenticator')
7
+ const httpClientFactory = require('./http')
8
+
9
+ // ...
10
+ // Configuration.
11
+ const loadConfig = configFilePath => !fs.existsSync(configFilePath) ? null : JSON.parse(fs.readFileSync(configFilePath))
12
+ const saveConfig = (configFilePath, config) => fs.writeFileSync(configFilePath, JSON.stringify(config, null, '\t'))
13
+
14
+ // ...
15
+
16
+ class ATV {
17
+ constructor(addr, port) {
18
+ this.addr = addr
19
+ this.port = port || 7000
20
+
21
+ this.httpClient = httpClientFactory()
22
+ this.auth_secret = null
23
+ }
24
+
25
+ // ...
26
+
27
+ auth(configFilePath, authenticator) {
28
+ async function auth(owner) {
29
+ await owner.httpClient.connect(owner.addr, owner.port)
30
+
31
+ let conf = loadConfig(configFilePath)
32
+ if (!conf || !conf['auth_secret']) {
33
+ // a pairing does not exist and must be performed.
34
+
35
+ // ...
36
+ // SRP parameters.
37
+ const srp = new SRP(2048)
38
+
39
+ let I = '366B4165DD64AD3A'
40
+ let P
41
+ let s
42
+ let B
43
+ let a
44
+ let A
45
+ let M1
46
+
47
+ await owner.httpClient.request('POST', '/pair-pin-start')
48
+ .then(() => authenticator())
49
+ .then((pin) => {
50
+ P = pin
51
+
52
+ return owner.httpClient.request(
53
+ 'POST',
54
+ '/pair-setup-pin',
55
+ {
56
+ 'Content-Type': 'application/x-apple-binary-plist',
57
+ },
58
+ bplistCreator({
59
+ user: '366B4165DD64AD3A',
60
+ method: 'pin',
61
+ }),
62
+ )
63
+ })
64
+ .then((res) => {
65
+ const { pk, salt } = bplistParser.parseBuffer(res.body)[0]
66
+
67
+ s = salt.toString('hex')
68
+ B = pk.toString('hex')
69
+
70
+ // SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
71
+ // subsequent session authentication(s).
72
+ a = crypto.randomBytes(32).toString('hex')
73
+
74
+ // SRP: Compute A and M1.
75
+ A = srp.A(a)
76
+ M1 = srp.M1(I, P, s, a, B)
77
+
78
+ return owner.httpClient.request(
79
+ 'POST',
80
+ '/pair-setup-pin',
81
+ {
82
+ 'Content-Type': 'application/x-apple-binary-plist',
83
+ },
84
+ bplistCreator({
85
+ pk: Buffer.from(A, 'hex'),
86
+ proof: Buffer.from(M1, 'hex'),
87
+ }),
88
+ )
89
+ })
90
+ .then(() => {
91
+ // confirm the auth secret (a).
92
+ const { epk, authTag } = ATVAuthenticator.confirm(a, srp.K(I, P, s, a, B))
93
+
94
+ // complete pair-setup-pin by registering the auth secret with the target device.
95
+ return owner.httpClient.request(
96
+ 'POST',
97
+ '/pair-setup-pin',
98
+ {
99
+ 'Content-Type': 'application/x-apple-binary-plist',
100
+ },
101
+ bplistCreator({
102
+ epk: Buffer.from(epk, 'hex'),
103
+ authTag: Buffer.from(authTag, 'hex'),
104
+ }),
105
+ )
106
+ })
107
+ .then(() => {
108
+ // save the auth secret for subsequent session authentication(s).
109
+ !conf && (conf = {})
110
+ conf['auth_secret'] = a
111
+ saveConfig(configFilePath, conf)
112
+ })
113
+ }
114
+
115
+ // ...
116
+ // Authenticate session with the target device using existing pairing information.
117
+ const verifier = ATVAuthenticator.verifier(conf['auth_secret'])
118
+
119
+ return owner.httpClient.request(
120
+ 'POST',
121
+ '/pair-verify',
122
+ {
123
+ 'Content-Type': 'application/octet-stream',
124
+ },
125
+ verifier.verifierBody,
126
+ )
127
+ .then((res) => {
128
+ const atv_pub = res.body.slice(0, 32).toString('hex')
129
+ const atv_data = res.body.slice(32).toString('hex')
130
+
131
+ const shared = ATVAuthenticator.shared(verifier.v_pri, atv_pub)
132
+ const signed = ATVAuthenticator.signed(conf['auth_secret'], verifier.v_pub, atv_pub)
133
+ const signature = Buffer.from(
134
+ Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex')
135
+ + ATVAuthenticator.signature(shared, atv_data, signed),
136
+ 'hex',
137
+ )
138
+
139
+ return owner.httpClient.request(
140
+ 'POST',
141
+ '/pair-verify',
142
+ {
143
+ 'Content-Type': 'application/octet-stream',
144
+ },
145
+ signature,
146
+ )
147
+ })
148
+ };
149
+
150
+ return auth(this)
151
+ }
152
+
153
+ authSecret() {
154
+ return this.auth_secret
155
+ }
156
+
157
+ authSimple(authenticator) {
158
+ async function auth(owner) {
159
+ await owner.httpClient.connect(owner.addr, owner.port)
160
+
161
+ let conf = null
162
+ if (!conf || !conf['auth_secret']) {
163
+ // a pairing does not exist and must be performed.
164
+
165
+ // ...
166
+ // SRP parameters.
167
+ const srp = new SRP(2048)
168
+
169
+ let I = '366B4165DD64AD3A'
170
+ let P
171
+ let s
172
+ let B
173
+ let a
174
+ let A
175
+ let M1
176
+
177
+ await owner.httpClient.request('POST', '/pair-pin-start')
178
+ .then(() => authenticator())
179
+ .then((pin) => {
180
+ P = pin
181
+
182
+ return owner.httpClient.request(
183
+ 'POST',
184
+ '/pair-setup-pin',
185
+ {
186
+ 'Content-Type': 'application/x-apple-binary-plist',
187
+ },
188
+ bplistCreator({
189
+ user: '366B4165DD64AD3A',
190
+ method: 'pin',
191
+ }),
192
+ )
193
+ })
194
+ .then((res) => {
195
+ const { pk, salt } = bplistParser.parseBuffer(res.body)[0]
196
+
197
+ s = salt.toString('hex')
198
+ B = pk.toString('hex')
199
+
200
+ // SRP: Generate random auth_secret, 'a'; if pairing is successful, it'll be utilized in
201
+ // subsequent session authentication(s).
202
+ a = crypto.randomBytes(32).toString('hex')
203
+
204
+ // SRP: Compute A and M1.
205
+ A = srp.A(a)
206
+ M1 = srp.M1(I, P, s, a, B)
207
+
208
+ return owner.httpClient.request(
209
+ 'POST',
210
+ '/pair-setup-pin',
211
+ {
212
+ 'Content-Type': 'application/x-apple-binary-plist',
213
+ },
214
+ bplistCreator({
215
+ pk: Buffer.from(A, 'hex'),
216
+ proof: Buffer.from(M1, 'hex'),
217
+ }),
218
+ )
219
+ }).then(() => {
220
+ // confirm the auth secret (a).
221
+ const { epk, authTag } = ATVAuthenticator.confirm(a, srp.K(I, P, s, a, B))
222
+
223
+ // complete pair-setup-pin by registering the auth secret with the target device.
224
+ return owner.httpClient.request(
225
+ 'POST',
226
+ '/pair-setup-pin',
227
+ {
228
+ 'Content-Type': 'application/x-apple-binary-plist',
229
+ },
230
+ bplistCreator({
231
+ epk: Buffer.from(epk, 'hex'),
232
+ authTag: Buffer.from(authTag, 'hex'),
233
+ }),
234
+ )
235
+ })
236
+ .then(() => {
237
+ // save the auth secret for subsequent session authentication(s).
238
+ console.log(a)
239
+ owner.auth_secret = a
240
+ })
241
+ }
242
+ }
243
+ return auth(this)
244
+ }
245
+
246
+ verifySimple(secret) {
247
+ // ...
248
+ // Authenticate session with the target device using existing pairing information.
249
+ const verifier = ATVAuthenticator.verifier(secret)
250
+
251
+ return owner.httpClient.request(
252
+ 'POST',
253
+ '/pair-verify',
254
+ {
255
+ 'Content-Type': 'application/octet-stream',
256
+ },
257
+ verifier.verifierBody,
258
+ )
259
+ .then((res) => {
260
+ const atv_pub = res.body.slice(0, 32).toString('hex')
261
+ const atv_data = res.body.slice(32).toString('hex')
262
+
263
+ const shared = ATVAuthenticator.shared(verifier.v_pri, atv_pub)
264
+ const signed = ATVAuthenticator.signed(secret, verifier.v_pub, atv_pub)
265
+ const signature = Buffer.from(
266
+ Buffer.from([0x00, 0x00, 0x00, 0x00]).toString('hex')
267
+ + ATVAuthenticator.signature(shared, atv_data, signed),
268
+ 'hex',
269
+ )
270
+
271
+ return owner.httpClient.request(
272
+ 'POST',
273
+ '/pair-verify',
274
+ {
275
+ 'Content-Type': 'application/octet-stream',
276
+ },
277
+ signature,
278
+ )
279
+ })
280
+ }
281
+
282
+ play(videoUrl) {
283
+ return this.httpClient.request(
284
+ 'POST',
285
+ '/play',
286
+ {
287
+ 'Content-Type': 'application/x-apple-binary-plist',
288
+ },
289
+ bplistCreator({
290
+ 'Content-Location': videoUrl,
291
+ 'Start-Location': 0,
292
+ }),
293
+ )
294
+ }
295
+
296
+ stop() {
297
+ return this.httpClient.request('POST', '/stop')
298
+ }
299
+
300
+ close() {
301
+ this.httpClient.close()
302
+ }
303
+ }
304
+
305
+ // ...
306
+
307
+ module.exports = ATV
@@ -0,0 +1,136 @@
1
+ // RadioCLI modification, 2026-10-16: replace elliptic with native Ed25519.
2
+ // Distributed under the accompanying AGPLv3; pairing formats remain unchanged.
3
+ const ed25519 = require('./ed25519')
4
+ const crypto = require('crypto')
5
+ const axlsign = require('axlsign')
6
+ const { hexString2ArrayBuffer, buf2hex } = require('./util')
7
+
8
+ // ...
9
+ // Note: All functions expect parameters to be hex strings.
10
+
11
+ function pair_setup_aes_key(K) {
12
+ return crypto.createHash('sha512')
13
+ .update('Pair-Setup-AES-Key')
14
+ .update(hexString2ArrayBuffer(K))
15
+ .digest('hex')
16
+ .substring(0, 32)
17
+ }
18
+
19
+ function pair_setup_aes_iv(K) {
20
+ let ab = crypto.createHash('sha512')
21
+ .update('Pair-Setup-AES-IV')
22
+ .update(hexString2ArrayBuffer(K))
23
+ .digest()
24
+
25
+ ab = ab.slice(0, 16)
26
+ ab[ab.length - 1] += 0x01
27
+
28
+ return buf2hex(ab)
29
+ }
30
+
31
+ function pair_verify_aes_key(shared) {
32
+ return buf2hex(
33
+ crypto.createHash('sha512')
34
+ .update('Pair-Verify-AES-Key')
35
+ .update(hexString2ArrayBuffer(shared))
36
+ .digest()
37
+ .slice(0, 16),
38
+ )
39
+ }
40
+
41
+ function pair_verify_aes_iv(shared) {
42
+ return buf2hex(
43
+ crypto.createHash('sha512')
44
+ .update('Pair-Verify-AES-IV')
45
+ .update(hexString2ArrayBuffer(shared))
46
+ .digest()
47
+ .slice(0, 16),
48
+ )
49
+ }
50
+
51
+ // ...
52
+ // Public.
53
+
54
+ function a_pub(a) {
55
+ return ed25519.publicKey(a)
56
+ }
57
+
58
+ function confirm(a, K) {
59
+ const key = pair_setup_aes_key(K)
60
+ const iv = pair_setup_aes_iv(K)
61
+
62
+ const cipher = crypto.createCipheriv(
63
+ 'aes-128-gcm',
64
+ hexString2ArrayBuffer(key),
65
+ hexString2ArrayBuffer(iv),
66
+ )
67
+
68
+ let encrypted = cipher.update(hexString2ArrayBuffer(a_pub(a)), null, 'hex')
69
+ encrypted += cipher.final('hex')
70
+
71
+ return {
72
+ epk: encrypted,
73
+ authTag: buf2hex(cipher.getAuthTag()),
74
+ }
75
+ }
76
+
77
+ function verifier(a) {
78
+ const keyPair = axlsign.generateKeyPair(crypto.randomBytes(32))
79
+ const v_pri = buf2hex(keyPair.private)
80
+ const v_pub = buf2hex(keyPair.public)
81
+
82
+ const header = Buffer.from([0x01, 0x00, 0x00, 0x00])
83
+ const a_pub_buf = Buffer.from(a_pub(a), 'hex')
84
+
85
+ return {
86
+ verifierBody: Buffer.concat(
87
+ [header, keyPair.public, a_pub_buf],
88
+ header.byteLength + keyPair.public.byteLength + a_pub_buf.byteLength,
89
+ ),
90
+ v_pri,
91
+ v_pub,
92
+ }
93
+ }
94
+
95
+ function shared(v_pri, atv_pub) {
96
+ return buf2hex(
97
+ axlsign.sharedKey(
98
+ hexString2ArrayBuffer(v_pri),
99
+ hexString2ArrayBuffer(atv_pub),
100
+ ),
101
+ )
102
+ }
103
+
104
+ function signed(a, v_pub, atv_pub) {
105
+ return ed25519.sign(a, v_pub + atv_pub)
106
+ }
107
+
108
+ function signature(shared, atv_data, signed) {
109
+ const cipher = crypto.createCipheriv(
110
+ 'aes-128-ctr',
111
+ hexString2ArrayBuffer(pair_verify_aes_key(shared)),
112
+ hexString2ArrayBuffer(pair_verify_aes_iv(shared)),
113
+ )
114
+
115
+ // discard the result of encrypting atv_data.
116
+ cipher.update(hexString2ArrayBuffer(atv_data))
117
+
118
+ let encrypted = cipher.update(Buffer.from(signed, 'hex'), null, 'hex')
119
+ encrypted += cipher.final('hex')
120
+
121
+ return encrypted
122
+ }
123
+
124
+ module.exports = {
125
+ pair_setup_aes_key,
126
+ pair_setup_aes_iv,
127
+ pair_verify_aes_key,
128
+ pair_verify_aes_iv,
129
+
130
+ a_pub,
131
+ confirm,
132
+ verifier,
133
+ shared,
134
+ signed,
135
+ signature,
136
+ }
@@ -0,0 +1,69 @@
1
+ const events = require('events')
2
+ const util = require('util')
3
+ const config = require('./config')
4
+ const nu = require('./num_util')
5
+
6
+ const SEQ_NUM_WRAP = Math.pow(2, 16)
7
+
8
+ function AudioOut() {
9
+ events.EventEmitter.call(this)
10
+
11
+ this.lastSeq = -1
12
+ this.hasAirTunes = false
13
+ }
14
+
15
+ util.inherits(AudioOut, events.EventEmitter)
16
+
17
+ AudioOut.prototype.init = function (devices, circularBuffer) {
18
+ var self = this
19
+ config.rtp_time_ref = new Date().getTime()
20
+
21
+ devices.on('airtunes_devices', function (hasAirTunes) {
22
+ self.hasAirTunes = hasAirTunes
23
+ })
24
+
25
+ devices.on('need_sync', function () {
26
+ // a sync is forced when a new remote device is added
27
+ self.emit('need_sync', self.lastSeq)
28
+ })
29
+
30
+ function sendPacket(seq) {
31
+ var packet = circularBuffer.readPacket()
32
+
33
+ packet.seq = seq % SEQ_NUM_WRAP
34
+ packet.timestamp = nu.low32(seq * config.frames_per_packet + 2 * config.sampling_rate)
35
+
36
+ if (self.hasAirTunes && seq % config.sync_period == 0)
37
+ self.emit('need_sync', seq)
38
+
39
+ self.emit('packet', packet)
40
+ packet.release()
41
+ }
42
+
43
+ function syncAudio() {
44
+ /*
45
+ * Each time syncAudio() runs, a burst of packet is sent.
46
+ * Increasing config.stream_latency lowers CPU usage but increases the size of the burst.
47
+ * If the burst size exceeds the UDP windows size (which we do not know), packets are lost.
48
+ */
49
+ var elapsed = new Date().getTime() - config.rtp_time_ref
50
+
51
+ /*
52
+ * currentSeq is the # of the packet we should be sending now. We have some packets to catch-up
53
+ * since syncAudio is not always running.
54
+ */
55
+ var currentSeq = Math.floor(elapsed * config.sampling_rate / (config.frames_per_packet * 1000))
56
+
57
+ for (let i = self.lastSeq + 1; i <= currentSeq; i++)
58
+ sendPacket(i)
59
+
60
+ self.lastSeq = currentSeq
61
+
62
+ // reschedule ourselves later
63
+ setTimeout(syncAudio, config.stream_latency)
64
+ }
65
+
66
+ syncAudio()
67
+ }
68
+
69
+ module.exports = AudioOut