Skip to content

Commit 4c22705

Browse files
committed
fix(queue): a stopped output no longer winds the queue back (#360)
Found while measuring the Soloist track boundary: 26 ms after the queue correctly moved to the next track, its index went back to the one that had just finished. The room played track two while the queue said track one — so `next` played the track that was already sounding, a second time, and the queue highlighted the wrong line. That is the "played twice" and the "queue gets out of sync" half of the report, and it has nothing to do with Spotify. The trace named it: the engine session of the finished track is torn down *after* the new one has started, and on its way out it reports itself stopped with its own uri. `updateOutputState` reconciles the queue index to whatever uri an output names, so it dutifully wound the index back onto the track the room had already left. That reconcile exists to follow an output that is playing something of its own choosing — Squeezelite or Cast on a track the zone did not start. An output that has stopped is not on any entry and cannot say where the queue is, so a `stopped` report no longer moves it. The end-of-track forcing just above still reads that same report; only the index is left alone. The regression test fails without the change.
1 parent 38e1781 commit 4c22705

2 files changed

Lines changed: 28 additions & 1 deletion

File tree

src/application/zones/playback/outputStateUpdater.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,13 @@ export function updateOutputState(args: {
5151
ctx.player.updateTiming(duration, duration);
5252
}
5353
}
54-
const matchedIndex = state.uri && ctx.queue.items.length
54+
// Which queue entry an output is on is only ever told by an output that is on one. A `stopped`
55+
// report names the track that has just finished, and the session it comes from is torn down
56+
// *after* the queue has moved to the next one — so reading it as "this is where the queue is"
57+
// wound the index back onto the track the room had already left. Everything then disagreed with
58+
// what was sounding: `next` played the track that was already on (twice in a row, to a listener),
59+
// and the queue highlighted the wrong line. The end-of-track force above still reads it.
60+
const matchedIndex = state.uri && state.status !== 'stopped' && ctx.queue.items.length
5561
? findQueueIndexByUri(ctx.queue.items, state.uri)
5662
: -1;
5763
const session = ctx.player.getSession();

tests/playbackCoordinator.characterization.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,27 @@ test('output URI mismatch is ignored before first audio chunk for local queue',
971971
assert.equal(patches[1]?.patch.qid, 'id-2');
972972
});
973973

974+
test('a stopped output does not wind the queue back onto the track that finished', () => {
975+
// The engine session of a track that has ended is torn down after the queue has already moved
976+
// on, and it reports itself stopped with that track's uri. Read as "this is the current entry",
977+
// it put the queue back on a track the room had left: `next` then replayed what was playing.
978+
const { coordinator, ctx, playbackQueue } = createHarness();
979+
const items = [
980+
makeQueueItem({ title: 'One', audiopath: 'library://track/one', unique_id: 'id-1' }),
981+
makeQueueItem({ title: 'Two', audiopath: 'library://track/two', unique_id: 'id-2' }),
982+
];
983+
playbackQueue.setItems(items, 1);
984+
ctx.queue.authority = 'local';
985+
ctx.inputMode = 'queue';
986+
987+
coordinator.updateOutputState(ctx.id, {
988+
status: 'stopped',
989+
uri: items[0]!.audiopath,
990+
});
991+
992+
assert.equal(ctx.queueController.currentIndex(), 1);
993+
});
994+
974995
test('output stopped near end does not force end timing for controllable radio', () => {
975996
const { coordinator, ctx } = createHarness();
976997
const player = ctx.player as unknown as FakePlayer;

0 commit comments

Comments
 (0)