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:
+121
-42
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user