mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 05:46:38 +08:00
优化
This commit is contained in:
@@ -7,6 +7,46 @@ import type { Media } from '../types'
|
|||||||
import { DanmakuStage } from '../components/DanmakuStage'
|
import { DanmakuStage } from '../components/DanmakuStage'
|
||||||
import { PlayerControls } from '../components/PlayerControls'
|
import { PlayerControls } from '../components/PlayerControls'
|
||||||
|
|
||||||
|
type SubtitleCue = {
|
||||||
|
startTime: number
|
||||||
|
endTime: number
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVTTTimestamp(value: string): number {
|
||||||
|
const parts = value.trim().replace(',', '.').split(':')
|
||||||
|
if (parts.length !== 2 && parts.length !== 3) return Number.NaN
|
||||||
|
const seconds = Number(parts.pop())
|
||||||
|
const minutes = Number(parts.pop())
|
||||||
|
const hours = parts.length > 0 ? Number(parts.pop()) : 0
|
||||||
|
if (![hours, minutes, seconds].every(Number.isFinite)) return Number.NaN
|
||||||
|
return hours * 3600 + minutes * 60 + seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWebVTTCues(body: string): SubtitleCue[] {
|
||||||
|
const blocks = body
|
||||||
|
.replace(/^\uFEFF/, '')
|
||||||
|
.replace(/\r\n?/g, '\n')
|
||||||
|
.split(/\n{2,}/)
|
||||||
|
const cues: SubtitleCue[] = []
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
const lines = block.split('\n')
|
||||||
|
const timingIndex = lines.findIndex((line) => line.includes('-->'))
|
||||||
|
if (timingIndex < 0) continue
|
||||||
|
|
||||||
|
const [rawStart, rawEnd] = lines[timingIndex].split('-->', 2)
|
||||||
|
const startTime = parseVTTTimestamp(rawStart)
|
||||||
|
const endTime = parseVTTTimestamp(rawEnd.trim().split(/\s+/, 1)[0])
|
||||||
|
const text = lines.slice(timingIndex + 1).join('\n').trim()
|
||||||
|
if (Number.isFinite(startTime) && Number.isFinite(endTime) && endTime >= startTime && text) {
|
||||||
|
cues.push({ startTime, endTime, text })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cues
|
||||||
|
}
|
||||||
|
|
||||||
type PlayerVideoStageProps = {
|
type PlayerVideoStageProps = {
|
||||||
media: Media | null
|
media: Media | null
|
||||||
/** 媒体元数据加载失败提示(非空时替代「加载中」展示)。 */
|
/** 媒体元数据加载失败提示(非空时替代「加载中」展示)。 */
|
||||||
@@ -89,6 +129,42 @@ export function PlayerVideoStage({
|
|||||||
const revealControlsOnlyRef = useRef(false)
|
const revealControlsOnlyRef = useRef(false)
|
||||||
// 当前展示的字幕文本(由自定义字幕层渲染,100% 透明无黑框)
|
// 当前展示的字幕文本(由自定义字幕层渲染,100% 透明无黑框)
|
||||||
const [activeCueText, setActiveCueText] = useState<string>('')
|
const [activeCueText, setActiveCueText] = useState<string>('')
|
||||||
|
// 独立保存完整 WebVTT 时间轴。HLS seek 会替换媒体源,Chromium 此时可能清空
|
||||||
|
// <track>.track.cues;独立时间轴不受 MediaSource 重挂载和轨道 mode 切换影响。
|
||||||
|
const [subtitleTimeline, setSubtitleTimeline] = useState<{
|
||||||
|
path: string
|
||||||
|
cues: SubtitleCue[]
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedTrack = subs[subtitleIndex]
|
||||||
|
if (!media || subtitleIndex < 0 || !selectedTrack || selectedTrack.delivery !== 'webvtt') {
|
||||||
|
setSubtitleTimeline(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
setSubtitleTimeline(null)
|
||||||
|
fetch(subtitlesAPI.url(media.id, selectedTrack.path), { signal: controller.signal })
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) throw new Error(`subtitle request failed: ${response.status}`)
|
||||||
|
return response.text()
|
||||||
|
})
|
||||||
|
.then((body) => {
|
||||||
|
setSubtitleTimeline({
|
||||||
|
path: selectedTrack.path,
|
||||||
|
cues: parseWebVTTCues(body),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (!(error instanceof DOMException && error.name === 'AbortError')) {
|
||||||
|
// 保留原生 TextTrack 作为请求失败时的降级路径。
|
||||||
|
setSubtitleTimeline(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => controller.abort()
|
||||||
|
}, [media, subs, subtitleIndex])
|
||||||
|
|
||||||
// 监听舞台容器的真实尺寸(响应窗口大小调整和全屏切换)
|
// 监听舞台容器的真实尺寸(响应窗口大小调整和全屏切换)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -170,6 +246,15 @@ export function PlayerVideoStage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateCue = () => {
|
const updateCue = () => {
|
||||||
|
const absoluteTime = video.currentTime + (streamOffset ?? 0)
|
||||||
|
if (subtitleTimeline?.path === selectedTrack.path) {
|
||||||
|
const texts = subtitleTimeline.cues
|
||||||
|
.filter((cue) => absoluteTime >= cue.startTime && absoluteTime <= cue.endTime)
|
||||||
|
.map((cue) => cue.text)
|
||||||
|
setActiveCueText(texts.join('\n'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const selectedEl = video.querySelector<HTMLTrackElement>(
|
const selectedEl = video.querySelector<HTMLTrackElement>(
|
||||||
`track[data-subtitle-index="${subtitleIndex}"]`,
|
`track[data-subtitle-index="${subtitleIndex}"]`,
|
||||||
)
|
)
|
||||||
@@ -188,10 +273,14 @@ export function PlayerVideoStage({
|
|||||||
if (cue && cue.text) texts.push(cue.text)
|
if (cue && cue.text) texts.push(cue.text)
|
||||||
}
|
}
|
||||||
} else if (tt.cues && tt.cues.length > 0) {
|
} else if (tt.cues && tt.cues.length > 0) {
|
||||||
const cur = video.currentTime + (streamOffset ?? 0)
|
|
||||||
for (let i = 0; i < tt.cues.length; i++) {
|
for (let i = 0; i < tt.cues.length; i++) {
|
||||||
const cue = tt.cues[i] as VTTCue
|
const cue = tt.cues[i] as VTTCue
|
||||||
if (cue && cur >= cue.startTime && cur <= cue.endTime && cue.text) {
|
if (
|
||||||
|
cue &&
|
||||||
|
absoluteTime >= cue.startTime &&
|
||||||
|
absoluteTime <= cue.endTime &&
|
||||||
|
cue.text
|
||||||
|
) {
|
||||||
texts.push(cue.text)
|
texts.push(cue.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +340,7 @@ export function PlayerVideoStage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [subtitleIndex, subs, videoRef, media, streamOffset])
|
}, [subtitleIndex, subs, videoRef, media, streamOffset, subtitleTimeline])
|
||||||
|
|
||||||
// 根据视频画面宽高比与舞台宽高比,确定视频在哪个轴向撑满 100%
|
// 根据视频画面宽高比与舞台宽高比,确定视频在哪个轴向撑满 100%
|
||||||
const isWiderThanStage =
|
const isWiderThanStage =
|
||||||
|
|||||||
Reference in New Issue
Block a user