Files
player/src/contexts/PlayerContext.tsx
T
hibna 52ca1ef6c2 Add live broadcast detection and UI indicator
Introduces isLiveBroadcast to VideoState and PlayerContext to detect live streams (duration is Infinity or 0). Updates ControlsLayer to show a 'LIVE' badge and hide progress/time for live broadcasts. Adjusts VideoElement to set and update isLiveBroadcast on metadata and duration changes. Adds related CSS for the live indicator and improves ESLint config for unused vars.
2025-11-04 06:52:24 +03:00

201 lines
5.3 KiB
TypeScript

import React, { createContext, useContext, useRef, useState, useCallback } from 'react'
import type { PlayerContextValue, VideoState, UIState, PlayerSettings, AudioTrack } from '../types'
import type { Translations } from '../i18n'
import { getTranslations, detectBrowserLanguage } from '../i18n'
type SelectedQuality = PlayerSettings['quality']
type SelectedSubtitle = PlayerSettings['subtitle']
interface PlayerContextType extends PlayerContextValue {
setVideoState: React.Dispatch<React.SetStateAction<VideoState>>
setUIState: React.Dispatch<React.SetStateAction<UIState>>
translations: Translations
}
const PlayerContext = createContext<PlayerContextType | null>(null)
// eslint-disable-next-line react-refresh/only-export-components
export const usePlayerContext = () => {
const context = useContext(PlayerContext)
if (!context) {
throw new Error('usePlayerContext must be used within a PlayerProvider')
}
return context
}
interface PlayerProviderProps {
children: React.ReactNode
initialVolume?: number
initialMuted?: boolean
initialPlaybackRate?: number
language?: string
}
export const PlayerProvider: React.FC<PlayerProviderProps> = ({
children,
initialVolume = 1,
initialMuted = false,
initialPlaybackRate = 1,
language,
}) => {
const videoRef = useRef<HTMLVideoElement | null>(null)
const containerRef = useRef<HTMLDivElement | null>(null)
// Get translations based on language prop or browser language
const translations = getTranslations(language || detectBrowserLanguage())
const [videoState, setVideoState] = useState<VideoState>({
playing: false,
currentTime: 0,
duration: 0,
buffered: 0,
volume: initialVolume,
muted: initialMuted,
playbackRate: initialPlaybackRate,
fullscreen: false,
pictureInPicture: false,
loading: false,
error: null,
seeking: false,
isLiveBroadcast: false,
})
const [uiState, setUIState] = useState<UIState>({
controlsVisible: true,
settingsOpen: false,
volumeControlOpen: false,
qualityMenuOpen: false,
subtitleMenuOpen: false,
})
const [settings, setSettings] = useState<PlayerSettings>({
quality: null,
subtitle: null,
audioTrack: null,
playbackRate: initialPlaybackRate,
})
// Video controls
const play = useCallback(() => {
videoRef.current?.play()
}, [])
const pause = useCallback(() => {
videoRef.current?.pause()
}, [])
const togglePlay = useCallback(() => {
if (videoState.playing) {
pause()
} else {
play()
}
}, [videoState.playing, play, pause])
const seek = useCallback((time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time
}
}, [])
const setVolume = useCallback((volume: number) => {
if (videoRef.current) {
const clampedVolume = Math.max(0, Math.min(1, volume))
videoRef.current.volume = clampedVolume
setVideoState((prev) => ({ ...prev, volume: clampedVolume }))
}
}, [])
const toggleMute = useCallback(() => {
if (videoRef.current) {
videoRef.current.muted = !videoRef.current.muted
setVideoState((prev) => ({ ...prev, muted: !prev.muted }))
}
}, [])
const setPlaybackRate = useCallback((rate: number) => {
if (videoRef.current) {
videoRef.current.playbackRate = rate
setVideoState((prev) => ({ ...prev, playbackRate: rate }))
setSettings((prev) => ({ ...prev, playbackRate: rate }))
}
}, [])
// Fullscreen & PIP
const toggleFullscreen = useCallback(() => {
if (!document.fullscreenElement) {
containerRef.current?.requestFullscreen()
} else {
document.exitFullscreen()
}
}, [])
const togglePictureInPicture = useCallback(async () => {
if (!document.pictureInPictureElement) {
try {
await videoRef.current?.requestPictureInPicture()
} catch (error) {
if (error instanceof Error) {
setVideoState((prev) => ({ ...prev, error }))
}
}
} else {
await document.exitPictureInPicture()
}
}, [])
// UI controls
const showControls = useCallback(() => {
setUIState((prev) => ({ ...prev, controlsVisible: true }))
}, [])
const hideControls = useCallback(() => {
setUIState((prev) => ({ ...prev, controlsVisible: false }))
}, [])
const toggleSettings = useCallback(() => {
setUIState((prev) => ({ ...prev, settingsOpen: !prev.settingsOpen }))
}, [])
// Settings
const setQuality = useCallback((quality: SelectedQuality) => {
setSettings((prev) => ({ ...prev, quality }))
}, [])
const setSubtitle = useCallback((subtitle: SelectedSubtitle) => {
setSettings((prev) => ({ ...prev, subtitle }))
}, [])
const setAudioTrack = useCallback((audioTrack: AudioTrack | null) => {
setSettings((prev) => ({ ...prev, audioTrack }))
}, [])
const value: PlayerContextType = {
videoState,
uiState,
settings,
videoRef,
containerRef,
setVideoState,
setUIState,
translations,
play,
pause,
togglePlay,
seek,
setVolume,
toggleMute,
setPlaybackRate,
toggleFullscreen,
togglePictureInPicture,
showControls,
hideControls,
toggleSettings,
setQuality,
setSubtitle,
setAudioTrack,
}
return <PlayerContext.Provider value={value}>{children}</PlayerContext.Provider>
}