From 4466d0a15c2be9d1a0c23dace95cbd079d228d97 Mon Sep 17 00:00:00 2001 From: rahim Date: Thu, 5 Mar 2026 17:48:59 -0800 Subject: [PATCH] fix(core): auto-unmute on volume change and restore volume on unmute (#752) --- .../dom/store/features/tests/volume.test.ts | 66 +++++++++++++++++++ .../core/src/dom/store/features/volume.ts | 19 +++++- 2 files changed, 84 insertions(+), 1 deletion(-) 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 ad5ee4af..2132b725 100644 --- a/packages/core/src/dom/store/features/tests/volume.test.ts +++ b/packages/core/src/dom/store/features/tests/volume.test.ts @@ -78,6 +78,39 @@ describe('volumeFeature', () => { expect(video.volume).toBe(1); }); + + it('unmutes when setting volume above 0 while muted', async () => { + const video = createMockVideo({ muted: true, volume: 0.5 }); + const store = createStore()(volumeFeature); + store.attach({ media: video, container: null }); + + await store.setVolume(0.7); + + expect(video.volume).toBe(0.7); + expect(video.muted).toBe(false); + }); + + it('does not unmute when setting volume to 0', async () => { + const video = createMockVideo({ muted: true, volume: 0.5 }); + const store = createStore()(volumeFeature); + store.attach({ media: video, container: null }); + + await store.setVolume(0); + + expect(video.volume).toBe(0); + expect(video.muted).toBe(true); + }); + + it('does not change muted when already unmuted', async () => { + const video = createMockVideo({ muted: false, volume: 0.5 }); + const store = createStore()(volumeFeature); + store.attach({ media: video, container: null }); + + await store.setVolume(0.8); + + expect(video.volume).toBe(0.8); + expect(video.muted).toBe(false); + }); }); describe('toggleMuted', () => { @@ -102,6 +135,39 @@ describe('volumeFeature', () => { expect(video.muted).toBe(false); expect(result).toBe(false); }); + + it('restores volume to 0.25 when unmuting at volume 0', async () => { + const video = createMockVideo({ muted: true, volume: 0 }); + const store = createStore()(volumeFeature); + store.attach({ media: video, container: null }); + + await store.toggleMuted(); + + expect(video.muted).toBe(false); + expect(video.volume).toBe(0.25); + }); + + it('preserves volume when unmuting with volume > 0', async () => { + const video = createMockVideo({ muted: true, volume: 0.6 }); + const store = createStore()(volumeFeature); + store.attach({ media: video, container: null }); + + 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); + }); }); }); }); diff --git a/packages/core/src/dom/store/features/volume.ts b/packages/core/src/dom/store/features/volume.ts index 553ee59d..ee1d2ce1 100644 --- a/packages/core/src/dom/store/features/volume.ts +++ b/packages/core/src/dom/store/features/volume.ts @@ -3,6 +3,9 @@ import { listen } from '@videojs/utils/dom'; import type { MediaFeatureAvailability, MediaVolumeState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; +/** Volume to restore when unmuting at zero. */ +const UNMUTE_VOLUME = 0.25; + export const volumeFeature = definePlayerFeature({ name: 'volume', state: ({ target }): MediaVolumeState => ({ @@ -12,13 +15,27 @@ export const volumeFeature = definePlayerFeature({ setVolume(volume: number) { const { media } = target(); - media.volume = Math.max(0, Math.min(1, volume)); + const clamped = Math.max(0, Math.min(1, volume)); + + // Auto-unmute when raising volume above zero. + if (clamped > 0 && media.muted) { + media.muted = false; + } + + media.volume = clamped; return media.volume; }, toggleMuted() { const { media } = target(); + const willUnmute = media.muted; media.muted = !media.muted; + + // Restore a sensible volume when unmuting at zero. + if (willUnmute && media.volume === 0) { + media.volume = UNMUTE_VOLUME; + } + return media.muted; }, }),