优化
This commit is contained in:
truewhile
2026-08-24 23:06:35 +08:00
parent c0c1412f4a
commit c514fb360e
3 changed files with 138 additions and 5 deletions
+37 -1
View File
@@ -27,6 +27,23 @@ import { PlayerDanmakuPanel } from '../components/PlayerDanmakuPanel'
//
// External subtitles next to the source file are auto-discovered and
// attached as <track> elements.
const SUBTITLE_STORAGE_KEY = 'mmtl.subtitle'
// 初始字幕偏好:localStorage 记录上次选择的轨道(-1=关闭);没有偏好时
// 默认 0(自动加载第一条字幕)。
function initialSubtitleIndex(): number {
try {
const saved = localStorage.getItem(SUBTITLE_STORAGE_KEY)
if (saved !== null && saved !== '') {
const n = parseInt(saved, 10)
if (Number.isFinite(n)) return n
}
} catch {
// ignore
}
return 0
}
export function PlayerPage() {
const { id = '' } = useParams()
const [params, setParams] = useSearchParams()
@@ -40,6 +57,7 @@ export function PlayerPage() {
const [media, setMedia] = useState<Media | null>(null)
const [mode, setMode] = useState<PlayerMode>('direct')
const [subs, setSubs] = useState<SubtitleTrack[]>([])
const [subtitleIndex, setSubtitleIndex] = useState<number>(initialSubtitleIndex)
const [hlsUnavailable, setHlsUnavailable] = useState(false)
const [playerError, setPlayerError] = useState('')
// 「客户端直连解码」模式:宿主机不转码,播放器强制 direct play、隐藏 HLS 切换。
@@ -156,7 +174,13 @@ export function PlayerPage() {
})
subtitlesAPI
.list(id)
.then((tracks) => setSubs(tracks ?? []))
.then((tracks) => {
const list = tracks ?? []
setSubs(list)
// 记忆的轨道下标可能超出当前媒体的轨道数(不同媒体字幕数量不同),
// 越界时回退到第一条;无字幕则关闭。
setSubtitleIndex((cur) => (cur >= list.length ? (list.length > 0 ? 0 : -1) : cur))
})
.catch(() => setSubs([]))
}, [id, params, directOnly])
@@ -246,6 +270,16 @@ export function PlayerPage() {
setParams(params, { replace: true })
}, [mode, params, setParams])
// 用户切换字幕轨道:-1=关闭;记忆偏好,下次播放默认沿用。
const selectSubtitle = useCallback((index: number) => {
setSubtitleIndex(index)
try {
localStorage.setItem(SUBTITLE_STORAGE_KEY, String(index))
} catch {
// ignore
}
}, [])
const handleVideoError = useCallback(() => {
// 浏览器对 <video src> 的错误描述非常有限,把详细原因
// 转给开发者控制台 + 一条 toast;常见原因是 codec 不支持。
@@ -281,6 +315,8 @@ export function PlayerPage() {
media={media}
playerError={playerError}
subs={subs}
subtitleIndex={subtitleIndex}
onSelectSubtitle={selectSubtitle}
videoRef={ref}
onVideoError={handleVideoError}
danmakuEnabled={danmakuEnabled}
+27 -2
View File
@@ -1,3 +1,4 @@
import { useEffect } from 'react'
import type { ReactNode, RefObject } from 'react'
import { subtitlesAPI, type SubtitleTrack } from '../api/subtitles'
@@ -10,6 +11,9 @@ type PlayerVideoStageProps = {
media: Media | null
playerError: string
subs: SubtitleTrack[]
/** 当前激活字幕轨道:-1=关闭,0..n-1=对应轨道。 */
subtitleIndex: number
onSelectSubtitle: (index: number) => void
videoRef: RefObject<HTMLVideoElement>
onVideoError: () => void
danmakuEnabled: boolean
@@ -30,6 +34,8 @@ export function PlayerVideoStage({
media,
playerError,
subs,
subtitleIndex,
onSelectSubtitle,
videoRef,
onVideoError,
danmakuEnabled,
@@ -58,6 +64,23 @@ export function PlayerVideoStage({
else void stage.requestFullscreen?.()
}
// 把用户选择的字幕轨道应用到 <video> 的 textTracks(跨浏览器显式设置
// mode;<track default> 只影响初始值,部分浏览器不会自动显示)。
useEffect(() => {
const video = videoRef.current
if (!video || subs.length === 0) return
const apply = () => {
const tracks = video.textTracks
for (let i = 0; i < tracks.length; i++) {
tracks[i].mode = i === subtitleIndex ? 'showing' : 'disabled'
}
}
apply()
// 轨道元数据就绪后再应用一次,确保字幕真正可见
video.addEventListener('loadedmetadata', apply)
return () => video.removeEventListener('loadedmetadata', apply)
}, [subtitleIndex, subs, videoRef])
return (
<div
className="relative flex flex-1 items-center justify-center overflow-hidden bg-black"
@@ -73,14 +96,13 @@ export function PlayerVideoStage({
className="relative z-0 max-h-screen w-full max-w-[1600px] bg-black"
onError={onVideoError}
>
{subs.map((track, index) => (
{subs.map((track) => (
<track
key={track.path}
kind="subtitles"
src={subtitlesAPI.url(media.id, track.path)}
srcLang={track.lang}
label={track.label || track.lang}
default={index === 0}
/>
))}
</video>
@@ -98,6 +120,9 @@ export function PlayerVideoStage({
/>
<PlayerControls
videoRef={videoRef}
subs={subs}
subtitleIndex={subtitleIndex}
onSelectSubtitle={onSelectSubtitle}
danmakuOpen={danmakuOpen}
danmakuEnabled={danmakuEnabled}
onToggleDanmaku={onToggleDanmaku}