feat: implement phase2 player isolation and media config

This commit is contained in:
hibna
2026-02-12 18:39:11 +03:00
parent fcd2a14a05
commit 73d5d65d2b
8 changed files with 226 additions and 20 deletions
+25 -4
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useCallback, useState } from 'react'
import { usePlayerContext } from '../contexts/PlayerContext'
import type { SubtitleTrack, AudioTrack, VideoQuality } from '../types'
import type { SubtitleTrack, AudioTrack, VideoQuality, VideoProtocol } from '../types'
import { validateVideoURL, getCORSErrorMessage, isCORSError } from '../utils/corsHelper'
import { setHlsAudioTrack, setHlsQualityLevel } from '../utils/hlsControl'
import { setupHlsInstance } from '../utils/hlsSetup'
@@ -14,12 +14,17 @@ import './VideoElement.css'
interface VideoElementProps {
src: string
poster?: string
protocol?: 'auto' | VideoProtocol
autoplay?: boolean
loop?: boolean
muted?: boolean
volume?: number
playbackRate?: number
currentTime?: number
crossOrigin?: '' | 'anonymous' | 'use-credentials'
preload?: 'none' | 'metadata' | 'auto'
playsInline?: boolean
controlsList?: string
subtitles?: SubtitleTrack[]
onPlay?: () => void
onPause?: () => void
@@ -45,12 +50,17 @@ interface VideoElementProps {
export const VideoElement: React.FC<VideoElementProps> = ({
src,
poster,
protocol = 'auto',
autoplay = false,
loop = false,
muted = false,
volume,
playbackRate,
currentTime: initialCurrentTime,
crossOrigin,
preload = 'metadata',
playsInline = true,
controlsList,
subtitles = [],
onPlay,
onPause,
@@ -416,7 +426,15 @@ export const VideoElement: React.FC<VideoElementProps> = ({
}
// Detect video protocol
const detection = detectVideoProtocol(src)
const detectedProtocol = detectVideoProtocol(src)
const detection =
protocol === 'auto'
? detectedProtocol
: {
...detectedProtocol,
protocol,
needsSpecialPlayer: protocol !== 'native',
}
let cleanupFn: (() => void) | null = null
const teardownPlayer = () => {
@@ -596,6 +614,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
}
}, [
src,
protocol,
autoplay,
videoRef,
handleError,
@@ -762,8 +781,10 @@ export const VideoElement: React.FC<VideoElementProps> = ({
poster={poster}
loop={loop}
muted={muted}
playsInline
preload="metadata"
crossOrigin={crossOrigin}
playsInline={playsInline}
preload={preload}
controlsList={controlsList}
onPlay={handlePlay}
onPause={handlePause}
onTimeUpdate={handleTimeUpdate}
+53 -13
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useCallback } from 'react'
import React, { useMemo, useState, useCallback } from 'react'
import { PlayerProvider, usePlayerContext } from '../contexts/PlayerContext'
import { VideoElement } from './VideoElement'
import { ControlsLayer } from './ControlsLayer'
@@ -38,14 +38,20 @@ const VideoPlayerContent: React.FC<
> = ({
src,
poster,
protocol = 'auto',
autoplay = false,
loop = false,
muted = false,
volume,
playbackRate,
currentTime,
crossOrigin,
preload = 'metadata',
playsInline = true,
controlsList,
controls = true,
subtitles = [],
theme,
keyboardShortcuts = true,
pictureInPicture = true,
className = '',
@@ -75,21 +81,55 @@ const VideoPlayerContent: React.FC<
}) => {
const { containerRef, uiState } = usePlayerContext()
const controlsHiddenClass = !uiState.controlsVisible ? 'controls-hidden' : ''
const themedStyle = useMemo<React.CSSProperties>(() => {
if (!theme) {
return style || {}
}
const cssVariables: Record<string, string> = {}
if (theme.primaryColor) {
cssVariables['--player-primary'] = theme.primaryColor
}
if (theme.accentColor) {
cssVariables['--player-primary-hover'] = theme.accentColor
}
if (theme.backgroundColor) {
cssVariables['--player-bg'] = theme.backgroundColor
}
if (theme.textColor) {
cssVariables['--player-text'] = theme.textColor
}
return {
...cssVariables,
...(style || {}),
} as React.CSSProperties
}, [theme, style])
// Merge manual subtitles and HLS-detected subtitles
const allSubtitles = [...subtitles, ...hlsSubtitles]
return (
<div ref={containerRef} className={`video-player ${controlsHiddenClass} ${className}`} style={style}>
<div
ref={containerRef}
className={`video-player ${controlsHiddenClass} ${className}`}
style={themedStyle}
tabIndex={0}
>
<VideoElement
src={src}
poster={poster}
protocol={protocol}
autoplay={autoplay}
loop={loop}
muted={muted}
volume={volume}
playbackRate={playbackRate}
currentTime={currentTime}
crossOrigin={crossOrigin}
preload={preload}
playsInline={playsInline}
controlsList={controlsList}
subtitles={subtitles}
onPlay={onPlay}
onPause={onPause}
@@ -127,12 +167,17 @@ const VideoPlayerContent: React.FC<
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
src,
poster,
protocol = 'auto',
autoplay = false,
loop = false,
muted = false,
volume,
playbackRate,
currentTime,
crossOrigin,
preload = 'metadata',
playsInline = true,
controlsList,
controls = true,
subtitles = [],
theme,
@@ -162,17 +207,6 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [qualities, setQualities] = useState<VideoQuality[]>([])
const [hlsSubtitles, setHlsSubtitles] = useState<SubtitleTrack[]>([])
// Apply theme CSS variables
useEffect(() => {
if (theme) {
const root = document.documentElement
if (theme.primaryColor) root.style.setProperty('--player-primary', theme.primaryColor)
if (theme.accentColor) root.style.setProperty('--player-primary-hover', theme.accentColor)
if (theme.backgroundColor) root.style.setProperty('--player-bg', theme.backgroundColor)
if (theme.textColor) root.style.setProperty('--player-text', theme.textColor)
}
}, [theme])
const handleAudioTracksLoaded = useCallback((tracks: AudioTrack[]) => {
setAudioTracks(tracks)
}, [])
@@ -190,14 +224,20 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
<VideoPlayerContent
src={src}
poster={poster}
protocol={protocol}
autoplay={autoplay}
loop={loop}
muted={muted}
volume={volume}
playbackRate={playbackRate}
currentTime={currentTime}
crossOrigin={crossOrigin}
preload={preload}
playsInline={playsInline}
controlsList={controlsList}
controls={controls}
subtitles={subtitles}
theme={theme}
keyboardShortcuts={keyboardShortcuts}
pictureInPicture={pictureInPicture}
className={className}
@@ -77,6 +77,17 @@ describe('SettingsMenu', () => {
audioTrack: 'Audio Track',
settings: 'Settings',
level: 'Level',
play: 'Play',
pause: 'Pause',
mute: 'Mute',
unmute: 'Unmute',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
enterPictureInPicture: 'Enter picture-in-picture',
exitPictureInPicture: 'Exit picture-in-picture',
videoProgress: 'Video progress',
volume: 'Volume',
live: 'LIVE',
},
}
})