Skip to content

Commit d44bfee

Browse files
stage
1 parent e015977 commit d44bfee

9 files changed

Lines changed: 166 additions & 4160 deletions

File tree

example_execlient.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ ffmpeg.stderr.on('data', function(data) {
2525
setTimeout(()=>{
2626
const ws = new WebSocket('ws://localhost:8980');
2727
airtunes.stdout.pipe(process.stdout);
28+
airtunes.stderr.pipe(process.stdout);
2829
ws.on('error', console.error);
2930

3031
ws.on('open', function open() {
3132
ws.send(JSON.stringify({"type":"addDevices",
3233
"host":"192.168.100.12",
3334
"args":{"port":7000,
34-
"volume":20, "airplay2": false,
35-
"txt":["cn=0,1,2,3","da=true","et=0,3,5","ft=0x4A7FCA00,0xBC354BD0","sf=0xa0404","md=0,1,2","am=AudioAccessory5,1","pk=lolno","tp=UDP","vn=65537","vs=670.6.2","ov=16.2","vv=2"],
35+
"volume":20, "airplay2": true ,
36+
//"txt":["cn=0,1,2,3","da=true","et=0,3,5","ft=0x4A7FCA00,0xBC354BD0","sf=0xa0404","md=0,1,2","am=AudioAccessory5,1","pk=lolno","tp=UDP","vn=65537","vs=670.6.2","ov=16.2","vv=2"],
37+
"txt":["cn=0,1,2,3","da=true","et=0,3,5","ft=0x4A7FCA00,0xBC354BD0","sf=0x80484","md=0,1,2","am=AudioAccessory5,1","pk=lol","tp=UDP","vn=65537","vs=670.6.2","ov=16.2","vv=2"],
3638
"debug":true,
3739
"forceAlac":false}}))
3840
});

examples/play_stdin.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,24 @@ function ondeviceup(name, host, port, addresses, text, airplay2 = null) {
196196
} catch (e) {}
197197
let host_name = addresses != null && typeof addresses == "object" && addresses.length > 0 ? addresses[0] : typeof addresses == "string" ? addresses : "";
198198

199+
let needPassword = false;
200+
let needPin = false;
201+
let transient = false;
202+
let c = text.filter((u) => String(u).startsWith('sf='))
203+
let statusflags = c[0] ? parseInt(c[0].substring(3)).toString(2).split('') : []
204+
if (c.length == 0) {
205+
c = text.filter((u) => String(u).startsWith('flags='))
206+
statusflags = c[0] ? parseInt(c[0].substring(6)).toString(2).split('') : []
207+
}
208+
if (statusflags != []){
209+
let PasswordRequired = (statusflags[statusflags.length - 1 - 7] == '1')
210+
let PinRequired = (statusflags[statusflags.length - 1 - 3] == '1')
211+
let OneTimePairingRequired = (statusflags[statusflags.length - 1 - 9] == '1')
212+
needPassword = PasswordRequired;
213+
needPin = (PinRequired || OneTimePairingRequired)
214+
transient = (!(PasswordRequired || PinRequired || OneTimePairingRequired)) ?? true
215+
}
216+
199217
if (
200218
castDevices.findIndex((item) => {
201219
return item != null && item.name == shown_name && item.host == host_name && item.host != "Unknown";
@@ -208,6 +226,7 @@ function ondeviceup(name, host, port, addresses, text, airplay2 = null) {
208226
addresses: addresses,
209227
txt: text,
210228
airplay2: airplay2,
229+
needPassword: needPassword,
211230
});
212231
// if (this.devices.indexOf(host_name) === -1) {
213232
// this.devices.push(host_name);

examples/play_stdin_sample.js

Lines changed: 130 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,16 @@
11
const {Worker} = require("worker_threads");
22
var ab2str = require('arraybuffer-to-string')
3+
const mdns = require("mdns-js");
34
var AirTunes = require('../lib/');
4-
// var argv = require('optimist')
5-
// .usage('Usage: $0 --host [host] --port [num] --volume [num] --password [string] --mode [mode] --airplay2 [1/0] --debug [mode] --ft [featuresHexes] --sf [statusFlags] --et [encryptionTypes] --cn [audioCodecs]')
6-
// .default('port', 5002)
7-
// .default('volume', 30)
8-
// .default('ft',"0x7F8AD0,0x38BCF46")
9-
// .default('sf',"0x98404")
10-
// .default('cn',"0,1,2,3")
11-
// .default('et',"0,3,5")
12-
// .default('airplay2',"0")
13-
// .default('forceAlac', false)
14-
// .demand(['host'])
15-
// .argv;
16-
// argv.txt = [
17-
// `cn=${argv.cn}`,
18-
// 'da=true',
19-
// `et=${argv.et}`,
20-
// `ft=${argv.ft}`,
21-
// `sf=${argv.sf}`,
22-
// 'md=0,1,2',
23-
// 'am=AudioAccessory5,1',
24-
// 'pk=lolno',
25-
// 'tp=UDP',
26-
// 'vn=65537',
27-
// 'vs=610.20.41',
28-
// 'ov=15.4.1',
29-
// 'vv=2'
30-
// ];
31-
// console.log('pipe PCM data to play over AirTunes');
32-
// console.log('example: type sample.pcm | node play_stdin.js --host <AirTunes host>\n (yes doesnt work on windows powershell cus Microsoft is stupid, use cmd)');
33-
34-
// Only works on OSX
35-
// airtunes.addCoreAudio();
36-
process.env.UV_THREADPOOL_SIZE = 6;
37-
//console.log('adding device: ' + argv.host + ':' + argv.port);
38-
var airtunes = new AirTunes();
39-
//var device = airtunes.add(argv.host, argv);
40-
41-
42-
// when the device is online, spawn ffmpeg to transcode the file
43-
// device.on('status', function(status) {
44-
// console.log('status: ' + status);
5+
var castDevices = [];
456

46-
// // if(status === 'need_password'){
47-
// // device.setPasscode(argv.password);
48-
// // }
49-
50-
// if(status !== 'ready')
51-
// return;
52-
53-
// if(status == 'ready') {
54-
// }
55-
// // pipe data to AirTunes
56-
// //process.stdin.pipe(airtunes);
57-
// });
7+
process.env.UV_THREADPOOL_SIZE = 6;
588

59-
// Read data from file mirrors.raw
60-
var fs = require('fs');
61-
var file = fs.createReadStream('mirrors.raw');
62-
file.pipe(airtunes);
9+
var airtunes = new AirTunes();
6310

64-
// process.stdin.on('data', function (data) {
65-
// airtunes.write(data);
66-
// });
11+
process.stdin.on('data', function (data) {
12+
airtunes.write(data);
13+
});
6714

6815
// monitor buffer events
6916
airtunes.on('buffer', function(status) {
@@ -81,41 +28,19 @@ airtunes.on('buffer', function(status) {
8128
}
8229
});
8330

84-
// device.on('status', function(status) {
85-
// process.stdin.on('data', function () {
86-
// process.stdin.pipe(airtunes);
87-
// process.stdin.resume();
88-
// });
89-
90-
// });
91-
92-
// device.on('error', function(err) {
93-
// console.log('device error: ' + err);
94-
// process.exit(1);
95-
// });
96-
97-
// setTimeout(function () {
98-
// console.log('stopping');
99-
// airtunes.stopAll(function () {
100-
// console.log('all stopped');
101-
// });
102-
// }, 1000);
103-
104-
// // monitor buffer events
105-
// airtunes.on('buffer', function(status) {
106-
// console.log('buffer ' + status);
107-
108-
// // after the playback ends, give AirTunes some time to finish
109-
// if(status === 'end') {
110-
// console.log('playback ended, waiting for AirTunes devices');
111-
// setTimeout(function() {
112-
// airtunes.stopAll(function() {
113-
// console.log('end');
114-
// process.exit();
115-
// });
116-
// }, 2000);
117-
// }
118-
// });
31+
32+
// monitor buffer events
33+
airtunes.on('buffer', function(status) {
34+
console.log('buffer ' + status);
35+
let status_json = {
36+
type : "bufferStatus",
37+
status: status ?? "",
38+
}
39+
if(worker != null){
40+
worker.postMessage(JSON.stringify(status_json));
41+
}
42+
43+
});
11944

12045
airtunes.on('device', function(key, status, desc) {
12146
let status_json = {
@@ -139,53 +64,60 @@ var func = `
13964
parentPort.postMessage({message: data});
14065
});
14166
parentPort.on("message", data => {
142-
console.log("ass");
14367
ws.send(data);
14468
});
14569
});`;
14670
var worker = new Worker(func, {eval: true});
14771
worker.on("message", (result) => {
14872
parsed_data = JSON.parse(ab2str(result.message));
149-
if (parsed_data.type == "addDevices") {
150-
// Sample data for adding devices:
151-
//'{"type":"addDevices",
152-
// "host":"192.168.3.4",
153-
// "args":{"port":7000,
154-
// "volume":50,
155-
// "password":3000,
156-
// "txt":["tp=UDP","sm=false","sv=false","ek=1","et=0,1","md=0,1,2","cn=0,1","ch=2","ss=16","sr=44100","pw=false","vn=3","txtvers=1"],
157-
// "airplay2":1,
158-
// "debug":true,
159-
// "forceAlac":false}}'
160-
airtunes.add(parsed_data.host, parsed_data.args);
73+
if (parsed_data.type == "scanDevices") {
74+
// Sample data for scanning available devices:
75+
//'{"type":"scanDevices",
76+
// "timeout": 3000}
77+
castDevices = [];
78+
getAvailableDevices();
79+
setTimeout(() => { worker.postMessage(JSON.stringify({
80+
type : "airplayDevices", devices: castDevices}));}, parsed_data.timeout ?? 1000);
81+
} else if (parsed_data.type == "addDevices") {
82+
// Sample data for adding devices:
83+
//'{"type":"addDevices",
84+
// "host":"192.168.3.4",
85+
// "args":{"port":7000,
86+
// "volume":50,
87+
// "password":3000,
88+
// "txt":["tp=UDP","sm=false","sv=false","ek=1","et=0,1","md=0,1,2","cn=0,1","ch=2","ss=16","sr=44100","pw=false","vn=3","txtvers=1"],
89+
// "airplay2":1,
90+
// "debug":true,
91+
// "forceAlac":false}}'
92+
airtunes.add(parsed_data.host, parsed_data.args);
16193
} else if (parsed_data.type == "setVolume"){
16294
// Sample data for setting volume:
16395
// {"type":"setVolume",
16496
// "devicekey": "192.168.3.4:7000",
16597
// "volume":30}
166-
airtunes.setVolume(parsed_data.devicekey, parsed_data.volume,null);
98+
airtunes.setVolume(parsed_data.devicekey, parsed_data.volume,function(){});
16799
} else if (parsed_data.type == "setProgress"){
168100
// Sample data for setting progress:
169101
// {"type":"setProgress",
170102
// "devicekey": "192.168.3.4:7000",
171103
// "progress": 0,
172104
// "duration": 0}
173-
airtunes.setProgress(parsed_data.devicekey, parsed_data.progress, parsed_data.duration,null);
105+
airtunes.setProgress(parsed_data.devicekey, parsed_data.progress, parsed_data.duration,function(){});
174106
} else if (parsed_data.type == "setArtwork"){
175107
// Sample data for setting artwork:
176108
// {"type":"setArtwork",
177109
// "devicekey": "192.168.3.4:7000",
178-
// "contentType" : "image/png";
110+
// "contentType" : "image/png",
179111
// "artwork": "hex data"}
180-
airtunes.setArtwork(parsed_data.devicekey, Buffer.from(parsed_data.artwork,"hex"),null);
112+
airtunes.setArtwork(parsed_data.devicekey, Buffer.from(parsed_data.artwork,"hex"),parsed_data.contentType);
181113
} else if (parsed_data.type == "setTrackInfo"){
182114
// Sample data for setting track info:
183115
// {"type":"setTrackInfo",
184116
// "devicekey": "192.168.3.4:7000",
185117
// "artist": "John Doe",
186118
// "album": "John Doe Album",
187119
// "name": "John Doe Song"}
188-
airtunes.setTrackInfo(parsed_data.devicekey, parsed_data.name, parsed_data.artist, parsed_data.album, parsed_data.name,null);
120+
airtunes.setTrackInfo(parsed_data.devicekey, parsed_data.name, parsed_data.artist, parsed_data.album, function(){});
189121
} else if (parsed_data.type == "setPasscode"){
190122
// Sample data for setting passcode:
191123
// {"type":"setPasscode",
@@ -204,3 +136,88 @@ worker.on("message", (result) => {
204136
}
205137
});
206138

139+
function getAvailableDevices() {
140+
const browser = mdns.createBrowser(mdns.tcp("raop"));
141+
browser.on("ready", browser.discover);
142+
143+
browser.on("update", (service) => {
144+
if (service.addresses && service.fullname && service.fullname.includes("_raop._tcp")) {
145+
// console.log(service.txt)
146+
console.log(
147+
`${service.name} ${service.host}:${service.port} ${service.addresses} ${service.fullname}`
148+
)
149+
let itemname = service.fullname.substring(service.fullname.indexOf("@") + 1, service.fullname.indexOf("._raop._tcp"));
150+
ondeviceup(itemname, service.host, service.port, service.addresses, service.txt);
151+
}
152+
});
153+
154+
const browser2 = mdns.createBrowser(mdns.tcp("airplay"));
155+
browser2.on("ready", browser2.discover);
156+
157+
browser2.on("update", (service) => {
158+
if (service.addresses && service.fullname && service.fullname.includes("_airplay._tcp")) {
159+
// console.log(service.txt)
160+
console.log(
161+
`${service.name} ${service.host}:${service.port} ${service.addresses} ${service.fullname}`
162+
)
163+
let itemname = service.fullname.substring(service.fullname.indexOf("@") + 1, service.fullname.indexOf("._airplay._tcp"));
164+
ondeviceup(itemname, service.host, service.port, service.addresses, service.txt, true);
165+
}
166+
});
167+
168+
}
169+
170+
function ondeviceup(name, host, port, addresses, text, airplay2 = null) {
171+
// console.log(this.castDevices.findIndex((item) => {return (item.name == host.replace(".local","") && item.port == port )}))
172+
173+
let d = "";
174+
let audiook = true;
175+
try {
176+
d = text.filter((u) => String(u).startsWith("features="));
177+
if (d.length == 0) d = text.filter((u) => String(u).startsWith("ft="));
178+
let features_set = d.length > 0 ? d[0].substring(d[0].indexOf("=") + 1).split(",") : [];
179+
let features = [...(features_set.length > 0 ? parseInt(features_set[0]).toString(2).split("") : []), ...(features_set.length > 1 ? parseInt(features_set[1]).toString(2).split("") : [])];
180+
if (features.length > 0) {
181+
audiook = features[features.length - 1 - 9] == "1";
182+
}
183+
} catch (_) {}
184+
if (audiook) {
185+
let shown_name = name;
186+
try {
187+
let model = text.filter((u) => String(u).startsWith("model="));
188+
let manufacturer = text.filter((u) => String(u).startsWith("manufacturer="));
189+
let name1 = text.filter((u) => String(u).startsWith("name="));
190+
if (name1.length > 0) {
191+
shown_name = name1[0].split("=")[1];
192+
} else if (manufacturer.length > 0) {
193+
shown_name = (manufacturer.length > 0 ? manufacturer[0].substring(13) : "") + " " + (model.length > 0 ? model[0].substring(6) : "");
194+
shown_name = shown_name.trim().length > 1 ? shown_name : (host ?? "Unknown").replace(".local", "");
195+
}
196+
} catch (e) {}
197+
let host_name = addresses != null && typeof addresses == "object" && addresses.length > 0 ? addresses[0] : typeof addresses == "string" ? addresses : "";
198+
199+
if (
200+
castDevices.findIndex((item) => {
201+
return item != null && item.name == shown_name && item.host == host_name && item.host != "Unknown";
202+
}) == -1
203+
) {
204+
castDevices.push({
205+
name: shown_name,
206+
host: host_name,
207+
port: port,
208+
addresses: addresses,
209+
txt: text,
210+
airplay2: airplay2,
211+
});
212+
// if (this.devices.indexOf(host_name) === -1) {
213+
// this.devices.push(host_name);
214+
// }
215+
if (shown_name) {
216+
console.log("deviceFound", host_name, shown_name);
217+
}
218+
} else {
219+
console.log("deviceFound (added)", host_name, shown_name);
220+
}
221+
}
222+
}
223+

file.txt

-21.7 MB
Binary file not shown.

lib/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ var Config = {
1010
packets_in_buffer: 200, // increase this buffer protects against network issues
1111
coreaudio_min_level: 5, // if CoreAudio's internal buffer drops too much, inject some silence to raise it
1212
coreaudio_check_period: 2000, // CoreAudio buffer level check period
13-
coreaudio_preload: 1408*50, // ~0.5s of silence pushed to CoreAudio to avoid draining AudioQueue
13+
coreaudio_preload: 352*2*2*50, // ~0.5s of silence pushed to CoreAudio to avoid draining AudioQueue
1414
sampling_rate: 44100, // fixed by AirTunes v2
1515
sync_period: 126, // UDP sync packets are sent to all AirTunes devices regularly
1616
stream_latency: 100, // audio UDP packets are flushed in bursts periodically

lib/device_airtunes.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = "") {
102102
let PinRequired = (this.statusflags[this.statusflags.length - 1 - 3] == '1')
103103
let OneTimePairingRequired = (this.statusflags[this.statusflags.length - 1 - 9] == '1')
104104
console.log('needPss', PasswordRequired, PinRequired, OneTimePairingRequired)
105-
this.needPassword = (PasswordRequired || PinRequired || OneTimePairingRequired)
105+
this.needPassword = PasswordRequired;
106106
this.needPin = (PinRequired || OneTimePairingRequired)
107-
this.transient = (!this.needPassword) ?? true
107+
this.transient = (!(PasswordRequired || PinRequired || OneTimePairingRequired)) ?? true
108108
console.log('needPss', this.needPassword)
109109
}
110110
console.log('transient', this.transient)
@@ -117,10 +117,10 @@ function AirTunesDevice(host, audioOut, options, mode = 0, txt = "") {
117117
this.borkedshp = true;
118118
}
119119
let k = this.txt.filter((u) => String(u).startsWith('am='))
120-
if ((k[0] ?? "").includes("AppleTV3,1") || (k[0] ?? "").includes("AirReceiver3,1") || (k[0] ?? "").includes("AirRecever3,1") || (k[0] ?? "").includes('Shairport Sync')){
120+
if ((k[0] ?? "").includes("AppleTV3,1") || (k[0] ?? "").includes("AirReceiver3,1") || (k[0] ?? "").includes("AirRecever3,1") || (k[0] ?? "").includes('Shairport')){
121121
this.alacEncoding = true
122122
}
123-
if ((k[0] ?? "").includes('Shairport Sync')){
123+
if ((k[0] ?? "").includes('Shairport')){
124124
// shairport sync doesn't support airplay 2 via NTP
125125
this.airplay2 = false
126126
}
@@ -392,7 +392,7 @@ function pcmToALAC(encoder, pcmData, bindings, bindingsok) {
392392
} else {
393393
// I only did the actual computational part, the rest that I didn't do should be realitively simple to do.
394394
let bsize = 352, frames = 352; // Set these to whatever they should be
395-
const p = new Uint8Array(1416); // p = *out;
395+
const p = new Uint8Array((352 * 2 * 2) + 8); // p = *out;
396396
const input = new Uint32Array(pcmData.length / 4);
397397
let j = 0;
398398
for (let i = 0; i < pcmData.length; i+=4) {

0 commit comments

Comments
 (0)