mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
fix(playlist): keep the M3U player volume in step with the shared bus
The volume handed to each new player instance was a constructor snapshot of localStorage, while the engines persist their own changes straight to that key and never call back. Every channel switch mints a new source revision, which recreates the engine component and re-reads the input, so lowering the volume in the player and zapping snapped playback back to the value the page was opened with — on the live layout as much as on the new movie detail. The signal now re-reads the shared bus per channel. Empty and out-of-range stored values fall back to full volume: Number() maps both null and '' to 0, which would have started a first run muted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e8f9592eca
commit
2930729e80
2 files changed
+82
-7
No files matched your search
@@ -311,4 +311,51 @@ describe('VideoPlayerComponent — M3U movie recognition gate', () => {
|
||||
|
||||
expect(component.showMovieDetail()).toBe(true);
|
||||
});
|
||||
|
||||
describe('volume handed to each new player', () => {
|
||||
afterEach(() => localStorage.removeItem('volume'));
|
||||
|
||||
it('re-reads the shared volume bus on every channel change', () => {
|
||||
localStorage.setItem('volume', '0.4');
|
||||
syncStoreState(movieChannel);
|
||||
fixture.detectChanges();
|
||||
expect(component.volume()).toBe(0.4);
|
||||
|
||||
// The engines persist straight to localStorage and never call
|
||||
// back, so a volume set inside the player is only visible there.
|
||||
localStorage.setItem('volume', '0.15');
|
||||
syncStoreState(liveChannel);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.volume()).toBe(0.15);
|
||||
});
|
||||
|
||||
it.each(['not-a-number', '', ' ', '5', '-1'])(
|
||||
'falls back to full volume for the stored value %p',
|
||||
(stored) => {
|
||||
localStorage.setItem('volume', stored);
|
||||
syncStoreState(movieChannel);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.volume()).toBe(1);
|
||||
}
|
||||
);
|
||||
|
||||
it('falls back to full volume when nothing is stored yet', () => {
|
||||
localStorage.removeItem('volume');
|
||||
syncStoreState(movieChannel);
|
||||
fixture.detectChanges();
|
||||
|
||||
// `Number(null)` is 0 — a first run must not start muted.
|
||||
expect(component.volume()).toBe(1);
|
||||
});
|
||||
|
||||
it('keeps a stored silence', () => {
|
||||
localStorage.setItem('volume', '0');
|
||||
syncStoreState(movieChannel);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.volume()).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
linkedSignal,
|
||||
signal,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
@@ -131,6 +132,23 @@ const M3U_SIDEBAR_MIN_WIDTH = 200;
|
||||
const M3U_SIDEBAR_MAX_WIDTH = 600;
|
||||
const M3U_SIDEBAR_DEFAULT_WIDTH = 460;
|
||||
|
||||
/**
|
||||
* Shared `volume` bus the player engines and the audio player persist to.
|
||||
*
|
||||
* The empty cases must be rejected BEFORE `Number()` sees them: it maps both
|
||||
* `null` (nothing stored yet) and `''` to 0, which would silently start every
|
||||
* first-run playback muted.
|
||||
*/
|
||||
function readStoredVolume(): number {
|
||||
const stored = localStorage.getItem('volume')?.trim();
|
||||
if (!stored) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const parsed = Number(stored);
|
||||
return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : 1;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-video-player',
|
||||
imports: [
|
||||
@@ -456,15 +474,25 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
showChannelNumberOverlay = false;
|
||||
private channelNumberTimeout?: number;
|
||||
|
||||
readonly volume = signal(1);
|
||||
/**
|
||||
* Volume handed to each new player instance, re-read from the shared
|
||||
* `volume` localStorage bus on every channel change.
|
||||
*
|
||||
* The bus is what the engines actually write to (they persist on
|
||||
* `volumechange` and never call back into this component), and every
|
||||
* channel switch mints a new source revision, which recreates the engine
|
||||
* component and re-reads this input. A plain constructor snapshot
|
||||
* therefore snapped playback back to the volume the page was opened
|
||||
* with as soon as the user zapped after adjusting it in the player.
|
||||
* Remote-control writes still `set()` this signal directly and store the
|
||||
* same value, so re-reading the bus per channel agrees with them.
|
||||
*/
|
||||
readonly volume = linkedSignal({
|
||||
source: () => this.activeChannel()?.id ?? null,
|
||||
computation: () => readStoredVolume(),
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Initialize volume from localStorage in constructor
|
||||
const savedVolume = localStorage.getItem('volume');
|
||||
if (savedVolume !== null) {
|
||||
this.volume.set(Number(savedVolume));
|
||||
}
|
||||
|
||||
// React to settings changes
|
||||
effect(() => {
|
||||
this.playerSettings = {
|
||||
|
||||
Reference in new issue
Block a user