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:
+8
-3
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user