Add SRT subtitle support and improve subtitle handling

This update enables SRT subtitle files by converting them to VTT blob URLs, sets the default subtitle track automatically, and improves subtitle track switching. Subtitle styling is enhanced in CSS, and tests are updated to use the correct subtitle prop format.
This commit is contained in:
hibna
2025-10-29 14:15:43 +03:00
parent 02e578f954
commit c5efcb95d5
4 changed files with 177 additions and 8 deletions
+8 -3
View File
@@ -8,11 +8,16 @@ function App() {
const [useDemo, setUseDemo] = useState(true)
// Demo video URLs (you can replace with your own)
const demoVideoUrl = '/Stormy Weather_c7e908aa/master.m3u8'
const demoPoster = undefined
const demoVideoUrl = '/s6ebilmemkac.mp4'
const demoPoster = '/s6ebilmemkac.webp'
const demoSubtitles: SubtitleTrack[] = [
// Add your subtitle URLs here
{
src: '/ses.srt',
lang: 'tr',
label: 'Türkçe',
default: true,
},
]
const currentVideoUrl = useDemo ? demoVideoUrl : videoUrl
+22
View File
@@ -24,3 +24,25 @@
.video-element::-webkit-media-controls-panel {
display: none !important;
}
/* Subtitle styling */
.video-element::cue {
background-color: rgba(0, 0, 0, 0.8);
color: white;
font-size: 1.2em;
font-family: Arial, sans-serif;
line-height: 1.4;
padding: 0.2em 0.5em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.9);
}
/* Ensure text tracks are visible */
.video-element::-webkit-media-text-track-container {
overflow: visible !important;
position: relative !important;
z-index: 1 !important;
}
.video-element::-webkit-media-text-track-display {
overflow: visible !important;
}
+145 -3
View File
@@ -4,6 +4,7 @@ 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 { createSubtitleBlobURL } from '../utils/subtitles'
import './VideoElement.css'
interface VideoElementProps {
@@ -45,10 +46,12 @@ export const VideoElement: React.FC<VideoElementProps> = ({
onAudioTracksLoaded,
onQualityLevelsLoaded,
}) => {
const { videoRef, setVideoState, toggleFullscreen, settings, setQuality } = usePlayerContext()
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 [processedSubtitles, setProcessedSubtitles] = useState<SubtitleTrack[]>([])
const subtitleBlobUrlsRef = React.useRef<string[]>([])
// Handle video events
const handlePlay = useCallback(() => {
@@ -88,8 +91,25 @@ export const VideoElement: React.FC<VideoElementProps> = ({
muted: video.muted,
}))
// Enable default subtitle if specified
const tracks = video.textTracks
if (tracks && processedSubtitles.length > 0) {
const defaultSubtitle = processedSubtitles.find((sub) => sub.default)
if (defaultSubtitle) {
// Find the corresponding track and set it as showing
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i]
if (track.language === defaultSubtitle.lang) {
track.mode = 'showing'
setSubtitle(defaultSubtitle)
break
}
}
}
}
onLoadedMetadata?.()
}, [videoRef, setVideoState, onLoadedMetadata])
}, [videoRef, setVideoState, onLoadedMetadata, processedSubtitles, setSubtitle])
const handleVolumeChange = useCallback(() => {
const video = videoRef.current
@@ -196,6 +216,55 @@ export const VideoElement: React.FC<VideoElementProps> = ({
}
}, [setVideoState])
// Process subtitles - convert SRT to VTT blob URLs
useEffect(() => {
// Clean up old blob URLs
subtitleBlobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url))
subtitleBlobUrlsRef.current = []
if (subtitles.length === 0) {
setProcessedSubtitles([])
return
}
const processSubtitles = async () => {
const processed = await Promise.all(
subtitles.map(async (subtitle) => {
try {
// Check if it's an SRT file
if (subtitle.src.endsWith('.srt')) {
// Fetch and convert SRT to VTT
const response = await fetch(subtitle.src)
if (!response.ok) {
throw new Error(`Failed to fetch subtitle: ${response.status} ${response.statusText}`)
}
const srtContent = await response.text()
const blobUrl = createSubtitleBlobURL(srtContent, 'srt')
subtitleBlobUrlsRef.current.push(blobUrl)
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)
return subtitle
}
})
)
setProcessedSubtitles(processed)
}
processSubtitles()
// Cleanup function
return () => {
subtitleBlobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url))
subtitleBlobUrlsRef.current = []
}
}, [subtitles])
// Detect HLS source and load hls.js if needed
useEffect(() => {
const video = videoRef.current
@@ -376,6 +445,79 @@ export const VideoElement: React.FC<VideoElementProps> = ({
setHlsQualityLevel(hlsInstance, targetLevelIndex)
}, [settings.quality, availableQualities, videoRef])
// Handle subtitle track changes
useEffect(() => {
const video = videoRef.current
if (!video) return
const tracks = video.textTracks
if (!tracks || tracks.length === 0) return
// Disable all tracks first
for (let i = 0; i < tracks.length; i++) {
tracks[i].mode = 'hidden'
}
// Enable the selected subtitle track
if (settings.subtitle) {
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i]
if (track.language === settings.subtitle.lang) {
track.mode = 'showing'
console.log(`Enabled subtitle track: ${track.label} (${track.language})`)
break
}
}
}
}, [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
console.log(`Track loaded: ${track.label} (${track.srclang})`, track.readyState)
}
const handleTrackError = (e: Event) => {
const track = e.target as HTMLTrackElement
console.error(`Track error: ${track.label} (${track.srclang})`, track.track.cues?.length)
}
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(`Active track: ${track.label}, cues: ${track.cues?.length || 0}, active cues: ${track.activeCues?.length || 0}`)
}
}
}
for (let i = 0; i < textTracks.length; i++) {
textTracks[i].addEventListener('cuechange', handleCueChange)
}
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
@@ -399,7 +541,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
onError={handleError}
onClick={handleVideoClick}
>
{subtitles.map((subtitle, index) => (
{processedSubtitles.map((subtitle, index) => (
<track
key={index}
kind="subtitles"
+2 -2
View File
@@ -95,8 +95,8 @@ describe('VideoPlayer', () => {
it('renders with subtitles prop', () => {
const subtitles = [
{ src: 'subtitles-en.vtt', srcLang: 'en', label: 'English' },
{ src: 'subtitles-tr.vtt', srcLang: 'tr', label: 'Türkçe' },
{ src: 'subtitles-en.vtt', lang: 'en', label: 'English' },
{ src: 'subtitles-tr.vtt', lang: 'tr', label: 'Türkçe' },
];
const { container } = render(<VideoPlayer {...defaultProps} subtitles={subtitles} />);