diff --git a/packages/core/src/core/ui/mute-button/mute-button-core.ts b/packages/core/src/core/ui/mute-button/mute-button-core.ts index 1cdc702f..92d39e33 100644 --- a/packages/core/src/core/ui/mute-button/mute-button-core.ts +++ b/packages/core/src/core/ui/mute-button/mute-button-core.ts @@ -68,7 +68,7 @@ export class MuteButtonCore { getState(): MuteButtonState { const media = this.#media!; return { - muted: media.muted, + muted: media.muted || media.volume === 0, volumeLevel: getVolumeLevel(media), }; } diff --git a/packages/core/src/core/ui/mute-button/tests/mute-button-core.test.ts b/packages/core/src/core/ui/mute-button/tests/mute-button-core.test.ts index 23d25ef4..630155c4 100644 --- a/packages/core/src/core/ui/mute-button/tests/mute-button-core.test.ts +++ b/packages/core/src/core/ui/mute-button/tests/mute-button-core.test.ts @@ -50,6 +50,15 @@ describe('MuteButtonCore', () => { expect(core.getState().volumeLevel).toBe('off'); }); + it('derives muted as true when volume is 0 and not muted', () => { + const core = new MuteButtonCore(); + core.setMedia(createMediaState({ volume: 0, muted: false })); + const state = core.getState(); + + expect(state.muted).toBe(true); + expect(state.volumeLevel).toBe('off'); + }); + it('returns low when volume < 0.5', () => { const core = new MuteButtonCore(); core.setMedia(createMediaState({ volume: 0.3 })); diff --git a/packages/core/src/core/ui/volume-slider/tests/volume-slider-core.test.ts b/packages/core/src/core/ui/volume-slider/tests/volume-slider-core.test.ts index 7c06adeb..c9a42ca6 100644 --- a/packages/core/src/core/ui/volume-slider/tests/volume-slider-core.test.ts +++ b/packages/core/src/core/ui/volume-slider/tests/volume-slider-core.test.ts @@ -105,6 +105,16 @@ describe('VolumeSliderCore', () => { expect(state.muted).toBe(false); }); + + it('derives muted as true when volume is 0 and not muted', () => { + const core = new VolumeSliderCore(); + core.setInput(createInput()); + core.setMedia(createMediaState({ volume: 0, muted: false })); + const state = core.getState(); + + expect(state.muted).toBe(true); + expect(state.fillPercent).toBe(0); + }); }); describe('getAttrs', () => { @@ -150,14 +160,14 @@ describe('VolumeSliderCore', () => { expect(attrs['aria-label']).toBe('Audio'); }); - it('shows 0 percent when volume is 0', () => { + it('shows 0 percent muted when volume is 0', () => { const core = new VolumeSliderCore(); core.setInput(createInput()); core.setMedia(createMediaState({ volume: 0 })); const state = core.getState(); const attrs = core.getAttrs(state); - expect(attrs['aria-valuetext']).toBe('0 percent'); + expect(attrs['aria-valuetext']).toBe('0 percent, muted'); }); }); diff --git a/packages/core/src/core/ui/volume-slider/volume-slider-core.ts b/packages/core/src/core/ui/volume-slider/volume-slider-core.ts index 239155a2..5236a60f 100644 --- a/packages/core/src/core/ui/volume-slider/volume-slider-core.ts +++ b/packages/core/src/core/ui/volume-slider/volume-slider-core.ts @@ -40,6 +40,7 @@ export class VolumeSliderCore extends SliderCore { getState(): VolumeSliderState { const media = this.#media!; const { volume, muted } = media; + const effectivelyMuted = muted || volume === 0; const { dragging, dragPercent } = this.input; const volumePercent = volume * 100; const value = dragging ? this.valueFromPercent(dragPercent) : volumePercent; @@ -47,9 +48,9 @@ export class VolumeSliderCore extends SliderCore { return { ...base, - fillPercent: muted ? 0 : base.fillPercent, + fillPercent: effectivelyMuted ? 0 : base.fillPercent, volume, - muted, + muted: effectivelyMuted, }; } diff --git a/packages/core/src/dom/store/features/tests/volume.test.ts b/packages/core/src/dom/store/features/tests/volume.test.ts index 2132b725..498e2c85 100644 --- a/packages/core/src/dom/store/features/tests/volume.test.ts +++ b/packages/core/src/dom/store/features/tests/volume.test.ts @@ -114,25 +114,27 @@ describe('volumeFeature', () => { }); describe('toggleMuted', () => { - it('toggles mute from false to true', async () => { - const video = createMockVideo({ muted: false }); + it('mutes when unmuted with volume > 0', async () => { + const video = createMockVideo({ muted: false, volume: 0.8 }); const store = createStore()(volumeFeature); store.attach({ media: video, container: null }); const result = await store.toggleMuted(); expect(video.muted).toBe(true); + expect(video.volume).toBe(0.8); expect(result).toBe(true); }); - it('toggles mute from true to false', async () => { - const video = createMockVideo({ muted: true }); + it('unmutes when muted with volume > 0', async () => { + const video = createMockVideo({ muted: true, volume: 0.6 }); const store = createStore()(volumeFeature); store.attach({ media: video, container: null }); const result = await store.toggleMuted(); expect(video.muted).toBe(false); + expect(video.volume).toBe(0.6); expect(result).toBe(false); }); @@ -147,26 +149,16 @@ describe('volumeFeature', () => { expect(video.volume).toBe(0.25); }); - it('preserves volume when unmuting with volume > 0', async () => { - const video = createMockVideo({ muted: true, volume: 0.6 }); + it('unmutes and restores volume when volume is 0 and not muted', async () => { + const video = createMockVideo({ muted: false, volume: 0 }); const store = createStore()(volumeFeature); store.attach({ media: video, container: null }); - await store.toggleMuted(); + const result = await store.toggleMuted(); expect(video.muted).toBe(false); - expect(video.volume).toBe(0.6); - }); - - it('does not change volume when muting', async () => { - const video = createMockVideo({ muted: false, volume: 0.8 }); - const store = createStore()(volumeFeature); - store.attach({ media: video, container: null }); - - await store.toggleMuted(); - - expect(video.muted).toBe(true); - expect(video.volume).toBe(0.8); + expect(video.volume).toBe(0.25); + expect(result).toBe(false); }); }); }); diff --git a/packages/core/src/dom/store/features/volume.ts b/packages/core/src/dom/store/features/volume.ts index ee1d2ce1..22409afc 100644 --- a/packages/core/src/dom/store/features/volume.ts +++ b/packages/core/src/dom/store/features/volume.ts @@ -28,12 +28,14 @@ export const volumeFeature = definePlayerFeature({ toggleMuted() { const { media } = target(); - const willUnmute = media.muted; - media.muted = !media.muted; + const effectivelyMuted = media.muted || media.volume === 0; - // Restore a sensible volume when unmuting at zero. - if (willUnmute && media.volume === 0) { - media.volume = UNMUTE_VOLUME; + if (effectivelyMuted) { + media.muted = false; + // Restore a sensible volume when unmuting at zero. + if (media.volume === 0) media.volume = UNMUTE_VOLUME; + } else { + media.muted = true; } return media.muted;