Skip to content

Commit 49e5548

Browse files
committed
fix(airplay): a receiver on the NTP lane plays, and a failed setup lets go
Switching to the NTP lane opens a second connection and closes the first, then the rest of the setup carried on addressing the closed one -- so the stream setup failed with "RTSP connection is not open" and the device stayed silent. Every receiver that asks for that lane was affected; a BeoSound Shape is one. The retry that followed leaked: the sender's two UDP sockets are bound before the stream is set up and only a sender that reaches construction ever closes them, so each failed attempt left a pair behind. Measured against a Shape, that ran from 33 sockets to over 100 in two minutes.
1 parent a96b1de commit 49e5548

1 file changed

Lines changed: 23 additions & 8 deletions

File tree

src/adapters/outputs/airplay/ap2Sender.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ export class Ap2Sender implements AirplaySender {
367367
}
368368

369369
try {
370-
const connection = await AirPlayConnection.open({
370+
let connection = await AirPlayConnection.open({
371371
host: this.config.host,
372372
...(this.config.port !== undefined ? { port: this.config.port } : {}),
373373
...(this.config.password !== undefined ? { password: this.config.password } : {}),
@@ -400,21 +400,36 @@ export class Ap2Sender implements AirplaySender {
400400
onEvent: (event) => this.handleSessionEvent(event),
401401
});
402402
this.connection = ntpConnection;
403+
// Everything past here talks to the receiver, and the connection it was
404+
// reached on just changed. Keeping the old one in hand leaves the rest
405+
// of the setup addressing a socket that was closed two lines ago.
406+
connection = ntpConnection;
403407
session = await ntpConnection.setupSession(this.config.name ?? 'sonn', {
404408
timing: 'ntp',
405409
timingPort,
406410
});
407411
this.timing = 'ntp';
408412
}
409413

414+
// Bound before the stream is set up, so they have to be released by hand
415+
// when anything after this throws: only a sender that got as far as being
416+
// constructed will ever close them itself, and a retry loop that leaks two
417+
// sockets a go eats the process.
410418
const sockets = await RealtimeSender.bindSockets();
411-
const stream = await setupRealtimeStream(connection.rtsp, connection.sessionUrl, {
412-
audioKey: connection.hap.sharedSecret,
413-
localDataPort: sockets.dataPort,
414-
localControlPort: sockets.controlPort,
415-
streamConnectionId: Math.floor(Math.random() * 0x7fff_ffff),
416-
});
417-
await sendVolume(connection.rtsp, connection.sessionUrl, this.currentVolume);
419+
let stream;
420+
try {
421+
stream = await setupRealtimeStream(connection.rtsp, connection.sessionUrl, {
422+
audioKey: connection.hap.sharedSecret,
423+
localDataPort: sockets.dataPort,
424+
localControlPort: sockets.controlPort,
425+
streamConnectionId: Math.floor(Math.random() * 0x7fff_ffff),
426+
});
427+
await sendVolume(connection.rtsp, connection.sessionUrl, this.currentVolume);
428+
} catch (err) {
429+
sockets.data.close();
430+
sockets.control.close();
431+
throw err;
432+
}
418433

419434
this.sender = new RealtimeSender(
420435
{

0 commit comments

Comments
 (0)