Add SRT/FLV/RTMP support and update documentation

Introduced Python scripts for SRT subtitle checking and fixing, and added comprehensive documentation covering advanced features such as protocol detection, subtitle/audio/quality management, keyboard shortcuts, and touch gestures. Updated local settings to allow new build and Python commands, added TypeScript definitions for FLV, and implemented RTMP/FLV protocol support in the player. Removed CHANGELOG.md and made various improvements to styles and example app.
This commit is contained in:
hibna
2025-11-03 02:35:56 +03:00
parent 42a12dfa8b
commit 36f83ff72c
26 changed files with 3833 additions and 1212 deletions
+121 -42
View File
@@ -4,6 +4,8 @@ import type { SubtitleTrack, AudioTrack, VideoQuality } from '../types'
import { validateVideoURL, getCORSErrorMessage, isCORSError } from '../utils/corsHelper'
import { setHlsAudioTrack, setHlsQualityLevel } from '../utils/hlsControl'
import { setupHlsInstance } from '../utils/hlsSetup'
import { setupRtmpInstance } from '../utils/rtmpSetup'
import { detectVideoProtocol } from '../utils/videoProtocol'
import { createSubtitleBlobURL } from '../utils/subtitles'
import './VideoElement.css'
@@ -25,6 +27,7 @@ interface VideoElementProps {
onSeeked?: () => void
onAudioTracksLoaded?: (tracks: AudioTrack[]) => void
onQualityLevelsLoaded?: (qualities: VideoQuality[]) => void
onSubtitleTracksLoaded?: (tracks: SubtitleTrack[]) => void
}
export const VideoElement: React.FC<VideoElementProps> = ({
@@ -45,11 +48,13 @@ export const VideoElement: React.FC<VideoElementProps> = ({
onSeeked,
onAudioTracksLoaded,
onQualityLevelsLoaded,
onSubtitleTracksLoaded,
}) => {
const { videoRef, setVideoState, toggleFullscreen, settings, setQuality, setSubtitle } = usePlayerContext()
const lastClickTimeRef = React.useRef<number>(0)
const [availableAudioTracks, setAvailableAudioTracks] = useState<AudioTrack[]>([])
const [availableQualities, setAvailableQualities] = useState<VideoQuality[]>([])
const [hlsSubtitles, setHlsSubtitles] = useState<SubtitleTrack[]>([])
const [processedSubtitles, setProcessedSubtitles] = useState<SubtitleTrack[]>([])
const subtitleBlobUrlsRef = React.useRef<string[]>([])
@@ -210,7 +215,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
}
}, [setVideoState])
// Process subtitles - convert SRT to VTT blob URLs
// Process subtitles - convert SRT to VTT blob URLs and merge with HLS subtitles
useEffect(() => {
let cancelled = false
@@ -218,14 +223,17 @@ export const VideoElement: React.FC<VideoElementProps> = ({
subtitleBlobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url))
subtitleBlobUrlsRef.current = []
if (subtitles.length === 0) {
// Merge manual subtitles and HLS subtitles
const allSubtitles = [...subtitles, ...hlsSubtitles]
if (allSubtitles.length === 0) {
setProcessedSubtitles([])
return
}
const processSubtitles = async () => {
const processed = await Promise.all(
subtitles.map(async (subtitle) => {
allSubtitles.map(async (subtitle) => {
try {
// Check if it's an SRT file
if (subtitle.src.endsWith('.srt')) {
@@ -273,7 +281,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
subtitleBlobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url))
subtitleBlobUrlsRef.current = []
}
}, [subtitles])
}, [subtitles, hlsSubtitles])
useEffect(() => {
const video = videoRef.current
@@ -297,7 +305,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
}
}, [processedSubtitles, settings.subtitle, setSubtitle, videoRef])
// Detect HLS source and load hls.js if needed
// Detect video protocol and setup appropriate player
useEffect(() => {
const video = videoRef.current
if (!video) return
@@ -306,6 +314,8 @@ export const VideoElement: React.FC<VideoElementProps> = ({
onAudioTracksLoaded?.([])
setAvailableQualities([])
onQualityLevelsLoaded?.([])
setHlsSubtitles([])
onSubtitleTracksLoaded?.([])
// Validate video URL first
const validation = validateVideoURL(src)
@@ -316,57 +326,117 @@ export const VideoElement: React.FC<VideoElementProps> = ({
return
}
const isHLS = src.includes('.m3u8')
// Detect video protocol
const detection = detectVideoProtocol(src)
let cleanupFn: (() => void) | null = null
const setupHls = async () => {
if (isHLS && video.canPlayType('application/vnd.apple.mpegurl') === '') {
try {
cleanupFn = await setupHlsInstance({
video,
src,
autoplay,
onAudioTracksLoaded: (tracks) => {
setAvailableAudioTracks(tracks)
onAudioTracksLoaded?.(tracks)
},
onQualityLevelsLoaded: (qualities) => {
setAvailableQualities(qualities)
onQualityLevelsLoaded?.(qualities)
},
onError: handleError,
})
} catch (err) {
let error: Error
if (err instanceof Error && isCORSError(err)) {
const corsMessage = getCORSErrorMessage(src)
error = new Error(corsMessage)
} else {
error = err instanceof Error ? err : new Error('Failed to load HLS')
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)
const setupPlayer = async () => {
try {
switch (detection.protocol) {
case 'hls': {
// HLS streaming setup
const canPlayHLS = video.canPlayType('application/vnd.apple.mpegurl')
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)
if (shouldUseHlsJs) {
console.log('[VideoElement] Setting up HLS.js...')
cleanupFn = await setupHlsInstance({
video,
src,
autoplay,
onAudioTracksLoaded: (tracks) => {
setAvailableAudioTracks(tracks)
onAudioTracksLoaded?.(tracks)
},
onQualityLevelsLoaded: (qualities) => {
setAvailableQualities(qualities)
onQualityLevelsLoaded?.(qualities)
},
onSubtitleTracksLoaded: (tracks) => {
setHlsSubtitles(tracks)
onSubtitleTracksLoaded?.(tracks)
},
onError: handleError,
})
} else {
console.log('[VideoElement] Using native HLS playback')
video.src = src
if (autoplay) {
void video.play().catch(() => undefined)
}
}
break
}
case 'rtmp': {
// RTMP/FLV streaming setup
console.log('[VideoElement] Setting up RTMP/FLV player...')
cleanupFn = await setupRtmpInstance({
video,
src,
autoplay,
onError: handleError,
onLoadedMetadata,
})
break
}
case 'dash': {
// DASH streaming - not yet implemented
const error = new Error('DASH streaming is not yet supported')
console.error('[VideoElement]', error.message)
setVideoState((prev) => ({ ...prev, error, loading: false }))
onError?.(error)
break
}
case 'native':
default: {
// Native HTML5 video (MP4, WebM, etc.)
console.log('[VideoElement] Using native video.src')
video.src = src
if (autoplay) {
void video.play().catch(() => undefined)
}
break
}
setVideoState((prev) => ({
...prev,
error,
loading: false,
}))
onError?.(error)
}
} else {
video.src = src
if (autoplay) {
void video.play().catch(() => undefined)
} catch (err) {
let error: Error
if (err instanceof Error && isCORSError(err)) {
const corsMessage = getCORSErrorMessage(src)
error = new Error(corsMessage)
} else {
error = err instanceof Error ? err : new Error(`Failed to load ${detection.protocol.toUpperCase()} video`)
}
console.error('[VideoElement] Setup error:', error)
setVideoState((prev) => ({
...prev,
error,
loading: false,
}))
onError?.(error)
}
}
setupHls()
setupPlayer()
// Cleanup function
return () => {
if (cleanupFn) {
cleanupFn()
}
// Also check for any lingering HLS instance
// Also check for any lingering player instances
if ((video as any).__hlsInstance) {
const hls = (video as any).__hlsInstance
if (hls && typeof hls.destroy === 'function') {
@@ -374,6 +444,13 @@ export const VideoElement: React.FC<VideoElementProps> = ({
}
delete (video as any).__hlsInstance
}
if ((video as any).__rtmpInstance) {
const rtmp = (video as any).__rtmpInstance
if (rtmp && typeof rtmp.destroy === 'function') {
rtmp.destroy()
}
delete (video as any).__rtmpInstance
}
}
}, [
src,
@@ -384,6 +461,8 @@ export const VideoElement: React.FC<VideoElementProps> = ({
onError,
onAudioTracksLoaded,
onQualityLevelsLoaded,
onSubtitleTracksLoaded,
onLoadedMetadata,
])
// Handle audio track changes