mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-03 04:26:36 +08:00
优化
优化
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user