.
This commit is contained in:
@@ -25,24 +25,100 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtitle styling */
|
/* Modern Subtitle Styling */
|
||||||
.video-element::cue {
|
.video-element::cue {
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
/* Typography */
|
||||||
color: white;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
font-size: 1.2em;
|
font-size: 1.75rem;
|
||||||
font-family: Arial, sans-serif;
|
font-weight: 700;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
padding: 0.2em 0.5em;
|
letter-spacing: 0.5px;
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.9);
|
|
||||||
|
/* Colors - No background, only text with strong shadow */
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
/* Visual Effects - Strong shadow for readability */
|
||||||
|
text-shadow:
|
||||||
|
/* Strong black outline */
|
||||||
|
-2px -2px 0 #000,
|
||||||
|
2px -2px 0 #000,
|
||||||
|
-2px 2px 0 #000,
|
||||||
|
2px 2px 0 #000,
|
||||||
|
0 -2px 0 #000,
|
||||||
|
0 2px 0 #000,
|
||||||
|
-2px 0 0 #000,
|
||||||
|
2px 0 0 #000,
|
||||||
|
/* Additional shadow for depth */
|
||||||
|
0 4px 8px rgba(0, 0, 0, 0.9),
|
||||||
|
0 0 12px rgba(0, 0, 0, 0.8);
|
||||||
|
|
||||||
|
/* Better rendering */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure text tracks are visible */
|
/* Fullscreen subtitle adjustments */
|
||||||
|
:fullscreen .video-element::cue,
|
||||||
|
.video-element:fullscreen::cue {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure text tracks are properly positioned - above controls */
|
||||||
.video-element::-webkit-media-text-track-container {
|
.video-element::-webkit-media-text-track-container {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
position: relative !important;
|
position: absolute !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
z-index: 1 !important;
|
z-index: 1 !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
justify-content: flex-end !important;
|
||||||
|
align-items: center !important;
|
||||||
|
/* Position above controls bar (controls bar is ~120px high with padding) */
|
||||||
|
padding-bottom: 130px !important;
|
||||||
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-element::-webkit-media-text-track-display {
|
.video-element::-webkit-media-text-track-display {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 85% !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Multi-line subtitle support */
|
||||||
|
.video-element::cue-region {
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better contrast for different cue types */
|
||||||
|
.video-element::cue(b) {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-element::cue(i) {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-element::cue(u) {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.video-element::cue {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-element::-webkit-media-text-track-container {
|
||||||
|
padding-bottom: 110px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:fullscreen .video-element::cue,
|
||||||
|
.video-element:fullscreen::cue {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,15 +96,9 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
if (tracks && processedSubtitles.length > 0) {
|
if (tracks && processedSubtitles.length > 0) {
|
||||||
const defaultSubtitle = processedSubtitles.find((sub) => sub.default)
|
const defaultSubtitle = processedSubtitles.find((sub) => sub.default)
|
||||||
if (defaultSubtitle) {
|
if (defaultSubtitle) {
|
||||||
// Find the corresponding track and set it as showing
|
console.log(`🎯 Found default subtitle in metadata: ${defaultSubtitle.label}`)
|
||||||
for (let i = 0; i < tracks.length; i++) {
|
// Set subtitle in context (this will trigger the useEffect that enables it)
|
||||||
const track = tracks[i]
|
setSubtitle(defaultSubtitle)
|
||||||
if (track.language === defaultSubtitle.lang) {
|
|
||||||
track.mode = 'showing'
|
|
||||||
setSubtitle(defaultSubtitle)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +212,8 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
|
|
||||||
// Process subtitles - convert SRT to VTT blob URLs
|
// Process subtitles - convert SRT to VTT blob URLs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
// Clean up old blob URLs
|
// Clean up old blob URLs
|
||||||
subtitleBlobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url))
|
subtitleBlobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url))
|
||||||
subtitleBlobUrlsRef.current = []
|
subtitleBlobUrlsRef.current = []
|
||||||
@@ -239,8 +235,17 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
throw new Error(`Failed to fetch subtitle: ${response.status} ${response.statusText}`)
|
throw new Error(`Failed to fetch subtitle: ${response.status} ${response.statusText}`)
|
||||||
}
|
}
|
||||||
const srtContent = await response.text()
|
const srtContent = await response.text()
|
||||||
|
console.log(`SRT content length: ${srtContent.length} chars`)
|
||||||
|
|
||||||
const blobUrl = createSubtitleBlobURL(srtContent, 'srt')
|
const blobUrl = createSubtitleBlobURL(srtContent, 'srt')
|
||||||
subtitleBlobUrlsRef.current.push(blobUrl)
|
subtitleBlobUrlsRef.current.push(blobUrl)
|
||||||
|
|
||||||
|
// Debug: fetch the blob URL to verify VTT content
|
||||||
|
const vttResponse = await fetch(blobUrl)
|
||||||
|
const vttContent = await vttResponse.text()
|
||||||
|
console.log(`VTT content preview (first 500 chars):`, vttContent.substring(0, 500))
|
||||||
|
console.log(`Total VTT length: ${vttContent.length} chars`)
|
||||||
|
|
||||||
console.log(`Processed SRT subtitle "${subtitle.label}": ${subtitle.src} -> ${blobUrl}`)
|
console.log(`Processed SRT subtitle "${subtitle.label}": ${subtitle.src} -> ${blobUrl}`)
|
||||||
return { ...subtitle, src: blobUrl }
|
return { ...subtitle, src: blobUrl }
|
||||||
}
|
}
|
||||||
@@ -253,13 +258,18 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
setProcessedSubtitles(processed)
|
|
||||||
|
// Only update state if not cancelled
|
||||||
|
if (!cancelled) {
|
||||||
|
setProcessedSubtitles(processed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processSubtitles()
|
processSubtitles()
|
||||||
|
|
||||||
// Cleanup function
|
// Cleanup function
|
||||||
return () => {
|
return () => {
|
||||||
|
cancelled = true
|
||||||
subtitleBlobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url))
|
subtitleBlobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url))
|
||||||
subtitleBlobUrlsRef.current = []
|
subtitleBlobUrlsRef.current = []
|
||||||
}
|
}
|
||||||
@@ -475,20 +485,50 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
const tracks = video.textTracks
|
const tracks = video.textTracks
|
||||||
if (!tracks || tracks.length === 0) return
|
if (!tracks || tracks.length === 0) return
|
||||||
|
|
||||||
// Disable all tracks first
|
const enableSubtitle = () => {
|
||||||
for (let i = 0; i < tracks.length; i++) {
|
// Disable all tracks first
|
||||||
tracks[i].mode = 'hidden'
|
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) {
|
||||||
|
// Wait for track to have cues before showing
|
||||||
|
if (track.cues && track.cues.length > 0) {
|
||||||
|
track.mode = 'showing'
|
||||||
|
console.log(`🔊 Enabled subtitle track: ${track.label} (${track.language})`)
|
||||||
|
console.log(` - cues available: ${track.cues.length}`)
|
||||||
|
console.log(` - track.mode: ${track.mode}`)
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ Track ${track.label} has no cues yet, waiting...`)
|
||||||
|
// Track not ready yet, will be handled by load event
|
||||||
|
track.mode = 'showing'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable the selected subtitle track
|
// Try to enable immediately
|
||||||
if (settings.subtitle) {
|
enableSubtitle()
|
||||||
|
|
||||||
|
// Also listen for track load events to retry
|
||||||
|
const handleTrackChange = () => {
|
||||||
|
console.log(`🔄 Track changed, re-enabling subtitle`)
|
||||||
|
enableSubtitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
|
tracks[i].addEventListener('load', handleTrackChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
for (let i = 0; i < tracks.length; i++) {
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
const track = tracks[i]
|
tracks[i].removeEventListener('load', handleTrackChange)
|
||||||
if (track.language === settings.subtitle.lang) {
|
|
||||||
track.mode = 'showing'
|
|
||||||
console.log(`Enabled subtitle track: ${track.label} (${track.language})`)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [settings.subtitle, videoRef])
|
}, [settings.subtitle, videoRef])
|
||||||
@@ -500,12 +540,26 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
|
|
||||||
const handleTrackLoad = (e: Event) => {
|
const handleTrackLoad = (e: Event) => {
|
||||||
const track = e.target as HTMLTrackElement
|
const track = e.target as HTMLTrackElement
|
||||||
console.log(`Track loaded: ${track.label} (${track.srclang})`, track.readyState)
|
const textTrack = track.track
|
||||||
|
console.log(`✅ Track loaded: ${track.label} (${track.srclang})`)
|
||||||
|
console.log(` - readyState: ${track.readyState}`)
|
||||||
|
console.log(` - track.mode: ${textTrack.mode}`)
|
||||||
|
console.log(` - track.cues: ${textTrack.cues?.length || 0}`)
|
||||||
|
console.log(` - src: ${track.src}`)
|
||||||
|
|
||||||
|
// Log first few cues if available
|
||||||
|
if (textTrack.cues && textTrack.cues.length > 0) {
|
||||||
|
console.log(` - First cue: ${(textTrack.cues[0] as VTTCue).startTime}s - ${(textTrack.cues[0] as VTTCue).endTime}s: "${(textTrack.cues[0] as VTTCue).text}"`)
|
||||||
|
} else {
|
||||||
|
console.warn(` ⚠️ No cues found in track!`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTrackError = (e: Event) => {
|
const handleTrackError = (e: Event) => {
|
||||||
const track = e.target as HTMLTrackElement
|
const track = e.target as HTMLTrackElement
|
||||||
console.error(`Track error: ${track.label} (${track.srclang})`, track.track.cues?.length)
|
console.error(`❌ Track error: ${track.label} (${track.srclang})`)
|
||||||
|
console.error(` - src: ${track.src}`)
|
||||||
|
console.error(` - readyState: ${track.readyState}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackElements = video.querySelectorAll('track')
|
const trackElements = video.querySelectorAll('track')
|
||||||
@@ -520,7 +574,11 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
for (let i = 0; i < textTracks.length; i++) {
|
for (let i = 0; i < textTracks.length; i++) {
|
||||||
const track = textTracks[i]
|
const track = textTracks[i]
|
||||||
if (track.mode === 'showing') {
|
if (track.mode === 'showing') {
|
||||||
console.log(`Active track: ${track.label}, cues: ${track.cues?.length || 0}, active cues: ${track.activeCues?.length || 0}`)
|
console.log(`🎬 Cuechange: ${track.label}, cues: ${track.cues?.length || 0}, active cues: ${track.activeCues?.length || 0}`)
|
||||||
|
if (track.activeCues && track.activeCues.length > 0) {
|
||||||
|
const cue = track.activeCues[0] as VTTCue
|
||||||
|
console.log(` - Active cue text: "${cue.text}"`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -529,6 +587,15 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
|||||||
textTracks[i].addEventListener('cuechange', handleCueChange)
|
textTracks[i].addEventListener('cuechange', handleCueChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log all text tracks after a delay to see their state
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(`📊 Text tracks summary (${textTracks.length} total):`)
|
||||||
|
for (let i = 0; i < textTracks.length; i++) {
|
||||||
|
const track = textTracks[i]
|
||||||
|
console.log(` [${i}] ${track.label} (${track.language}): mode=${track.mode}, cues=${track.cues?.length || 0}`)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
trackElements.forEach((track) => {
|
trackElements.forEach((track) => {
|
||||||
track.removeEventListener('load', handleTrackLoad)
|
track.removeEventListener('load', handleTrackLoad)
|
||||||
|
|||||||
Reference in New Issue
Block a user