@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,133 @@
1
+ const Stream = require('stream')
2
+ const util = require('util')
3
+ const PacketPool = require('./packet_pool')
4
+
5
+ const WAITING = 0
6
+ const FILLING = 1
7
+ const NORMAL = 2
8
+ const DRAINING = 3
9
+ const ENDING = 4
10
+ const ENDED = 5
11
+
12
+ function CircularBuffer(packetsInBuffer, packetSize) {
13
+ Stream.call(this)
14
+
15
+ this.packetPool = new PacketPool()
16
+ this.maxSize = packetsInBuffer * packetSize
17
+ this.packetSize = packetSize
18
+ this.writable = true
19
+ this.muted = false
20
+ this.buffers = []
21
+ this.currentSize = 0
22
+ this.status = WAITING
23
+ }
24
+
25
+ util.inherits(CircularBuffer, Stream)
26
+
27
+ CircularBuffer.prototype.write = function (chunk) {
28
+ this.buffers.push(chunk)
29
+ this.currentSize += chunk.length
30
+
31
+ if (this.status === ENDING || this.status === ENDED)
32
+ throw new Error('Cannot write in buffer after closing it')
33
+
34
+ if (this.status === WAITING) {
35
+ // notify when we receive the first chunk
36
+ this.emit('status', 'buffering')
37
+ this.status = FILLING
38
+ }
39
+
40
+ if (this.status === FILLING && this.currentSize > this.maxSize / 2) {
41
+ this.status = NORMAL
42
+ this.emit('status', 'playing')
43
+ }
44
+
45
+ if (this.currentSize >= this.maxSize) {
46
+ this.status = DRAINING
47
+ return false
48
+ }
49
+ else {
50
+ return true
51
+ }
52
+ }
53
+
54
+ CircularBuffer.prototype.readPacket = function () {
55
+ var packet = this.packetPool.getPacket()
56
+
57
+ // play silence until buffer is filled enough
58
+ if (this.status !== ENDING && this.status !== ENDED
59
+ && (this.status === FILLING || this.currentSize < this.packetSize)) {
60
+ packet.pcm.fill(0)
61
+
62
+ if (this.status !== FILLING && this.status !== WAITING) {
63
+ this.status = FILLING
64
+ this.emit('status', 'buffering')
65
+ }
66
+ }
67
+ else {
68
+ var offset = 0, remaining = this.packetSize
69
+
70
+ // fill a whole packet with data
71
+ while (remaining > 0) {
72
+ // pad packet with silence if buffer is empty
73
+ if (this.buffers.length === 0) {
74
+ packet.pcm.fill(0, offset)
75
+ remaining = 0
76
+ break
77
+ }
78
+
79
+ var first = this.buffers[0]
80
+
81
+ if (first.length <= remaining) {
82
+ // pop the whole buffer from the queue
83
+ first.copy(packet.pcm, offset)
84
+ offset += first.length
85
+ remaining -= first.length
86
+ this.buffers.shift()
87
+ }
88
+ else {
89
+ // first buffer contains enough data to fill a packet: slice it
90
+ first.copy(packet.pcm, offset, 0, remaining)
91
+ this.buffers[0] = first.slice(remaining)
92
+ offset += remaining
93
+ remaining = 0
94
+ }
95
+ }
96
+
97
+ this.currentSize -= this.packetSize
98
+
99
+ // emit 'end' only once
100
+ if (this.status === ENDING && this.currentSize <= 0) {
101
+ this.status = ENDED
102
+ this.currentSize = 0
103
+ this.emit('status', 'end')
104
+ }
105
+
106
+ // notify that the buffer now has enough room if needed
107
+ if (this.status === DRAINING && this.currentSize < this.maxSize / 2) {
108
+ this.status = NORMAL
109
+ this.emit('drain')
110
+ }
111
+ }
112
+
113
+ if (this.muted)
114
+ packet.pcm.fill(0)
115
+
116
+ return packet
117
+ }
118
+
119
+ CircularBuffer.prototype.end = function () {
120
+ // flush the buffer if it was filling
121
+ if (this.status === FILLING)
122
+ this.emit('status', 'playing')
123
+
124
+ this.status = ENDING
125
+ }
126
+
127
+ CircularBuffer.prototype.reset = function () {
128
+ this.buffers = []
129
+ this.currentSize = 0
130
+ this.status = WAITING
131
+ }
132
+
133
+ module.exports = CircularBuffer
@@ -0,0 +1,26 @@
1
+ const nu = require('./num_util')
2
+
3
+ const Config = {
4
+ user_agent: 'iTunes/11.3.1 (Windows; Microsoft Windows 10 x64 (Build 19044); x64) (dt:2)',
5
+ udp_default_port: 54621, // preferred starting port in AirTunes v2
6
+ frames_per_packet: 352, // samples per frames in ALAC packets
7
+ channels_per_frame: 2, // always stereo in AirTunes v2
8
+ bits_per_channel: 16, // -> 2 bytes per channel
9
+ packet_size: 352 * 2 * 2, // frames*channels*bytes
10
+ packets_in_buffer: 200, // increase this buffer protects against network issues
11
+ coreaudio_min_level: 5, // if CoreAudio's internal buffer drops too much, inject some silence to raise it
12
+ coreaudio_check_period: 2000, // CoreAudio buffer level check period
13
+ coreaudio_preload: 352 * 2 * 2 * 50, // ~0.5s of silence pushed to CoreAudio to avoid draining AudioQueue
14
+ sampling_rate: 44100, // fixed by AirTunes v2
15
+ sync_period: 126, // UDP sync packets are sent to all AirTunes devices regularly
16
+ stream_latency: 100, // audio UDP packets are flushed in bursts periodically
17
+ rtsp_timeout: 2147483647, // RTSP servers are considered gone if no reply is received before the timeout
18
+ rtsp_heartbeat: 15000, // some RTSP (like HomePod) servers requires heartbeat. Recommanded value: 60000 (1 minute). Set to 0 if you want to disable it.
19
+ rtp_time_ref: 0,
20
+ device_magic: nu.randomInt(9),
21
+ ntp_epoch: 0x83aa7e80,
22
+ iv_base64: 'ePRBLI0XN5ArFaaz7ncNZw',
23
+ rsa_aeskey_base64: 'VjVbxWcmYgbBbhwBNlCh3K0CMNtWoB844BuiHGUJT51zQS7SDpMnlbBIobsKbfEJ3SCgWHRXjYWf7VQWRYtEcfx7ejA8xDIk5PSBYTvXP5dU2QoGrSBv0leDS6uxlEWuxBq3lIxCxpWO2YswHYKJBt06Uz9P2Fq2hDUwl3qOQ8oXb0OateTKtfXEwHJMprkhsJsGDrIc5W5NJFMAo6zCiM9bGSDeH2nvTlyW6bfI/Q0v0cDGUNeY3ut6fsoafRkfpCwYId+bg3diJh+uzw5htHDyZ2sN+BFYHzEfo8iv4KDxzeya9llqg6fRNQ8d5YjpvTnoeEQ9ye9ivjkBjcAfVw',
24
+ }
25
+
26
+ module.exports = Config
@@ -0,0 +1,532 @@
1
+ const dgram = require('dgram')
2
+ const events = require('events')
3
+ const util = require('util')
4
+ const crypto = require('crypto')
5
+ const process = require('process')
6
+ const config = require('./config')
7
+ const nu = require('./num_util')
8
+ const RTSP = require('./rtsp')
9
+ const UDPServers = require('./udp_servers')
10
+
11
+ const RTP_HEADER_SIZE = 12
12
+
13
+ function BufferWithNames(size) {
14
+ // Very simple solution for this immediate need...
15
+ // I call it BufferWithNames because the names are not unique keys.
16
+ this.size = size
17
+ this.buffer = []
18
+ }
19
+
20
+ BufferWithNames.prototype.add = function (name, item) {
21
+ while (this.buffer.length > this.size) {
22
+ this.buffer.shift()
23
+ }
24
+ this.buffer.push([name, item])
25
+ }
26
+
27
+ BufferWithNames.prototype.getLatestNamed = function (name) {
28
+ var buf = this.buffer
29
+ for (let i = buf.length - 1; i >= 0; i--) {
30
+ if (buf[i][0] == name) {
31
+ return buf[i][1]
32
+ }
33
+ }
34
+ }
35
+
36
+ function AirTunesDevice(host, audioOut, options, mode = 0, txt = '') {
37
+ events.EventEmitter.call(this)
38
+
39
+ if (!host)
40
+ throw new Error('host is mandatory')
41
+
42
+ this.bindings = null
43
+ this.bindingsok = false
44
+ this.audioPacketHistory = new BufferWithNames(100)
45
+ try {
46
+ if (!(process.platform == 'darwin')) { // MacOSX arm64 segfaults on airtunes.node
47
+ this.bindings = require('../build/Release/airtunes')
48
+ this.bindingsok = true
49
+ }
50
+ }
51
+ catch (e) {}
52
+
53
+ this.udpServers = new UDPServers()
54
+ this.audioOut = audioOut
55
+ this.type = 'airtunes'
56
+ this.options = options
57
+ this.host = host
58
+ this.port = options?.port || 5000
59
+ this.key = this.host + ':' + this.port
60
+ this.mode = mode // Homepods with or without passcode
61
+ // if(options.password != null && legacy == true){
62
+ // this.mode = 1; // Airport / Shairport legacy passcode mode
63
+ // this.mode = 2 // MFi mode
64
+ // }
65
+ this.forceAlac = options.forceAlac ?? true
66
+ // this.skipAutoVolume = options.skipAutoVolume ?? false
67
+ this.statusflags = []
68
+ this.alacEncoding = options?.alacEncoding ?? true
69
+ this.airplay2 = options?.airplay2 ?? false
70
+ this.txt = txt
71
+ this.borkedshp = false
72
+ console.log('yasc', this.txt)
73
+ console.log('port', this.port)
74
+ let a = this.txt.filter(u => String(u).startsWith('et='))
75
+ if ((a[0] ?? '').includes('4')) {
76
+ this.mode = 2
77
+ }
78
+ let b = this.txt.filter(u => String(u).startsWith('cn='))
79
+ if (!this.forceAlac) {
80
+ if ((b[0] ?? '').includes('0')) {
81
+ this.alacEncoding = false
82
+ }
83
+ }
84
+ let c = this.txt.filter(u => String(u).startsWith('sf='))
85
+ this.statusflags = c[0] ? parseInt(c[0].substring(3)).toString(2).split('') : []
86
+ if (c.length == 0) {
87
+ c = this.txt.filter(u => String(u).startsWith('flags='))
88
+ this.statusflags = c[0] ? parseInt(c[0].substring(6)).toString(2).split('') : []
89
+ }
90
+ this.needPassword = false
91
+ this.needPin = false
92
+ this.transient = false
93
+ let d = ''
94
+ d = this.txt.filter(u => String(u).startsWith('features='))
95
+ if (d.length == 0) d = this.txt.filter(u => String(u).startsWith('ft='))
96
+ let features_set = d.length > 0 ? d[0].substring(d[0].indexOf('=') + 1).split(',') : []
97
+ this.features = [...features_set.length > 0 ? parseInt(features_set[0]).toString(2).split('') : [], ...features_set.length > 1 ? parseInt(features_set[1]).toString(2).split('') : []]
98
+ if (this.features.length > 0) {
99
+ this.transient = (this.features[this.features.length - 1 - 48] == '1')
100
+ }
101
+
102
+ if (this.statusflags != []) {
103
+ let PasswordRequired = (this.statusflags[this.statusflags.length - 1 - 7] == '1')
104
+ let PinRequired = (this.statusflags[this.statusflags.length - 1 - 3] == '1')
105
+ let OneTimePairingRequired = (this.statusflags[this.statusflags.length - 1 - 9] == '1')
106
+ console.log('needPss', PasswordRequired, PinRequired, OneTimePairingRequired)
107
+ this.needPassword = PasswordRequired
108
+ this.needPin = (PinRequired || OneTimePairingRequired)
109
+ this.transient = (!(PasswordRequired || PinRequired || OneTimePairingRequired)) ?? true
110
+ console.log('needPss', this.needPassword)
111
+ }
112
+ console.log('transient', this.transient)
113
+ // detect old shairports with broken text
114
+ let oldver1 = this.txt.filter(u => String(u).startsWith('sm='))
115
+ let oldver2 = this.txt.filter(u => String(u).startsWith('sv='))
116
+ if ((b[0] ?? '') == 'cn=0,1' && (a[0] ?? '') == 'et=0,1' && (oldver1[0] ?? '') == 'sm=false' && (oldver2[0] ?? '') == 'sv=false' && this.statusflags.length == 0) {
117
+ console.log('borked shairport found')
118
+ this.alacEncoding = true
119
+ this.borkedshp = true
120
+ }
121
+ let k = this.txt.filter(u => String(u).startsWith('am='))
122
+ if ((k[0] ?? '').includes('AppleTV3,1') || (k[0] ?? '').includes('AirReceiver3,1') || (k[0] ?? '').includes('AirRecever3,1') || (k[0] ?? '').includes('Shairport')) {
123
+ this.alacEncoding = true
124
+ this.airplay2 = false
125
+ }
126
+ k = this.txt.filter(u => String(u).startsWith('rmodel='))
127
+ if ((k[0] ?? '').includes('AppleTV3,1') || (k[0] ?? '').includes('AirReceiver3,1') || (k[0] ?? '').includes('AirRecever3,1') || (k[0] ?? '').includes('Shairport')) {
128
+ this.alacEncoding = true
129
+ this.airplay2 = false
130
+ }
131
+ let manufacturer = this.txt.filter(u => String(u).startsWith('manufacturer='))
132
+ if ((manufacturer[0] ?? '').includes('Sonos')) {
133
+ this.mode = 2
134
+ this.needPin = true
135
+ }
136
+ console.log('needPin', this.needPin)
137
+ console.log('mode-atv', this.mode)
138
+ console.log('alacEncoding', this.alacEncoding)
139
+ try {
140
+ this.rtsp = new RTSP.Client(options.volume || 50, options.password || null, audioOut,
141
+ {
142
+ mode: this.mode,
143
+ txt: this.txt,
144
+ alacEncoding: this.alacEncoding,
145
+ needPassword: this.needPassword,
146
+ airplay2: this.airplay2,
147
+ needPin: this.needPin,
148
+ debug: options.debug,
149
+ transient: this.transient,
150
+ borkedshp: this.borkedshp,
151
+ })
152
+ }
153
+ catch (e) {
154
+ console.log('rtsp error', e)
155
+ }
156
+ this.audioCallback = null
157
+ this.encoder = []
158
+ this.credentials = null
159
+
160
+ // this.func = `
161
+ // const {Worker, isMainThread, parentPort, workerData} = require('node:worker_threads');
162
+ // var { WebSocketServer } = require('ws');
163
+ // const wss = new WebSocketServer({ port: 8980 });
164
+ // wss.on('connection', function connection(ws) {
165
+ // ws.on('message', function message(data) {
166
+ // parentPort.postMessage({message: data});
167
+ // });
168
+ // parentPort.on("message", data => {
169
+ // console.log("ass");
170
+ // ws.send(data);
171
+ // });
172
+ // });`;
173
+ // this.worker = new Worker(func, {eval: true});
174
+ }
175
+
176
+ util.inherits(AirTunesDevice, events.EventEmitter)
177
+
178
+ AirTunesDevice.prototype.start = function () {
179
+ var self = this
180
+ this.audioSocket = dgram.createSocket('udp4')
181
+
182
+ // Wait until timing and control ports are chosen. We need them in RTSP handshake.
183
+ this.udpServers.on('ports', function (err) {
184
+ if (err) {
185
+ console.log(err.code)
186
+ self.status = 'stopped'
187
+ self.emit('status', 'stopped')
188
+ console.log('port issues')
189
+ self.emit('error', 'udp_ports', err.code)
190
+
191
+ return
192
+ }
193
+ self.doHandshake()
194
+ })
195
+
196
+ this.udpServers.bind(this.host)
197
+ }
198
+
199
+ AirTunesDevice.prototype.doHandshake = function () {
200
+ var self = this
201
+ try {
202
+ if (this.rtsp == null) {
203
+ try {
204
+ this.rtsp = new RTSP.Client(this.options.volume || 30, this.options.password || null, this.audioOut,
205
+ {
206
+ mode: this.mode,
207
+ txt: this.txt,
208
+ alacEncoding: this.alacEncoding,
209
+ needPassword: this.needPassword,
210
+ airplay2: this.airplay2,
211
+ needPin: this.needPin,
212
+ debug: true,
213
+ transient: this.transient,
214
+ borkedshp: this.borkedshp,
215
+ })
216
+ }
217
+ catch (e) {
218
+ console.log(e)
219
+ }
220
+ }
221
+ this.rtsp.on('config', function (setup) {
222
+ self.audioLatency = setup.audioLatency
223
+ self.requireEncryption = setup.requireEncryption
224
+ self.serverPort = setup.server_port
225
+ self.controlPort = setup.control_port
226
+ self.timingPort = setup.timing_port
227
+ self.credentials = setup.credentials
228
+ })
229
+
230
+ this.rtsp.on('ready', function () {
231
+ self.relayAudio()
232
+ })
233
+
234
+ this.rtsp.on('need_password', function () {
235
+ self.emit('status', 'need_password')
236
+ })
237
+
238
+ this.rtsp.on('pair_failed', function () {
239
+ self.emit('status', 'pair_failed')
240
+ })
241
+
242
+ this.rtsp.on('pair_success', function () {
243
+ self.emit('status', 'pair_success')
244
+ })
245
+
246
+ this.rtsp.on('end', function (err) {
247
+ console.log(err)
248
+ self.cleanup()
249
+
250
+ if (err !== 'stopped')
251
+ self.emit(err)
252
+ })
253
+ }
254
+ catch (e) {
255
+ console.log(e)
256
+ }
257
+ // console.log(this.udpServers, this.host,this.port)
258
+ this.rtsp.startHandshake(this.udpServers, this.host, this.port)
259
+ }
260
+
261
+ AirTunesDevice.prototype.relayAudio = function () {
262
+ var self = this
263
+ this.status = 'ready'
264
+ this.emit('status', 'ready')
265
+
266
+ this.audioCallback = function (packet) {
267
+ var airTunes = makeAirTunesPacket(packet, self.encoder, self.requireEncryption, self.alacEncoding, self.credentials, self.bindings, self.bindingsok)
268
+ // if (self.credentials) {
269
+ // airTunes = self.credentials.encrypt(airTunes)
270
+ // }
271
+ if (self.audioSocket == null) {
272
+ self.audioSocket = dgram.createSocket('udp4')
273
+ }
274
+ self.audioSocket.send(
275
+ airTunes, 0, airTunes.length,
276
+ self.serverPort, self.host,
277
+ )
278
+ }
279
+ // this.sendAirTunesPacket = function(airTunes) {
280
+ // try{
281
+ // if(self.audioSocket == null){
282
+ // self.audioSocket = dgram.createSocket('udp4');
283
+ // }
284
+ // self.audioSocket.send(
285
+ // airTunes, 0, airTunes.length,
286
+ // self.serverPort, self.host
287
+ // );} catch(e){
288
+
289
+ // console.log('send error',e)
290
+ // }
291
+ // };
292
+
293
+ // this.audioCallback = function(packet) {
294
+ // var airTunes = makeAirTunesPacket(packet, self.encoder, self.requireEncryption, self.alacEncoding, self.credentials, self.bindings, self.bindingsok);
295
+ // try{
296
+ // self.sendAirTunesPacket(airTunes);
297
+ // self.audioPacketHistory.add(packet.seq, airTunes); // If we need to resend it
298
+ // } catch(e){}
299
+ // };
300
+
301
+ // this.udpServers.on('resendRequested', function (missedSeq, count) {
302
+ // try{
303
+ // for (var i = 0; i < count; i++) {
304
+ // airTunes = self.audioPacketHistory.getLatestNamed(missedSeq + i);
305
+ // if (airTunes != null)
306
+ // self.sendAirTunesPacket(airTunes);}}
307
+ // catch (_){}
308
+ // });
309
+
310
+ this.audioOut.on('packet', this.audioCallback)
311
+ }
312
+
313
+ AirTunesDevice.prototype.onSyncNeeded = function (seq) {
314
+ this.udpServers.sendControlSync(seq, this)
315
+ // if ( this.airplay2)this.rtsp.sendControlSync(seq, this, this.rtsp);
316
+ }
317
+
318
+ AirTunesDevice.prototype.cleanup = function () {
319
+ this.audioSocket = null
320
+ this.audioPacketHistory = null
321
+ this.status = 'stopped'
322
+ this.emit('status', 'stopped')
323
+ console.log('stop')
324
+ if (this.audioCallback) {
325
+ this.audioOut.removeListener('packet', this.audioCallback)
326
+ this.audioCallback = null
327
+ }
328
+
329
+ this.udpServers.close()
330
+ this.removeAllListeners()
331
+ this.rtsp = null
332
+ }
333
+
334
+ AirTunesDevice.prototype.reportStatus = function () {
335
+ this.emit('status', this.status)
336
+ }
337
+
338
+ AirTunesDevice.prototype.stop = function (cb) {
339
+ try {
340
+ this.rtsp.once('end', function () {
341
+ if (cb)
342
+ cb()
343
+ })
344
+ console.log('ted')
345
+ this.rtsp.teardown()
346
+ }
347
+ catch (_) {}
348
+ }
349
+
350
+ AirTunesDevice.prototype.setVolume = function (volume, callback) {
351
+ this.rtsp.setVolume(volume, callback)
352
+ }
353
+
354
+ AirTunesDevice.prototype.setTrackInfo = function (name, artist, album, callback) {
355
+ this.rtsp.setTrackInfo(name, artist, album, callback)
356
+ }
357
+
358
+ AirTunesDevice.prototype.setProgress = function (progress, duration, callback) {
359
+ this.rtsp.setProgress(progress, duration, callback)
360
+ }
361
+
362
+ AirTunesDevice.prototype.setArtwork = function (art, contentType, callback) {
363
+ this.rtsp.setArtwork(art, contentType, callback)
364
+ }
365
+
366
+ AirTunesDevice.prototype.setPasscode = function (password) {
367
+ this.rtsp.setPasscode(password)
368
+ }
369
+
370
+ AirTunesDevice.prototype.requireEncryption = function () {
371
+ return this.requireEncryption
372
+ }
373
+
374
+ module.exports = AirTunesDevice
375
+
376
+ function makeAirTunesPacket(packet, encoder, requireEncryption, alacEncoding = true, credentials = null, bindings, bindingsok) {
377
+ // console.log("alacEncoding2",alacEncoding)
378
+ var alac = (alacEncoding || credentials) ? pcmToALAC(encoder, packet.pcm, bindings, bindingsok) : pcmParse(encoder, packet.pcm)
379
+ var airTunes = new Buffer(alac.length + RTP_HEADER_SIZE)
380
+
381
+ const header = makeRTPHeader(packet)
382
+ if (requireEncryption) {
383
+ if (bindingsok) {
384
+ if (bindings == null) {
385
+ bindings = require('../build/Release/airtunes')
386
+ }
387
+ bindings.encryptAES(alac, alac.length)
388
+ }
389
+ else {
390
+ alac = encryptAES(alac, alac.length)
391
+ }
392
+ }
393
+ if (credentials) {
394
+ let pcm = credentials.encryptAudio(alac, header.slice(4, 12), packet.seq)
395
+ let airplay = new Buffer(RTP_HEADER_SIZE + pcm.length)
396
+ header.copy(airplay)
397
+ pcm.copy(airplay, RTP_HEADER_SIZE)
398
+ return airplay
399
+ // console.log(alac.length)
400
+ }
401
+ else {
402
+ header.copy(airTunes)
403
+ alac.copy(airTunes, RTP_HEADER_SIZE)
404
+ return airTunes
405
+ }
406
+ }
407
+
408
+ function pcmToALAC(encoder, pcmData, bindings, bindingsok) {
409
+ var alacData = new Buffer(config.packet_size + 8)
410
+ if (bindingsok == true) {
411
+ var alacSize = bindings.encodeALAC(encoder, pcmData, alacData, pcmData.length)
412
+ return alacData.slice(0, alacSize)
413
+ }
414
+ else {
415
+ // I only did the actual computational part, the rest that I didn't do should be realitively simple to do.
416
+ let bsize = 352, frames = 352 // Set these to whatever they should be
417
+ const p = new Uint8Array((352 * 2 * 2) + 8) // p = *out;
418
+ const input = new Uint32Array(pcmData.length / 4)
419
+ let j = 0
420
+ for (let i = 0; i < pcmData.length; i += 4) {
421
+ let res = pcmData[i]
422
+ res |= pcmData[i + 1] << 8
423
+ res |= pcmData[i + 2] << 16
424
+ res |= pcmData[i + 3] << 24
425
+ input[j++] = res
426
+ } // uint32_t *in = (uint32_t*) sample;
427
+
428
+ let pindex = 0, iindex = 0
429
+
430
+ p[pindex++] = 1 << 5 // 0b100000
431
+ p[pindex++] = 0
432
+ // 0b1001x, where x = Most Significant Bit of bsize, or basically just { set x if (bsize > 0x80000000) }
433
+ p[pindex++] = (1 << 4) | (1 << 1) | ((bsize & 0x80000000) >>> 31)
434
+ // bXX--bYY = bits XX to YY of bsize
435
+ // So we basically just splitting bsize into the individual byte values and storing them in p
436
+ // We've also shifted everything to the left by one (hence why we need the bit from bsize above)
437
+ p[pindex++] = ((bsize & 0x7f800000) << 1) >>> 24 // b30--b23
438
+ p[pindex++] = ((bsize & 0x007f8000) << 1) >>> 16 // b22--b15
439
+ p[pindex++] = ((bsize & 0x00007f80) << 1) >>> 8 // b14--b7
440
+ p[pindex] = ((bsize & 0x0000007f) << 1) // b6--b0
441
+ // And this is why we shifted the bits to the left.
442
+ p[pindex++] |= (input[iindex] & 0x00008000) >>> 15 // b7 from in[iindex]
443
+
444
+ let count = frames - 1
445
+
446
+ while (count--) {
447
+ let i = input[iindex++] // Just to make it a bit easier to read
448
+
449
+ // This is weird lmao. Everything that we're adding has been shifted left by one.
450
+ // And here, we're soring the lower 16 bits then the higher 16 bits.
451
+ p[pindex++] = ((i & 0x00007f80) >>> 7) // b14--b7
452
+ p[pindex++] = ((i & 0x0000007f) << 1) | ((i & 0x80000000) >>> 31) // b6--b0, b31
453
+ p[pindex++] = ((i & 0x7f800000) >>> 23) // b30--b23
454
+ p[pindex++] = ((i & 0x007f0000) >>> 15) | ((input[iindex] & 0x00008000) >> 15)// b16--b15, b7 from in[pindex]
455
+ }
456
+
457
+ // Last Sample
458
+ let i = input[iindex]
459
+ p[pindex++] = ((i & 0x00007f80) >>> 7) // b14--b7
460
+ p[pindex++] = ((i & 0x0000007f) << 1) | ((i & 0x80000000) >>> 31) // b6--b0, b31
461
+ p[pindex++] = ((i & 0x7f800000) >>> 23) // b30--b23
462
+ p[pindex++] = ((i & 0x007f0000) >>> 15) // b16--b15, 0 as last bit because we have no more data after this
463
+
464
+ // When we've read all we can from in, we need to fill the remaining space in p with 0's
465
+ count = (bsize - frames) * 4
466
+ while (count--) p[pindex++] = 0
467
+
468
+ // Frame Footer ??
469
+ p[pindex - 1] |= 1
470
+ p[pindex++] = (7 >>> 1) << 6
471
+
472
+ // const size = pindex;
473
+
474
+ const alacSize = pindex // Should be right
475
+ alacData = Buffer.from(p.buffer)
476
+ return alacData.slice(0, alacSize)
477
+ }
478
+ // alacData = new Buffer(p);
479
+
480
+ // var alacSize = bindings.encodeALAC(encoder, pcmData, alacData, pcmData.length);
481
+ // console.log(alacData)
482
+ }
483
+
484
+ function pcmParse(encoder, pcmData) {
485
+ let dst = new Uint8Array(352 * 4)
486
+ let src = pcmData
487
+
488
+ let a = 0
489
+ let b = 0
490
+ for (let size = 0; size < 352; size++) {
491
+ dst[a++] = src[b + 1]
492
+ dst[a++] = src[b++]
493
+ b++
494
+
495
+ dst[a++] = src[b + 1]
496
+ dst[a++] = src[b++]
497
+ b++
498
+ }
499
+ return Buffer.from(dst)
500
+ }
501
+
502
+ function encryptAES(alacData, alacSize) {
503
+ let result = new Buffer.concat([])
504
+ const isv = Buffer.from([0x78, 0xf4, 0x41, 0x2c, 0x8d, 0x17, 0x37, 0x90, 0x2b, 0x15, 0xa6, 0xb3, 0xee, 0x77, 0x0d, 0x67])
505
+ const aes_key = Buffer.from([0x14, 0x49, 0x7d, 0xcc, 0x98, 0xe1, 0x37, 0xa8, 0x55, 0xc1, 0x45, 0x5a, 0x6b, 0xc0, 0xc9, 0x79])
506
+ let remainder = alacData.length % 16
507
+ let end_of_encoded_data = alacData.length - remainder
508
+ let cipher = crypto.createCipheriv('aes-128-cbc', aes_key, isv)
509
+ cipher.setAutoPadding(false)
510
+
511
+ for (let i = 0, l = end_of_encoded_data - 16; i <= l; i += 16) {
512
+ let chunk = cipher.update(alacData.slice(i, i + 16))
513
+ result = Buffer.concat([result, chunk])
514
+ }
515
+ return Buffer.concat([result, alacData.slice(end_of_encoded_data)])
516
+ }
517
+
518
+ function makeRTPHeader(packet) {
519
+ var header = new Buffer(RTP_HEADER_SIZE)
520
+
521
+ if (packet.seq === 0)
522
+ header.writeUInt16BE(0x80e0, 0)
523
+ else
524
+ header.writeUInt16BE(0x8060, 0)
525
+
526
+ header.writeUInt16BE(nu.low16(packet.seq), 2)
527
+
528
+ header.writeUInt32BE(packet.timestamp, 4)
529
+ header.writeUInt32BE(config.device_magic, 8)
530
+
531
+ return header
532
+ }