From 4eee98467dc0a70ebe05cc6667d08e07539ef84d Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sat, 25 Jul 2026 06:48:37 +0300 Subject: [PATCH] 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. --- .../src/features/streaming/ChannelPlayer.tsx | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/frontend/src/features/streaming/ChannelPlayer.tsx b/frontend/src/features/streaming/ChannelPlayer.tsx index 96cc7a6..235989c 100644 --- a/frontend/src/features/streaming/ChannelPlayer.tsx +++ b/frontend/src/features/streaming/ChannelPlayer.tsx @@ -3,6 +3,25 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' 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-плеер линейного канала. Перемотка невозможна: своя минимальная панель без таймлайна, а на * play/после ошибки плеер прыгает к живому краю. Cookie tw_stream уже выдана к монтированию. @@ -19,8 +38,19 @@ export function ChannelPlayer({ const videoRef = useRef(null) const hlsRef = useRef(null) const [playing, setPlaying] = useState(false) - const [muted, setMuted] = useState(true) - const [volume, setVolume] = useState(1) + const [muted, setMuted] = useState(() => readStoredAudio().muted) + 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 hideTimerRef = useRef(null) const overControlsRef = useRef(false) @@ -66,12 +96,25 @@ export function ChannelPlayer({ const src = `/api/channels/${slug}/live.m3u8` 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()) { hls = new Hls({ liveSyncDurationCount: 3, enableWorker: true, lowLatencyMode: false }) hlsRef.current = hls hls.loadSource(src) 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) => { if (data.fatal) { hls?.destroy() @@ -82,7 +125,7 @@ export function ChannelPlayer({ }) } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = src - video.addEventListener('loadedmetadata', () => void video.play().catch(() => undefined)) + video.addEventListener('loadedmetadata', startPlayback) video.addEventListener('error', () => onUnavailable?.()) } else { onUnavailable?.()