Keep Radio Next and Previous from starting playback before Play

The Next and Previous buttons cycled stations with the layer's default
autoplay, so a first press started buffering a broadcaster stream even
though the panel says audio connects only after Play. Stepping now only
selects until a stream is loading, buffering or playing; once one is,
playback follows the new station so the old broadcaster does not keep
playing under the new label.

The compact Next fallback scenario in qa-radio.mjs started from a stopped
stream and relied on the old behaviour, so it now starts from a playing
station.

Fixes HAL-722

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-29 17:36:02 +04:00
1 parent c434664549
commit 1ef9be2248
3 files changed
+52 -2

No files matched your search

+9 -1
View File
@@ -3820,7 +3820,15 @@ async function main() {
radio.stopPlayback();
Array.from({ length: gev.viewer.dataSources.length }, (_, index) => gev.viewer.dataSources.get(index))
.find((item) => item.name === 'Radio stations').clustering.enabled = true;
radio.selectStation('00000000-0000-4000-8000-000000000001', { autoplay: false, focus: false });
// Next only carries playback once a stream is active (HAL-722), so the
// fallback scenario starts from a playing station.
radio.selectStation('00000000-0000-4000-8000-000000000001', { autoplay: true, focus: false });
});
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
await page.evaluate(() => {
const gev = window.__godsEyeView;
const camera = gev.viewer.camera;
const radio = gev.dataManager.layers.get('radio').module;
const pool = gev.styleManager._radioControls._radioTunerPool;
const selectedIndex = pool.findIndex((station) => station.id === radio.getUIState().selected?.id);
window.__qaRadioExpectedPrimary = pool[(selectedIndex + 1) % pool.length];
+5
View File
@@ -338,9 +338,14 @@ export function bindRadioControls() {
const pool = this._radioTunerPool.length
? this._radioTunerPool
: this._radioTunerStations;
// Stepping only selects until the user has pressed Play; once a stream is
// active it follows the selection so the old broadcaster does not keep
// playing under a new station label.
const { audioState } = this.radio.getUIState();
const cycled = this.radio.cycleStation(direction, {
rotate,
stationIds: pool.map((station) => station.id),
autoplay: ['loading', 'buffering', 'playing'].includes(audioState),
origin: 'user',
});
if (!cycled) {
+38 -1
View File
@@ -40,8 +40,11 @@ function fixture() {
const panel = new Element();
const enable = new Element();
const next = new Element();
const prev = new Element();
const calls = [];
const cycleOptions = [];
const subscribers = [];
const uiState = { audioState: 'stopped' };
const radio = {
subscribe(callback) {
subscribers.push(callback);
@@ -51,10 +54,12 @@ function fixture() {
endTuning() {
calls.push('endTuning');
},
cycleStation(direction) {
cycleStation(direction, options) {
calls.push(['cycle', direction]);
cycleOptions.push(options);
return true;
},
getUIState: () => uiState,
};
const actions = {
isRegistered: () => true,
@@ -73,6 +78,7 @@ function fixture() {
_radioPanel: panel,
_radioEnableBtn: enable,
_radioNextBtn: next,
_radioPrevBtn: prev,
},
radio,
actions,
@@ -82,8 +88,11 @@ function fixture() {
controls,
enable,
next,
prev,
actions,
calls,
cycleOptions,
uiState,
subscribers,
document,
cleanup() {
@@ -172,3 +181,31 @@ test('destruction releases an active tuner pointer after revoking listeners', ()
f.cleanup();
}
});
test('Next and Previous select without playing until playback has started', () => {
const f = fixture();
try {
f.controls.connect();
for (const audioState of ['stopped', 'paused', 'error']) {
f.uiState.audioState = audioState;
f.next.dispatchEvent(new Event('click'));
f.prev.dispatchEvent(new Event('click'));
}
assert.deepEqual(
f.cycleOptions.map((options) => options.autoplay),
[false, false, false, false, false, false],
);
f.cycleOptions.length = 0;
for (const audioState of ['loading', 'buffering', 'playing']) {
f.uiState.audioState = audioState;
f.next.dispatchEvent(new Event('click'));
f.prev.dispatchEvent(new Event('click'));
}
assert.deepEqual(
f.cycleOptions.map((options) => options.autoplay),
[true, true, true, true, true, true],
);
} finally {
f.cleanup();
}
});