@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.
- package/CHANGELOG.md +103 -2
- package/CONTRIBUTING.md +28 -0
- package/README.md +108 -253
- package/SECURITY.md +23 -1
- package/dist/agent/alarm-service.js +10 -7
- package/dist/agent/cli.js +16 -1
- package/dist/agent/command-schema.js +47 -0
- package/dist/agent/headless-host.js +226 -81
- package/dist/agent/launcher.js +2 -1
- package/dist/agent/mcp-server.js +10 -5
- package/dist/agent/service.js +75 -40
- package/dist/agent/session.js +214 -24
- package/dist/agent/tui-session.js +213 -0
- package/dist/alarms/cli.js +19 -3
- package/dist/alarms/occurrence-cache.js +25 -0
- package/dist/alarms/runner.js +87 -23
- package/dist/alarms/schedule.js +29 -1
- package/dist/alarms/scheduler.js +11 -3
- package/dist/alarms/system-volume-linux.js +160 -0
- package/dist/alarms/system-volume-ownership.js +76 -8
- package/dist/alarms/system-volume.js +44 -23
- package/dist/cli/arguments.js +212 -0
- package/dist/cli/completion.js +44 -0
- package/dist/cli/doctor.js +156 -0
- package/dist/cli/interactive.js +79 -0
- package/dist/cli/listener.js +227 -0
- package/dist/cli/output.js +121 -0
- package/dist/cli/render-tui.js +34 -0
- package/dist/cli.js +182 -199
- package/dist/platform/capabilities.js +8 -1
- package/dist/platform/desktop-media.js +61 -0
- package/dist/platform/diagnostics.js +273 -0
- package/dist/platform/json-worker.js +111 -0
- package/dist/platform/launch-command.js +9 -6
- package/dist/platform/mpris.js +356 -0
- package/dist/platform/network.js +69 -1
- package/dist/platform/omarchy.js +147 -0
- package/dist/platform/packages.js +7 -1
- package/dist/platform/runtime.js +1 -1
- package/dist/platform/terminals.js +24 -4
- package/dist/player/airplay-discovery.js +144 -61
- package/dist/player/airplay-policy.js +15 -0
- package/dist/player/airplay-process.js +63 -0
- package/dist/player/airplay-readiness.js +42 -0
- package/dist/player/airplay-sender-health.js +49 -49
- package/dist/player/airplay-sender-package.js +5 -0
- package/dist/player/airplay-sender-patch.js +102 -4
- package/dist/player/airplay-setup.js +51 -0
- package/dist/player/airplay-worker.js +97 -41
- package/dist/player/backend-install.js +9 -6
- package/dist/player/ffmpeg-input.js +37 -0
- package/dist/player/mpv-desktop-media.js +194 -0
- package/dist/player/mpv-ipc-client.js +51 -2
- package/dist/player/player-controller.js +390 -73
- package/dist/player/radio-engine.js +282 -0
- package/dist/playlists/playlist.js +18 -16
- package/dist/providers/cache.js +291 -75
- package/dist/providers/discovery-session.js +111 -0
- package/dist/providers/provider-manager.js +106 -28
- package/dist/providers/radio-browser.js +272 -41
- package/dist/providers/radio-garden.js +4 -2
- package/dist/setup-playback.js +152 -0
- package/dist/setup.js +88 -21
- package/dist/storage/file-lock.js +130 -0
- package/dist/storage/read-settings.js +33 -0
- package/dist/storage/replace-file.js +20 -0
- package/dist/storage/repository.js +247 -0
- package/dist/storage/station-identity.js +21 -0
- package/dist/storage/station-schema.js +27 -0
- package/dist/storage/store.js +314 -76
- package/dist/types.js +22 -1
- package/dist/ui/AdaptiveContent.js +21 -24
- package/dist/ui/App.js +708 -454
- package/dist/ui/AppContent.js +5 -5
- package/dist/ui/airplay-settings.js +12 -5
- package/dist/ui/bidi-ranges.js +45 -0
- package/dist/ui/components/AdaptiveMarquee.js +16 -9
- package/dist/ui/components/ReceiverVisualRow.js +39 -0
- package/dist/ui/format.js +37 -4
- package/dist/ui/help-content.js +5 -2
- package/dist/ui/rtl-output.js +137 -0
- package/dist/ui/screen-items.js +10 -4
- package/dist/ui/screens/AirPlaySettingsScreen.js +3 -3
- package/dist/ui/screens/AlarmsScreen.js +5 -2
- package/dist/ui/screens/HelpScreen.js +3 -3
- package/dist/ui/screens/NowPlayingScreen.js +2 -21
- package/dist/ui/screens/SettingsScreen.js +16 -16
- package/dist/ui/screens/StatsScreen.js +1 -1
- package/dist/ui/text-direction.js +9 -0
- package/dist/ui/theme.js +46 -0
- package/dist/ui/use-alarm-tui.js +24 -24
- package/dist/ui/use-app-input.js +42 -8
- package/dist/ui/use-command-executor.js +21 -14
- package/dist/ui/visualizers/boat-lake-kernel.js +325 -0
- package/dist/ui/visualizers/boat-lake.js +161 -0
- package/dist/ui/visualizers/gallop.js +2 -2
- package/dist/ui/visualizers/receiver-style-registry.js +3 -1
- package/dist/ui/visualizers/receiver-visualizers.js +6 -108
- package/dist/ui/visualizers/sumi-ocean.js +183 -0
- package/dist/ui/visualizers/terminal-cell.js +45 -0
- package/dist/update-check.js +2 -1
- package/docs/THIRD_PARTY_NOTICES.md +6 -4
- package/docs/boat-lake.md +88 -0
- package/docs/sumi-ocean.md +80 -0
- package/package.json +36 -12
- package/scripts/build-airplay-native.mjs +22 -0
- package/vendor/node-airtunes2/LICENSE +208 -0
- package/vendor/node-airtunes2/RADIOCLI_PATCH.md +29 -0
- package/vendor/node-airtunes2/README.md +225 -0
- package/vendor/node-airtunes2/UPSTREAM.json +50 -0
- package/vendor/node-airtunes2/binding.gyp +41 -0
- package/vendor/node-airtunes2/lib/ap2_test.js +4 -0
- package/vendor/node-airtunes2/lib/atv.js +307 -0
- package/vendor/node-airtunes2/lib/atvAuthenticator.js +136 -0
- package/vendor/node-airtunes2/lib/audio_out.js +69 -0
- package/vendor/node-airtunes2/lib/circular_buffer.js +133 -0
- package/vendor/node-airtunes2/lib/config.js +26 -0
- package/vendor/node-airtunes2/lib/device_airtunes.js +532 -0
- package/vendor/node-airtunes2/lib/devices.js +266 -0
- package/vendor/node-airtunes2/lib/ed25519.js +26 -0
- package/vendor/node-airtunes2/lib/homekit/credentials.js +86 -0
- package/vendor/node-airtunes2/lib/homekit/encryption.js +69 -0
- package/vendor/node-airtunes2/lib/homekit/number.js +44 -0
- package/vendor/node-airtunes2/lib/homekit/tlv.js +95 -0
- package/vendor/node-airtunes2/lib/http.js +189 -0
- package/vendor/node-airtunes2/lib/index.d.ts +141 -0
- package/vendor/node-airtunes2/lib/index.js +127 -0
- package/vendor/node-airtunes2/lib/ntp.js +37 -0
- package/vendor/node-airtunes2/lib/num_util.js +21 -0
- package/vendor/node-airtunes2/lib/packet_pool.js +43 -0
- package/vendor/node-airtunes2/lib/rtsp.js +1807 -0
- package/vendor/node-airtunes2/lib/srp.js +158 -0
- package/vendor/node-airtunes2/lib/udp_servers.js +214 -0
- package/vendor/node-airtunes2/lib/util.js +12 -0
- package/vendor/node-airtunes2/package.json +31 -0
- package/vendor/node-airtunes2/src/CADebugMacros.h +257 -0
- package/vendor/node-airtunes2/src/CAHostTimeBase.cpp +105 -0
- package/vendor/node-airtunes2/src/CAHostTimeBase.h +224 -0
- package/vendor/node-airtunes2/src/aes_utils.c +864 -0
- package/vendor/node-airtunes2/src/aes_utils.h +24 -0
- package/vendor/node-airtunes2/src/base64.c +136 -0
- package/vendor/node-airtunes2/src/base64.h +42 -0
- package/vendor/node-airtunes2/src/bindings.cc +29 -0
- package/vendor/node-airtunes2/src/codec.cc +170 -0
- 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
|
+
}
|