feat: apply phase1 DX cleanup for private registry
This commit is contained in:
@@ -31,7 +31,7 @@ export const ControlsLayer: React.FC<ControlsLayerProps> = ({
|
||||
audioTracks = [],
|
||||
qualities = [],
|
||||
}) => {
|
||||
const { videoState, uiState, togglePlay, toggleFullscreen, showControls, hideControls } =
|
||||
const { videoState, uiState, togglePlay, toggleFullscreen, showControls, hideControls, translations } =
|
||||
usePlayerContext()
|
||||
const [isPointerOver, setIsPointerOver] = useState(false)
|
||||
const [lastInteraction, setLastInteraction] = useState<number>(0)
|
||||
@@ -228,7 +228,7 @@ export const ControlsLayer: React.FC<ControlsLayerProps> = ({
|
||||
{videoState.isLiveBroadcast && (
|
||||
<div className="live-indicator">
|
||||
<span className="live-dot"></span>
|
||||
<span className="live-text">LIVE</span>
|
||||
<span className="live-text">{translations.live}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+25
-106
@@ -8,6 +8,7 @@ import { setupRtmpInstance } from '../utils/rtmpSetup'
|
||||
import { setupMpegtsInstance } from '../utils/mpegtsSetup'
|
||||
import { detectVideoProtocol } from '../utils/videoProtocol'
|
||||
import { createSubtitleBlobURL } from '../utils/subtitles'
|
||||
import { logger } from '../utils/logger'
|
||||
import './VideoElement.css'
|
||||
|
||||
interface VideoElementProps {
|
||||
@@ -112,7 +113,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
|
||||
// Check if this is a live broadcast (duration is Infinity for live streams)
|
||||
const isLiveBroadcast = !isFinite(video.duration) || video.duration === 0
|
||||
console.log('[VideoElement] Is live broadcast?', isLiveBroadcast, 'duration:', video.duration)
|
||||
logger.log('[VideoElement] Is live broadcast?', isLiveBroadcast, 'duration:', video.duration)
|
||||
|
||||
setVideoState((prev) => ({
|
||||
...prev,
|
||||
@@ -127,7 +128,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
if (tracks && processedSubtitles.length > 0) {
|
||||
const defaultSubtitle = processedSubtitles.find((sub) => sub.default)
|
||||
if (defaultSubtitle) {
|
||||
console.log(`🎯 Found default subtitle in metadata: ${defaultSubtitle.label}`)
|
||||
logger.log(`🎯 Found default subtitle in metadata: ${defaultSubtitle.label}`)
|
||||
// Set subtitle in context (this will trigger the useEffect that enables it)
|
||||
setSubtitle(defaultSubtitle)
|
||||
}
|
||||
@@ -142,7 +143,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
|
||||
// Re-check if this is a live broadcast when duration changes
|
||||
const isLiveBroadcast = !isFinite(video.duration) || video.duration === 0
|
||||
console.log('[VideoElement] Duration changed. Is live broadcast?', isLiveBroadcast, 'duration:', video.duration)
|
||||
logger.log('[VideoElement] Duration changed. Is live broadcast?', isLiveBroadcast, 'duration:', video.duration)
|
||||
|
||||
setVideoState((prev) => ({
|
||||
...prev,
|
||||
@@ -339,25 +340,16 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
throw new Error(`Failed to fetch subtitle: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
const srtContent = await response.text()
|
||||
console.log(`SRT content length: ${srtContent.length} chars`)
|
||||
|
||||
const blobUrl = createSubtitleBlobURL(srtContent, 'srt')
|
||||
subtitleBlobUrlsRef.current.push(blobUrl)
|
||||
|
||||
// Debug: fetch the blob URL to verify VTT content
|
||||
const vttResponse = await fetch(blobUrl)
|
||||
const vttContent = await vttResponse.text()
|
||||
console.log(`VTT content preview (first 500 chars):`, vttContent.substring(0, 500))
|
||||
console.log(`Total VTT length: ${vttContent.length} chars`)
|
||||
|
||||
console.log(`Processed SRT subtitle "${subtitle.label}": ${subtitle.src} -> ${blobUrl}`)
|
||||
return { ...subtitle, src: blobUrl }
|
||||
}
|
||||
// VTT files can be used directly
|
||||
console.log(`Using VTT subtitle "${subtitle.label}": ${subtitle.src}`)
|
||||
return subtitle
|
||||
} catch (error) {
|
||||
console.error(`Failed to process subtitle ${subtitle.label}:`, error)
|
||||
logger.error(`Failed to process subtitle ${subtitle.label}:`, error)
|
||||
return subtitle
|
||||
}
|
||||
})
|
||||
@@ -459,10 +451,10 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[VideoElement] Source:', src)
|
||||
console.log('[VideoElement] Detected protocol:', detection.protocol)
|
||||
console.log('[VideoElement] Is live stream?', detection.isLive)
|
||||
console.log('[VideoElement] Needs special player?', detection.needsSpecialPlayer)
|
||||
logger.log('[VideoElement] Source:', src)
|
||||
logger.log('[VideoElement] Detected protocol:', detection.protocol)
|
||||
logger.log('[VideoElement] Is live stream?', detection.isLive)
|
||||
logger.log('[VideoElement] Needs special player?', detection.needsSpecialPlayer)
|
||||
|
||||
const setupPlayer = async () => {
|
||||
try {
|
||||
@@ -473,12 +465,12 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
|
||||
const shouldUseHlsJs = canPlayHLS === '' || !isSafari
|
||||
|
||||
console.log('[VideoElement] Native HLS support?', canPlayHLS)
|
||||
console.log('[VideoElement] Is Safari?', isSafari)
|
||||
console.log('[VideoElement] Will use HLS.js?', shouldUseHlsJs)
|
||||
logger.log('[VideoElement] Native HLS support?', canPlayHLS)
|
||||
logger.log('[VideoElement] Is Safari?', isSafari)
|
||||
logger.log('[VideoElement] Will use HLS.js?', shouldUseHlsJs)
|
||||
|
||||
if (shouldUseHlsJs) {
|
||||
console.log('[VideoElement] Setting up HLS.js...')
|
||||
logger.log('[VideoElement] Setting up HLS.js...')
|
||||
cleanupFn = await setupHlsInstance({
|
||||
video,
|
||||
src,
|
||||
@@ -507,7 +499,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
}
|
||||
} else {
|
||||
if (isCancelled) return
|
||||
console.log('[VideoElement] Using native HLS playback')
|
||||
logger.log('[VideoElement] Using native HLS playback')
|
||||
video.src = src
|
||||
if (autoplay) {
|
||||
void video.play().catch(() => undefined)
|
||||
@@ -518,7 +510,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
|
||||
case 'rtmp': {
|
||||
// RTMP/FLV streaming setup
|
||||
console.log('[VideoElement] Setting up RTMP/FLV player...')
|
||||
logger.log('[VideoElement] Setting up RTMP/FLV player...')
|
||||
cleanupFn = await setupRtmpInstance({
|
||||
video,
|
||||
src,
|
||||
@@ -536,7 +528,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
|
||||
case 'mpegts': {
|
||||
// MPEG-TS/IPTV streaming setup
|
||||
console.log('[VideoElement] Setting up MPEG-TS player...')
|
||||
logger.log('[VideoElement] Setting up MPEG-TS player...')
|
||||
cleanupFn = await setupMpegtsInstance({
|
||||
video,
|
||||
src,
|
||||
@@ -556,7 +548,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
// DASH streaming - not yet implemented
|
||||
if (isCancelled) return
|
||||
const error = new Error('DASH streaming is not yet supported')
|
||||
console.error('[VideoElement]', error.message)
|
||||
logger.error('[VideoElement]', error.message)
|
||||
setVideoState((prev) => ({ ...prev, error, loading: false }))
|
||||
onError?.(error)
|
||||
break
|
||||
@@ -566,7 +558,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
default: {
|
||||
// Native HTML5 video (MP4, WebM, etc.)
|
||||
if (isCancelled) return
|
||||
console.log('[VideoElement] Using native video.src')
|
||||
logger.log('[VideoElement] Using native video.src')
|
||||
video.src = src
|
||||
if (autoplay) {
|
||||
void video.play().catch(() => undefined)
|
||||
@@ -585,7 +577,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
|
||||
if (isCancelled) return
|
||||
|
||||
console.error('[VideoElement] Setup error:', error)
|
||||
logger.error('[VideoElement] Setup error:', error)
|
||||
setVideoState((prev) => ({
|
||||
...prev,
|
||||
error,
|
||||
@@ -728,11 +720,11 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
// Wait for track to have cues before showing
|
||||
if (track.cues && track.cues.length > 0) {
|
||||
track.mode = 'showing'
|
||||
console.log(`🔊 Enabled subtitle track: ${track.label} (${track.language})`)
|
||||
console.log(` - cues available: ${track.cues.length}`)
|
||||
console.log(` - track.mode: ${track.mode}`)
|
||||
logger.log(`🔊 Enabled subtitle track: ${track.label} (${track.language})`)
|
||||
logger.log(` - cues available: ${track.cues.length}`)
|
||||
logger.log(` - track.mode: ${track.mode}`)
|
||||
} else {
|
||||
console.warn(`⚠️ Track ${track.label} has no cues yet, waiting...`)
|
||||
logger.warn(`⚠️ Track ${track.label} has no cues yet, waiting...`)
|
||||
// Track not ready yet, will be handled by load event
|
||||
track.mode = 'showing'
|
||||
}
|
||||
@@ -747,7 +739,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
|
||||
// Also listen for track load events to retry
|
||||
const handleTrackChange = () => {
|
||||
console.log(`🔄 Track changed, re-enabling subtitle`)
|
||||
logger.log(`🔄 Track changed, re-enabling subtitle`)
|
||||
enableSubtitle()
|
||||
}
|
||||
|
||||
@@ -762,80 +754,6 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
}
|
||||
}, [settings.subtitle, videoRef])
|
||||
|
||||
// Debug: Monitor text track loading
|
||||
useEffect(() => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
const handleTrackLoad = (e: Event) => {
|
||||
const track = e.target as HTMLTrackElement
|
||||
const textTrack = track.track
|
||||
console.log(`✅ Track loaded: ${track.label} (${track.srclang})`)
|
||||
console.log(` - readyState: ${track.readyState}`)
|
||||
console.log(` - track.mode: ${textTrack.mode}`)
|
||||
console.log(` - track.cues: ${textTrack.cues?.length || 0}`)
|
||||
console.log(` - src: ${track.src}`)
|
||||
|
||||
// Log first few cues if available
|
||||
if (textTrack.cues && textTrack.cues.length > 0) {
|
||||
console.log(` - First cue: ${(textTrack.cues[0] as VTTCue).startTime}s - ${(textTrack.cues[0] as VTTCue).endTime}s: "${(textTrack.cues[0] as VTTCue).text}"`)
|
||||
} else {
|
||||
console.warn(` ⚠️ No cues found in track!`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTrackError = (e: Event) => {
|
||||
const track = e.target as HTMLTrackElement
|
||||
console.error(`❌ Track error: ${track.label} (${track.srclang})`)
|
||||
console.error(` - src: ${track.src}`)
|
||||
console.error(` - readyState: ${track.readyState}`)
|
||||
}
|
||||
|
||||
const trackElements = video.querySelectorAll('track')
|
||||
trackElements.forEach((track) => {
|
||||
track.addEventListener('load', handleTrackLoad)
|
||||
track.addEventListener('error', handleTrackError)
|
||||
})
|
||||
|
||||
// Also monitor text tracks
|
||||
const textTracks = video.textTracks
|
||||
const handleCueChange = () => {
|
||||
for (let i = 0; i < textTracks.length; i++) {
|
||||
const track = textTracks[i]
|
||||
if (track.mode === 'showing') {
|
||||
console.log(`🎬 Cuechange: ${track.label}, cues: ${track.cues?.length || 0}, active cues: ${track.activeCues?.length || 0}`)
|
||||
if (track.activeCues && track.activeCues.length > 0) {
|
||||
const cue = track.activeCues[0] as VTTCue
|
||||
console.log(` - Active cue text: "${cue.text}"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < textTracks.length; i++) {
|
||||
textTracks[i].addEventListener('cuechange', handleCueChange)
|
||||
}
|
||||
|
||||
// Log all text tracks after a delay to see their state
|
||||
setTimeout(() => {
|
||||
console.log(`📊 Text tracks summary (${textTracks.length} total):`)
|
||||
for (let i = 0; i < textTracks.length; i++) {
|
||||
const track = textTracks[i]
|
||||
console.log(` [${i}] ${track.label} (${track.language}): mode=${track.mode}, cues=${track.cues?.length || 0}`)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
trackElements.forEach((track) => {
|
||||
track.removeEventListener('load', handleTrackLoad)
|
||||
track.removeEventListener('error', handleTrackError)
|
||||
})
|
||||
for (let i = 0; i < textTracks.length; i++) {
|
||||
textTracks[i].removeEventListener('cuechange', handleCueChange)
|
||||
}
|
||||
}
|
||||
}, [videoRef, processedSubtitles])
|
||||
|
||||
return (
|
||||
<div className="video-container">
|
||||
<video
|
||||
@@ -876,3 +794,4 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PlayIcon } from '../../icons'
|
||||
import './CenterPlayButton.css'
|
||||
|
||||
export const CenterPlayButton: React.FC = () => {
|
||||
const { play } = usePlayerContext()
|
||||
const { play, translations } = usePlayerContext()
|
||||
|
||||
return (
|
||||
<div className="center-play-overlay">
|
||||
@@ -12,8 +12,8 @@ export const CenterPlayButton: React.FC = () => {
|
||||
className="center-play-button"
|
||||
type="button"
|
||||
onClick={play}
|
||||
aria-label="Play"
|
||||
title="Play"
|
||||
aria-label={translations.play}
|
||||
title={translations.play}
|
||||
>
|
||||
<PlayIcon size={72} color="var(--player-text)" />
|
||||
</button>
|
||||
|
||||
@@ -4,14 +4,17 @@ import { FullscreenIcon, FullscreenExitIcon } from '../../icons'
|
||||
import './ControlButton.css'
|
||||
|
||||
export const FullscreenButton: React.FC = () => {
|
||||
const { videoState, toggleFullscreen } = usePlayerContext()
|
||||
const { videoState, toggleFullscreen, translations } = usePlayerContext()
|
||||
const actionLabel = videoState.fullscreen
|
||||
? translations.exitFullscreen
|
||||
: translations.enterFullscreen
|
||||
|
||||
return (
|
||||
<button
|
||||
className="control-button fullscreen-button"
|
||||
onClick={toggleFullscreen}
|
||||
aria-label={videoState.fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
||||
title={videoState.fullscreen ? 'Exit fullscreen (F)' : 'Enter fullscreen (F)'}
|
||||
aria-label={actionLabel}
|
||||
title={`${actionLabel} (F)`}
|
||||
>
|
||||
{videoState.fullscreen ? (
|
||||
<FullscreenExitIcon size={24} color="var(--player-text)" />
|
||||
|
||||
@@ -4,7 +4,10 @@ import { PIPIcon } from '../../icons'
|
||||
import './ControlButton.css'
|
||||
|
||||
export const PIPButton: React.FC = () => {
|
||||
const { videoState, togglePictureInPicture } = usePlayerContext()
|
||||
const { videoState, togglePictureInPicture, translations } = usePlayerContext()
|
||||
const actionLabel = videoState.pictureInPicture
|
||||
? translations.exitPictureInPicture
|
||||
: translations.enterPictureInPicture
|
||||
|
||||
// Check if PIP is supported
|
||||
const isPIPSupported =
|
||||
@@ -20,8 +23,8 @@ export const PIPButton: React.FC = () => {
|
||||
<button
|
||||
className="control-button pip-button"
|
||||
onClick={togglePictureInPicture}
|
||||
aria-label={videoState.pictureInPicture ? 'Exit picture-in-picture' : 'Enter picture-in-picture'}
|
||||
title="Picture-in-picture (P)"
|
||||
aria-label={actionLabel}
|
||||
title={`${actionLabel} (P)`}
|
||||
>
|
||||
<PIPIcon size={24} color="var(--player-text)" />
|
||||
</button>
|
||||
|
||||
@@ -4,14 +4,15 @@ import { PlayIcon, PauseIcon } from '../../icons'
|
||||
import './ControlButton.css'
|
||||
|
||||
export const PlayPauseButton: React.FC = () => {
|
||||
const { videoState, togglePlay } = usePlayerContext()
|
||||
const { videoState, togglePlay, translations } = usePlayerContext()
|
||||
const actionLabel = videoState.playing ? translations.pause : translations.play
|
||||
|
||||
return (
|
||||
<button
|
||||
className="control-button play-pause-button"
|
||||
onClick={togglePlay}
|
||||
aria-label={videoState.playing ? 'Pause' : 'Play'}
|
||||
title={videoState.playing ? 'Pause (Space)' : 'Play (Space)'}
|
||||
aria-label={actionLabel}
|
||||
title={`${actionLabel} (Space)`}
|
||||
>
|
||||
{videoState.playing ? (
|
||||
<PauseIcon size={24} color="var(--player-text)" />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { usePlayerContext } from '../../contexts/PlayerContext'
|
||||
import './ProgressBar.css'
|
||||
|
||||
export const ProgressBar: React.FC = () => {
|
||||
const { videoState, seek } = usePlayerContext()
|
||||
const { videoState, seek, translations } = usePlayerContext()
|
||||
const progressRef = useRef<HTMLDivElement>(null)
|
||||
const [seeking, setSeeking] = useState(false)
|
||||
const [hoverTime, setHoverTime] = useState<number | null>(null)
|
||||
@@ -83,7 +83,7 @@ export const ProgressBar: React.FC = () => {
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
role="slider"
|
||||
aria-label="Video progress"
|
||||
aria-label={translations.videoProgress}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={videoState.duration}
|
||||
aria-valuenow={videoState.currentTime}
|
||||
|
||||
@@ -4,15 +4,15 @@ import { SettingsIcon } from '../../icons'
|
||||
import './ControlButton.css'
|
||||
|
||||
export const SettingsButton: React.FC = () => {
|
||||
const { toggleSettings } = usePlayerContext()
|
||||
const { toggleSettings, translations } = usePlayerContext()
|
||||
|
||||
return (
|
||||
<button
|
||||
className="control-button settings-button"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={toggleSettings}
|
||||
aria-label="Settings"
|
||||
title="Settings"
|
||||
aria-label={translations.settings}
|
||||
title={translations.settings}
|
||||
>
|
||||
<SettingsIcon size={24} color="var(--player-text)" />
|
||||
</button>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { VolumeUpIcon, VolumeDownIcon, VolumeMuteIcon } from '../../icons'
|
||||
import './VolumeControl.css'
|
||||
|
||||
export const VolumeControl: React.FC = () => {
|
||||
const { videoState, setVolume, toggleMute } = usePlayerContext()
|
||||
const { videoState, setVolume, toggleMute, translations } = usePlayerContext()
|
||||
const [showSlider, setShowSlider] = useState(false)
|
||||
const timeoutRef = useRef<number | undefined>(undefined)
|
||||
|
||||
@@ -30,6 +30,7 @@ export const VolumeControl: React.FC = () => {
|
||||
)
|
||||
|
||||
const VolumeIcon = videoState.muted ? VolumeMuteIcon : videoState.volume > 0.5 ? VolumeUpIcon : VolumeDownIcon
|
||||
const actionLabel = videoState.muted ? translations.unmute : translations.mute
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -40,8 +41,8 @@ export const VolumeControl: React.FC = () => {
|
||||
<button
|
||||
className="control-button volume-button"
|
||||
onClick={toggleMute}
|
||||
aria-label={videoState.muted ? 'Unmute' : 'Mute'}
|
||||
title={videoState.muted ? 'Unmute (M)' : 'Mute (M)'}
|
||||
aria-label={actionLabel}
|
||||
title={`${actionLabel} (M)`}
|
||||
>
|
||||
<VolumeIcon size={24} color="var(--player-text)" />
|
||||
</button>
|
||||
@@ -55,7 +56,7 @@ export const VolumeControl: React.FC = () => {
|
||||
value={videoState.muted ? 0 : videoState.volume}
|
||||
onChange={handleSliderChange}
|
||||
className="volume-slider"
|
||||
aria-label="Volume"
|
||||
aria-label={translations.volume}
|
||||
/>
|
||||
<div
|
||||
className="volume-slider-fill"
|
||||
|
||||
@@ -92,7 +92,7 @@ export const SettingsMenu: React.FC<SettingsMenuProps> = ({
|
||||
<div className="settings-main-option-content">
|
||||
<span className="settings-main-option-label">{translations.speed}</span>
|
||||
<span className="settings-main-option-value">
|
||||
{videoState.playbackRate === 1 ? 'Normal' : `${videoState.playbackRate}x`}
|
||||
{videoState.playbackRate === 1 ? translations.normal : `${videoState.playbackRate}x`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="settings-main-option-arrow">›</div>
|
||||
@@ -148,7 +148,7 @@ export const SettingsMenu: React.FC<SettingsMenuProps> = ({
|
||||
setTimeout(() => goBack(), 150)
|
||||
}}
|
||||
>
|
||||
<span>{rate === 1 ? 'Normal' : `${rate}x`}</span>
|
||||
<span>{rate === 1 ? translations.normal : `${rate}x`}</span>
|
||||
{videoState.playbackRate === rate && <CheckIcon size={16} color="var(--player-primary)" />}
|
||||
</button>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user