feat: implement phase2 player isolation and media config
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user