mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
fix(playback): restore audible volume when M unmutes at zero volume
Addresses the Codex review finding on #1398: after arrowing the volume down to zero (which mutes), M flipped muted off while leaving the volume at 0, so the player looked unmuted but stayed silent — in all three legacy engine adapters. Mirror the shared controls' ControlsVolume semantics with a per-adapter LegacyMuteMemory: muting remembers the audible volume, and unmuting while the volume sits at zero restores it, with the same 0.5 fallback when nothing was remembered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014h2cZi5DcFSbcmV7WgB6qB
This commit is contained in:
8 files changed
+136
-8
No files matched your search
@@ -362,7 +362,10 @@ Shared legacy rules: seek is gated on authoritative `isLive` plus a finite,
|
||||
positive duration (ArtPlayer gates on `art.duration`, the same value its seek
|
||||
setter clamps against, so an unknown duration never jumps to zero); volume
|
||||
steps by ±5% and syncs muted state the way `applyVideoVolume` does (raising
|
||||
out of mute unmutes, reaching zero mutes); `isAvailable` is the host's
|
||||
out of mute unmutes, reaching zero mutes); M mirrors `ControlsVolume`'s mute
|
||||
memory through `LegacyMuteMemory` — muting remembers the audible volume, and
|
||||
unmuting while the volume sits at zero restores it (same 0.5 fallback), so M
|
||||
can never leave the player silently "unmuted"; `isAvailable` is the host's
|
||||
`interactionEnabled`, so a visible playback diagnostic disables the keys; and
|
||||
Escape defaults to a no-op without consuming the key, because the vendor
|
||||
chrome owns its own overlays. Instances attach in the component's legacy
|
||||
|
||||
@@ -85,8 +85,34 @@ describe('attachArtPlayerLegacyShortcuts', () => {
|
||||
it('toggles mute with M', () => {
|
||||
dispatchKey('m');
|
||||
expect(art.muted).toBe(true);
|
||||
expect(art.volume).toBe(0.5);
|
||||
dispatchKey('m');
|
||||
expect(art.muted).toBe(false);
|
||||
expect(art.volume).toBe(0.5);
|
||||
});
|
||||
|
||||
it('restores an audible volume when unmuting from zero', () => {
|
||||
art.volume = 0.03;
|
||||
dispatchKey('ArrowDown');
|
||||
expect(art.volume).toBe(0);
|
||||
expect(art.muted).toBe(true);
|
||||
|
||||
dispatchKey('m');
|
||||
expect(art.muted).toBe(false);
|
||||
expect(art.volume).toBe(0.5);
|
||||
});
|
||||
|
||||
it('restores the remembered volume when unmuting after mute zeroed out', () => {
|
||||
art.volume = 0.7;
|
||||
dispatchKey('m');
|
||||
expect(art.muted).toBe(true);
|
||||
|
||||
dispatchKey('ArrowDown');
|
||||
expect(art.volume).toBe(0);
|
||||
|
||||
dispatchKey('m');
|
||||
expect(art.muted).toBe(false);
|
||||
expect(art.volume).toBe(0.7);
|
||||
});
|
||||
|
||||
it('toggles the vendor fullscreen with F', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Artplayer from 'artplayer';
|
||||
import { LegacyPlayerShortcuts } from '../player-controls';
|
||||
import { LegacyMuteMemory, LegacyPlayerShortcuts } from '../player-controls';
|
||||
|
||||
export interface ArtPlayerLegacyShortcutOptions {
|
||||
player: () => Artplayer | null;
|
||||
@@ -21,6 +21,7 @@ export function attachArtPlayerLegacyShortcuts(
|
||||
options: ArtPlayerLegacyShortcutOptions
|
||||
): LegacyPlayerShortcuts {
|
||||
const shortcuts = new LegacyPlayerShortcuts();
|
||||
const muteMemory = new LegacyMuteMemory();
|
||||
shortcuts.attach({
|
||||
isAvailable: options.isAvailable,
|
||||
hostElement: options.hostElement,
|
||||
@@ -71,8 +72,15 @@ export function attachArtPlayerLegacyShortcuts(
|
||||
},
|
||||
toggleMute: () => {
|
||||
const player = options.player();
|
||||
if (player) {
|
||||
player.muted = !player.muted;
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
if (player.muted) {
|
||||
player.volume = muteMemory.unmuteVolume(player.volume);
|
||||
player.muted = false;
|
||||
} else {
|
||||
muteMemory.rememberIfAudible(player.volume);
|
||||
player.muted = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -85,8 +85,34 @@ describe('attachHtmlVideoLegacyShortcuts', () => {
|
||||
it('toggles mute with M', () => {
|
||||
dispatchKey('m');
|
||||
expect(video.muted).toBe(true);
|
||||
expect(video.volume).toBe(0.5);
|
||||
dispatchKey('m');
|
||||
expect(video.muted).toBe(false);
|
||||
expect(video.volume).toBe(0.5);
|
||||
});
|
||||
|
||||
it('restores an audible volume when unmuting from zero', () => {
|
||||
video.volume = 0.03;
|
||||
dispatchKey('ArrowDown');
|
||||
expect(video.volume).toBe(0);
|
||||
expect(video.muted).toBe(true);
|
||||
|
||||
dispatchKey('m');
|
||||
expect(video.muted).toBe(false);
|
||||
expect(video.volume).toBe(0.5);
|
||||
});
|
||||
|
||||
it('restores the remembered volume when unmuting after mute zeroed out', () => {
|
||||
video.volume = 0.7;
|
||||
dispatchKey('m');
|
||||
expect(video.muted).toBe(true);
|
||||
|
||||
dispatchKey('ArrowDown');
|
||||
expect(video.volume).toBe(0);
|
||||
|
||||
dispatchKey('m');
|
||||
expect(video.muted).toBe(false);
|
||||
expect(video.volume).toBe(0.7);
|
||||
});
|
||||
|
||||
it('fullscreens the video element itself with F', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LegacyPlayerShortcuts } from '../player-controls';
|
||||
import { LegacyMuteMemory, LegacyPlayerShortcuts } from '../player-controls';
|
||||
import {
|
||||
applyVideoCurrentTime,
|
||||
applyVideoVolume,
|
||||
@@ -21,6 +21,7 @@ export function attachHtmlVideoLegacyShortcuts(
|
||||
options: HtmlVideoLegacyShortcutOptions
|
||||
): LegacyPlayerShortcuts {
|
||||
const shortcuts = new LegacyPlayerShortcuts();
|
||||
const muteMemory = new LegacyMuteMemory();
|
||||
shortcuts.attach({
|
||||
isAvailable: options.isAvailable,
|
||||
hostElement: options.hostElement,
|
||||
@@ -55,7 +56,12 @@ export function attachHtmlVideoLegacyShortcuts(
|
||||
},
|
||||
toggleMute: () => {
|
||||
const video = options.video();
|
||||
video.muted = !video.muted;
|
||||
if (video.muted) {
|
||||
applyVideoVolume(video, muteMemory.unmuteVolume(video.volume));
|
||||
} else {
|
||||
muteMemory.rememberIfAudible(video.volume);
|
||||
video.muted = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
return shortcuts;
|
||||
|
||||
@@ -59,3 +59,26 @@ export class LegacyPlayerShortcuts {
|
||||
this.shortcuts.detach();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute memory for the legacy engine adapters, mirroring the shared controls'
|
||||
* `ControlsVolume`: muting remembers the audible volume, and unmuting while
|
||||
* the volume sits at zero restores it (same 0.5 fallback), so M can never
|
||||
* leave the player silently "unmuted".
|
||||
*/
|
||||
export class LegacyMuteMemory {
|
||||
private lastAudibleVolume: number | null = null;
|
||||
|
||||
rememberIfAudible(volume: number): void {
|
||||
if (Number.isFinite(volume) && volume > 0) {
|
||||
this.lastAudibleVolume = volume;
|
||||
}
|
||||
}
|
||||
|
||||
unmuteVolume(currentVolume: number): number {
|
||||
if (Number.isFinite(currentVolume) && currentVolume > 0) {
|
||||
return currentVolume;
|
||||
}
|
||||
return this.lastAudibleVolume ?? 0.5;
|
||||
}
|
||||
}
|
||||
@@ -124,8 +124,34 @@ describe('attachVjsLegacyShortcuts', () => {
|
||||
it('toggles mute with M', () => {
|
||||
dispatchKey('m');
|
||||
expect(mock.mutedValue).toBe(true);
|
||||
expect(mock.volumeValue).toBe(0.5);
|
||||
dispatchKey('m');
|
||||
expect(mock.mutedValue).toBe(false);
|
||||
expect(mock.volumeValue).toBe(0.5);
|
||||
});
|
||||
|
||||
it('restores an audible volume when unmuting from zero', () => {
|
||||
mock.volumeValue = 0.03;
|
||||
dispatchKey('ArrowDown');
|
||||
expect(mock.volumeValue).toBe(0);
|
||||
expect(mock.mutedValue).toBe(true);
|
||||
|
||||
dispatchKey('m');
|
||||
expect(mock.mutedValue).toBe(false);
|
||||
expect(mock.volumeValue).toBe(0.5);
|
||||
});
|
||||
|
||||
it('restores the remembered volume when unmuting after mute zeroed out', () => {
|
||||
mock.volumeValue = 0.7;
|
||||
dispatchKey('m');
|
||||
expect(mock.mutedValue).toBe(true);
|
||||
|
||||
dispatchKey('ArrowDown');
|
||||
expect(mock.volumeValue).toBe(0);
|
||||
|
||||
dispatchKey('m');
|
||||
expect(mock.mutedValue).toBe(false);
|
||||
expect(mock.volumeValue).toBe(0.7);
|
||||
});
|
||||
|
||||
it('toggles the vendor fullscreen with F', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LegacyPlayerShortcuts } from '../player-controls';
|
||||
import { LegacyMuteMemory, LegacyPlayerShortcuts } from '../player-controls';
|
||||
import type { VideoJsPlayer } from './vjs-player.types';
|
||||
|
||||
export interface VjsLegacyShortcutOptions {
|
||||
@@ -17,6 +17,7 @@ export function attachVjsLegacyShortcuts(
|
||||
options: VjsLegacyShortcutOptions
|
||||
): LegacyPlayerShortcuts {
|
||||
const shortcuts = new LegacyPlayerShortcuts();
|
||||
const muteMemory = new LegacyMuteMemory();
|
||||
shortcuts.attach({
|
||||
isAvailable: options.isAvailable,
|
||||
hostElement: options.hostElement,
|
||||
@@ -74,7 +75,16 @@ export function attachVjsLegacyShortcuts(
|
||||
},
|
||||
toggleMute: () => {
|
||||
const player = options.player();
|
||||
player?.muted(!player.muted());
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
if (player.muted()) {
|
||||
player.volume(muteMemory.unmuteVolume(player.volume() ?? 0));
|
||||
player.muted(false);
|
||||
} else {
|
||||
muteMemory.rememberIfAudible(player.volume() ?? 0);
|
||||
player.muted(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
return shortcuts;
|
||||
|
||||
Reference in new issue
Block a user