Implement audio settings persistence in ChannelPlayer: add functionality to read and store volume and mute state in localStorage, ensuring user preferences are retained across sessions. Update playback logic to respect saved audio settings and handle autoplay restrictions gracefully.

This commit is contained in:
Leonid Pershin
2026-07-25 06:48:37 +03:00
parent 19013a61dc
commit 4eee98467d
@@ -3,6 +3,25 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Maximize, Pause, Play, Volume2, VolumeX } from 'lucide-react' import { Maximize, Pause, Play, Volume2, VolumeX } from 'lucide-react'
const STORAGE_KEY = 'tw:player'
/** Читает сохранённые громкость/mute из localStorage (с валидацией и дефолтами). */
function readStoredAudio(): { volume: number; muted: boolean } {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) {
const parsed = JSON.parse(raw) as { volume?: unknown; muted?: unknown }
const volume =
typeof parsed.volume === 'number' ? Math.min(1, Math.max(0, parsed.volume)) : 1
const muted = typeof parsed.muted === 'boolean' ? parsed.muted : true
return { volume, muted }
}
} catch {
/* недоступен/битый localStorage — дефолты */
}
return { volume: 1, muted: true }
}
/** /**
* HLS-плеер линейного канала. Перемотка невозможна: своя минимальная панель без таймлайна, а на * HLS-плеер линейного канала. Перемотка невозможна: своя минимальная панель без таймлайна, а на
* play/после ошибки плеер прыгает к живому краю. Cookie tw_stream уже выдана к монтированию. * play/после ошибки плеер прыгает к живому краю. Cookie tw_stream уже выдана к монтированию.
@@ -19,8 +38,19 @@ export function ChannelPlayer({
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const hlsRef = useRef<Hls | null>(null) const hlsRef = useRef<Hls | null>(null)
const [playing, setPlaying] = useState(false) const [playing, setPlaying] = useState(false)
const [muted, setMuted] = useState(true) const [muted, setMuted] = useState(() => readStoredAudio().muted)
const [volume, setVolume] = useState(1) const [volume, setVolume] = useState(() => readStoredAudio().volume)
// Последние настройки звука без пересоздания HLS-эффекта + сохранение в localStorage.
const audioRef = useRef({ volume, muted })
useEffect(() => {
audioRef.current = { volume, muted }
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ volume, muted }))
} catch {
/* localStorage недоступен — не критично */
}
}, [volume, muted])
const [controlsVisible, setControlsVisible] = useState(true) const [controlsVisible, setControlsVisible] = useState(true)
const hideTimerRef = useRef<number | null>(null) const hideTimerRef = useRef<number | null>(null)
const overControlsRef = useRef(false) const overControlsRef = useRef(false)
@@ -66,12 +96,25 @@ export function ChannelPlayer({
const src = `/api/channels/${slug}/live.m3u8` const src = `/api/channels/${slug}/live.m3u8`
let hls: Hls | null = null let hls: Hls | null = null
// Восстанавливаем сохранённую громкость/mute и запускаем; если браузер блокирует автоплей со
// звуком — откатываемся на воспроизведение без звука.
const startPlayback = () => {
const audio = audioRef.current
video.volume = audio.volume
video.muted = audio.muted
video.play().catch(() => {
video.muted = true
setMuted(true)
void video.play().catch(() => undefined)
})
}
if (Hls.isSupported()) { if (Hls.isSupported()) {
hls = new Hls({ liveSyncDurationCount: 3, enableWorker: true, lowLatencyMode: false }) hls = new Hls({ liveSyncDurationCount: 3, enableWorker: true, lowLatencyMode: false })
hlsRef.current = hls hlsRef.current = hls
hls.loadSource(src) hls.loadSource(src)
hls.attachMedia(video) hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => void video.play().catch(() => undefined)) hls.on(Hls.Events.MANIFEST_PARSED, startPlayback)
hls.on(Hls.Events.ERROR, (_event, data) => { hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) { if (data.fatal) {
hls?.destroy() hls?.destroy()
@@ -82,7 +125,7 @@ export function ChannelPlayer({
}) })
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = src video.src = src
video.addEventListener('loadedmetadata', () => void video.play().catch(() => undefined)) video.addEventListener('loadedmetadata', startPlayback)
video.addEventListener('error', () => onUnavailable?.()) video.addEventListener('error', () => onUnavailable?.())
} else { } else {
onUnavailable?.() onUnavailable?.()