mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Keep Radio playback state consistent after a silent Next
Review follow-ups for HAL-722. A Next or Previous that only selects a station now leaves nothing of the previous stream behind: - Selecting another station without playing stops a paused or failed stream, so the panel, Stop, voice summaries and the audio element agree on "stopped". A fallback the caller armed for the new selection is kept. - A silent UI step arms no broadcaster fallback, and a user Play of the selected station drops one left by a cancelled voice step. Voice steps pass `fallback: true` because voice starts the prepared playback later. - A user step to another station emits a `select` playback control, and the realtime voice handoff treats it like Pause and Stop. - A tuner release onto a station the directory no longer has stops the stream the drag paused, publishing one state with the gesture's origin, so the button reads PLAY and a voice handoff is cancelled. - The transport buttons offer Pause while a stream connects, matching what a click does. - The privacy note names every action that connects to a broadcaster (play, finishing tuning, choosing a station on the globe) in all locales. The Next/Previous layer tests live in src/data/radioNextState.test.mjs.
This commit is contained in:
1 parent
b775bfed70
commit
4dd5a313b3
20 files changed
+523
-71
No files matched your search
@@ -73,6 +73,24 @@ export function createSelection({
|
||||
return { filter: layerState._filter, volume: layerState._userVolume };
|
||||
}
|
||||
|
||||
// Selecting another station without playing retires a paused or failed
|
||||
// stream, so no control can resume or report a broadcaster that is no longer
|
||||
// the selection. Stop clears fallbacks; the caller may have armed one for
|
||||
// this selection, so it survives.
|
||||
function releaseStaleRadioStream(stationId) {
|
||||
if (
|
||||
!layerState._audioStationId ||
|
||||
layerState._audioStationId === stationId ||
|
||||
!['paused', 'error'].includes(layerState._audioState)
|
||||
)
|
||||
return;
|
||||
const fallbackId = layerState._playFallbackId;
|
||||
const fallbackFocus = layerState._playFallbackFocus;
|
||||
parts.playback.stopRadioPlayback();
|
||||
layerState._playFallbackId = fallbackId;
|
||||
layerState._playFallbackFocus = fallbackFocus;
|
||||
}
|
||||
|
||||
/** Select a station. Playback occurs only when autoplay is explicitly true. */
|
||||
|
||||
function selectRadioStation(
|
||||
@@ -88,6 +106,7 @@ export function createSelection({
|
||||
if (!parts.interaction.radioPresentationAllowed()) return false;
|
||||
const station = layerState._stationById.get(String(id));
|
||||
if (!station) return false;
|
||||
if (!autoplay) releaseStaleRadioStream(station.id);
|
||||
layerState._tuningUnavailableStationId = null;
|
||||
if (
|
||||
!parts.navigation.radioCameraNavigationOwnsSelection(
|
||||
@@ -104,6 +123,7 @@ export function createSelection({
|
||||
)
|
||||
parts.tuning.endRadioTuning();
|
||||
layerState._cancelledTuningPresentationStation = null;
|
||||
const changed = layerState._selectedId !== station.id;
|
||||
layerState._selectedId = station.id;
|
||||
const generation = ++layerState._selectionGeneration;
|
||||
const sessionGeneration = layerState._sessionGeneration;
|
||||
@@ -122,6 +142,9 @@ export function createSelection({
|
||||
if (focus) parts.navigation.focusStation(station);
|
||||
parts.presentation.emitState();
|
||||
if (autoplay) void parts.playback.playSelectedRadio({ origin, attemptId });
|
||||
// A user step to another station overrides a playback voice prepared.
|
||||
else if (origin === 'user' && changed)
|
||||
parts.presentation.emitPlaybackControl('select', origin);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -133,6 +156,7 @@ export function createSelection({
|
||||
rotate = false,
|
||||
stationIds = null,
|
||||
autoplay = true,
|
||||
fallback = autoplay,
|
||||
origin = 'programmatic',
|
||||
} = {},
|
||||
) {
|
||||
@@ -159,25 +183,29 @@ export function createSelection({
|
||||
current < 0
|
||||
? 0
|
||||
: (current + (direction < 0 ? -1 : 1) + ranked.length) % ranked.length;
|
||||
// Voice defers playback, so its silent step needs a fallback; UI steps do not.
|
||||
layerState._playFallbackId =
|
||||
ranked.length > 1 ? ranked[(nextIndex + 1) % ranked.length].id : null;
|
||||
fallback && ranked.length > 1
|
||||
? ranked[(nextIndex + 1) % ranked.length].id
|
||||
: null;
|
||||
const rotationCameraState = rotate
|
||||
? parts.navigation.radioCameraState()
|
||||
: null;
|
||||
const rotationNavigation = rotate
|
||||
? parts.navigation.beginRadioCameraNavigation(rotationCameraState)
|
||||
: null;
|
||||
layerState._playFallbackFocus = rotate
|
||||
? (fallbackStation) => {
|
||||
parts.navigation.rotateRadioStationIntoView(
|
||||
fallbackStation,
|
||||
0.65,
|
||||
rotationCameraState,
|
||||
rotationNavigation,
|
||||
);
|
||||
return rotationNavigation;
|
||||
}
|
||||
: false;
|
||||
layerState._playFallbackFocus =
|
||||
fallback && rotate
|
||||
? (fallbackStation) => {
|
||||
parts.navigation.rotateRadioStationIntoView(
|
||||
fallbackStation,
|
||||
0.65,
|
||||
rotationCameraState,
|
||||
rotationNavigation,
|
||||
);
|
||||
return rotationNavigation;
|
||||
}
|
||||
: false;
|
||||
const station = ranked[nextIndex];
|
||||
if (rotate)
|
||||
parts.navigation.rotateRadioStationIntoView(
|
||||
|
||||
Reference in new issue
Block a user