diff --git a/examples/App.tsx b/examples/App.tsx index da8a9ac..969949c 100644 --- a/examples/App.tsx +++ b/examples/App.tsx @@ -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 diff --git a/src/components/VideoElement.css b/src/components/VideoElement.css index 83a369d..f0a94ff 100644 --- a/src/components/VideoElement.css +++ b/src/components/VideoElement.css @@ -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; +} diff --git a/src/components/VideoElement.tsx b/src/components/VideoElement.tsx index 8d4a938..173ffec 100644 --- a/src/components/VideoElement.tsx +++ b/src/components/VideoElement.tsx @@ -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 = ({ onAudioTracksLoaded, onQualityLevelsLoaded, }) => { - const { videoRef, setVideoState, toggleFullscreen, settings, setQuality } = usePlayerContext() + const { videoRef, setVideoState, toggleFullscreen, settings, setQuality, setSubtitle } = usePlayerContext() const lastClickTimeRef = React.useRef(0) const [availableAudioTracks, setAvailableAudioTracks] = useState([]) const [availableQualities, setAvailableQualities] = useState([]) + const [processedSubtitles, setProcessedSubtitles] = useState([]) + const subtitleBlobUrlsRef = React.useRef([]) // Handle video events const handlePlay = useCallback(() => { @@ -88,8 +91,25 @@ export const VideoElement: React.FC = ({ 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 = ({ } }, [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 = ({ 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 (