diff --git a/client/public/version.js b/client/public/version.js index 033c289..da77a6e 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.25 R234"; +window.CHGRID_WEB_VERSION = "2026.02.25 R235"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/audio/itemEmitRuntime.ts b/client/src/audio/itemEmitRuntime.ts index be9efaf..efab08e 100644 --- a/client/src/audio/itemEmitRuntime.ts +++ b/client/src/audio/itemEmitRuntime.ts @@ -30,6 +30,8 @@ const UNSUBSCRIBE_HYSTERESIS_SQUARES = 8; const SPATIAL_RAMP_SECONDS = 0.2; const SPATIAL_TIME_CONSTANT_SECONDS = SPATIAL_RAMP_SECONDS / 3; const STREAM_PLAY_RETRY_MS = 5000; +const STREAM_PLAY_MAX_RETRIES = 6; +const STREAM_PLAY_RESET_COOLDOWN_MS = 60000; /** Maps a 0-100 speed control to playback-rate range used by emitted audio. */ function resolveEmitPlaybackRate(raw: unknown): number { @@ -67,6 +69,7 @@ export class ItemEmitRuntime { private readonly outputs = new Map(); private readonly pendingEmitStarts = new Set(); private readonly nextEmitStartAtMs = new Map(); + private readonly emitStartFailureCount = new Map(); private layerEnabled = true; private listenerPositions: Array<{ x: number; y: number }> = []; @@ -89,6 +92,7 @@ export class ItemEmitRuntime { this.outputs.delete(itemId); this.pendingEmitStarts.delete(itemId); this.nextEmitStartAtMs.delete(itemId); + this.emitStartFailureCount.delete(itemId); } cleanupAll(): void { @@ -278,8 +282,16 @@ export class ItemEmitRuntime { .play() .then(() => { this.nextEmitStartAtMs.delete(itemId); + this.emitStartFailureCount.delete(itemId); }) .catch(() => { + const failures = (this.emitStartFailureCount.get(itemId) ?? 0) + 1; + if (failures >= STREAM_PLAY_MAX_RETRIES) { + this.emitStartFailureCount.set(itemId, 0); + this.nextEmitStartAtMs.set(itemId, Date.now() + STREAM_PLAY_RESET_COOLDOWN_MS); + return; + } + this.emitStartFailureCount.set(itemId, failures); this.nextEmitStartAtMs.set(itemId, Date.now() + STREAM_PLAY_RETRY_MS); }) .finally(() => { diff --git a/client/src/audio/radioStationRuntime.ts b/client/src/audio/radioStationRuntime.ts index aba4293..392ea99 100644 --- a/client/src/audio/radioStationRuntime.ts +++ b/client/src/audio/radioStationRuntime.ts @@ -168,12 +168,15 @@ const UNSUBSCRIBE_HYSTERESIS_SQUARES = 8; const SPATIAL_RAMP_SECONDS = 0.2; const SPATIAL_TIME_CONSTANT_SECONDS = SPATIAL_RAMP_SECONDS / 3; const STREAM_PLAY_RETRY_MS = 5000; +const STREAM_PLAY_MAX_RETRIES = 6; +const STREAM_PLAY_RESET_COOLDOWN_MS = 60000; export class RadioStationRuntime { private readonly sharedRadioSources = new Map(); private readonly itemRadioOutputs = new Map(); private readonly pendingSharedStarts = new Set(); private readonly nextSharedStartAtMs = new Map(); + private readonly sharedStartFailureCount = new Map(); private layerEnabled = true; private listenerPositions: Array<{ x: number; y: number }> = []; @@ -339,6 +342,7 @@ export class RadioStationRuntime { this.sharedRadioSources.delete(streamUrl); this.pendingSharedStarts.delete(streamUrl); this.nextSharedStartAtMs.delete(streamUrl); + this.sharedStartFailureCount.delete(streamUrl); } private getOrCreateSharedSource(streamUrl: string): SharedRadioSource | null { @@ -390,8 +394,16 @@ export class RadioStationRuntime { .play() .then(() => { this.nextSharedStartAtMs.delete(shared.streamUrl); + this.sharedStartFailureCount.delete(shared.streamUrl); }) .catch(() => { + const failures = (this.sharedStartFailureCount.get(shared.streamUrl) ?? 0) + 1; + if (failures >= STREAM_PLAY_MAX_RETRIES) { + this.sharedStartFailureCount.set(shared.streamUrl, 0); + this.nextSharedStartAtMs.set(shared.streamUrl, Date.now() + STREAM_PLAY_RESET_COOLDOWN_MS); + return; + } + this.sharedStartFailureCount.set(shared.streamUrl, failures); this.nextSharedStartAtMs.set(shared.streamUrl, Date.now() + STREAM_PLAY_RETRY_MS); }) .finally(() => {