mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-13 16:46:37 +08:00
优化
This commit is contained in:
+120
-36
@@ -13,6 +13,7 @@ import type { Media } from '../types'
|
|||||||
import { getSeriesKey, seriesTitleFromPath } from '../utils/groupSeries'
|
import { getSeriesKey, seriesTitleFromPath } from '../utils/groupSeries'
|
||||||
import { isRemoteEmbyID } from '../utils/remoteEmby'
|
import { isRemoteEmbyID } from '../utils/remoteEmby'
|
||||||
import { pickPlayerMode, needsTranscodeForBrowser, isDirectStreamMedia, type PlayerMode } from './playerPageModel'
|
import { pickPlayerMode, needsTranscodeForBrowser, isDirectStreamMedia, type PlayerMode } from './playerPageModel'
|
||||||
|
import { classifyDirectPlayError } from './directPlayError'
|
||||||
import { apiErrorMessage } from './StrmManagePage'
|
import { apiErrorMessage } from './StrmManagePage'
|
||||||
import { PlayerTopBar } from './PlayerTopBar'
|
import { PlayerTopBar } from './PlayerTopBar'
|
||||||
import { PlayerVideoStage } from './PlayerVideoStage'
|
import { PlayerVideoStage } from './PlayerVideoStage'
|
||||||
@@ -42,6 +43,9 @@ export function PlayerPage() {
|
|||||||
const ref = useRef<HTMLVideoElement>(null)
|
const ref = useRef<HTMLVideoElement>(null)
|
||||||
const hlsRef = useRef<Hls | null>(null)
|
const hlsRef = useRef<Hls | null>(null)
|
||||||
const lastSentRef = useRef(0)
|
const lastSentRef = useRef(0)
|
||||||
|
const directRetryRef = useRef(false)
|
||||||
|
const retryingDirectRef = useRef(false)
|
||||||
|
const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
const [media, setMedia] = useState<Media | null>(null)
|
const [media, setMedia] = useState<Media | null>(null)
|
||||||
const [mode, setMode] = useState<PlayerMode>('direct')
|
const [mode, setMode] = useState<PlayerMode>('direct')
|
||||||
@@ -178,11 +182,40 @@ export function PlayerPage() {
|
|||||||
setDanmakuSelectedSource('')
|
setDanmakuSelectedSource('')
|
||||||
setDanmakuInfo(null)
|
setDanmakuInfo(null)
|
||||||
setDanmakuSearching(true)
|
setDanmakuSearching(true)
|
||||||
|
directRetryRef.current = false
|
||||||
|
retryingDirectRef.current = false
|
||||||
|
if (fallbackTimerRef.current) {
|
||||||
|
clearTimeout(fallbackTimerRef.current)
|
||||||
|
fallbackTimerRef.current = null
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (fallbackTimerRef.current) {
|
||||||
|
clearTimeout(fallbackTimerRef.current)
|
||||||
|
fallbackTimerRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
// 依赖收敛为 mode 参数的字符串值:避免 params 对象引用每次变化都重复拉取元数据
|
// 依赖收敛为 mode 参数的字符串值:避免 params 对象引用每次变化都重复拉取元数据
|
||||||
const modeParam = params.get('mode') as PlayerMode | null
|
const modeParam = params.get('mode') as PlayerMode | null
|
||||||
|
|
||||||
|
const setPlaybackMode = useCallback(
|
||||||
|
(next: PlayerMode) => {
|
||||||
|
setMode(next)
|
||||||
|
const nextParams = new URLSearchParams(window.location.search)
|
||||||
|
nextParams.set('mode', next)
|
||||||
|
setParams(nextParams, { replace: true })
|
||||||
|
},
|
||||||
|
[setParams],
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearFallbackTimer = useCallback(() => {
|
||||||
|
if (fallbackTimerRef.current) {
|
||||||
|
clearTimeout(fallbackTimerRef.current)
|
||||||
|
fallbackTimerRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Load metadata and pick a default mode.
|
// Load metadata and pick a default mode.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@@ -249,7 +282,15 @@ export function PlayerPage() {
|
|||||||
void import('hls.js').then(({ default: HlsCtor }) => {
|
void import('hls.js').then(({ default: HlsCtor }) => {
|
||||||
if (cancelled || !ref.current) return
|
if (cancelled || !ref.current) return
|
||||||
if (HlsCtor.isSupported()) {
|
if (HlsCtor.isSupported()) {
|
||||||
const hls = new HlsCtor({ enableWorker: true, lowLatencyMode: false })
|
const hls = new HlsCtor({
|
||||||
|
enableWorker: true,
|
||||||
|
lowLatencyMode: false,
|
||||||
|
// The server waits up to 45s for the first segment. Slow two-core
|
||||||
|
// hosts and remote STRM sources regularly need more than hls.js's
|
||||||
|
// 10s default, which otherwise aborts a healthy transcode.
|
||||||
|
manifestLoadingTimeOut: 60_000,
|
||||||
|
manifestLoadingMaxRetry: 1,
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
video.currentTime = 0
|
video.currentTime = 0
|
||||||
} catch {
|
} catch {
|
||||||
@@ -274,9 +315,7 @@ export function PlayerPage() {
|
|||||||
setHlsUnavailable(true)
|
setHlsUnavailable(true)
|
||||||
setPlayerError('HLS 转码不可用,正在尝试直接播放原始文件。若出现有画面无声音,通常是 MKV/AC3/EAC3 音轨需要配置本机 ffmpeg 转码为 AAC。')
|
setPlayerError('HLS 转码不可用,正在尝试直接播放原始文件。若出现有画面无声音,通常是 MKV/AC3/EAC3 音轨需要配置本机 ffmpeg 转码为 AAC。')
|
||||||
toast.error('HLS 转码失败,尝试切换到直接播放')
|
toast.error('HLS 转码失败,尝试切换到直接播放')
|
||||||
setMode('direct')
|
setPlaybackMode('direct')
|
||||||
params.set('mode', 'direct')
|
|
||||||
setParams(params, { replace: true })
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
@@ -296,13 +335,13 @@ export function PlayerPage() {
|
|||||||
setHlsUnavailable(true)
|
setHlsUnavailable(true)
|
||||||
setPlayerError('当前浏览器不支持 HLS,正在尝试直接播放。')
|
setPlayerError('当前浏览器不支持 HLS,正在尝试直接播放。')
|
||||||
toast.error('当前浏览器不支持 HLS,降级到直接播放')
|
toast.error('当前浏览器不支持 HLS,降级到直接播放')
|
||||||
setMode('direct')
|
setPlaybackMode('direct')
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setHlsUnavailable(true)
|
setHlsUnavailable(true)
|
||||||
setPlayerError('HLS 播放组件加载失败,正在尝试直接播放。')
|
setPlayerError('HLS 播放组件加载失败,正在尝试直接播放。')
|
||||||
setMode('direct')
|
setPlaybackMode('direct')
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const url = streamURL(mediaId)
|
const url = streamURL(mediaId)
|
||||||
@@ -310,18 +349,32 @@ export function PlayerPage() {
|
|||||||
// 其它异步播放器状态更新不应重启同一个直连请求;STRM 的重定向/换链
|
// 其它异步播放器状态更新不应重启同一个直连请求;STRM 的重定向/换链
|
||||||
// 比本地文件慢,重启请求可能产生一个短暂但会触发 onError 的中断。
|
// 比本地文件慢,重启请求可能产生一个短暂但会触发 onError 的中断。
|
||||||
if (video.src !== absoluteURL) {
|
if (video.src !== absoluteURL) {
|
||||||
|
directRetryRef.current = false
|
||||||
|
clearFallbackTimer()
|
||||||
video.src = url
|
video.src = url
|
||||||
|
void video.play().catch(() => undefined)
|
||||||
}
|
}
|
||||||
if (hlsUnavailable && needsTranscodeForBrowser(currentMedia)) {
|
if (hlsUnavailable && needsTranscodeForBrowser(currentMedia)) {
|
||||||
setPlayerError('当前正在直连播放原始文件;此封装或音轨浏览器兼容性有限,可能只有画面没有声音。请配置本机 ffmpeg 后切回 HLS 转码播放。')
|
setPlayerError('当前正在直连播放原始文件;此封装或音轨浏览器兼容性有限,可能只有画面没有声音。请配置本机 ffmpeg 后切回 HLS 转码播放。')
|
||||||
}
|
}
|
||||||
void video.play().catch(() => undefined)
|
|
||||||
}
|
}
|
||||||
|
const onPlaying = () => clearFallbackTimer()
|
||||||
|
video.addEventListener('playing', onPlaying)
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
|
video.removeEventListener('playing', onPlaying)
|
||||||
teardownHls()
|
teardownHls()
|
||||||
}
|
}
|
||||||
}, [activeBurnedSubtitleStream, hlsUnavailable, hlsStartSec, mediaId, mode, params, setParams, teardownHls])
|
}, [
|
||||||
|
activeBurnedSubtitleStream,
|
||||||
|
clearFallbackTimer,
|
||||||
|
hlsUnavailable,
|
||||||
|
hlsStartSec,
|
||||||
|
mediaId,
|
||||||
|
mode,
|
||||||
|
setPlaybackMode,
|
||||||
|
teardownHls,
|
||||||
|
])
|
||||||
|
|
||||||
// Stop host ffmpeg when leaving this HLS player. The keepalive request also
|
// Stop host ffmpeg when leaving this HLS player. The keepalive request also
|
||||||
// survives route navigation while the component is being torn down.
|
// survives route navigation while the component is being torn down.
|
||||||
@@ -372,21 +425,30 @@ export function PlayerPage() {
|
|||||||
}
|
}
|
||||||
const video = ref.current
|
const video = ref.current
|
||||||
if (!video) return
|
if (!video) return
|
||||||
|
let onSeeked: (() => void) | undefined
|
||||||
const applyResume = () => {
|
const applyResume = () => {
|
||||||
if (resumePosition > 0 && Math.abs(video.currentTime - resumePosition) > 2) {
|
if (resumePosition > 0 && Math.abs(video.currentTime - resumePosition) > 2) {
|
||||||
video.currentTime = resumePosition
|
onSeeked = () => {
|
||||||
setInitialSeekDone(true)
|
setInitialSeekDone(true)
|
||||||
const m = Math.floor(resumePosition / 60)
|
const m = Math.floor(resumePosition / 60)
|
||||||
const s = Math.floor(resumePosition % 60)
|
const s = Math.floor(resumePosition % 60)
|
||||||
const timeStr = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
const timeStr = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
||||||
toast.success(`已恢复上次播放进度至 ${timeStr}`, { duration: 2500 })
|
toast.success(`已恢复上次播放进度至 ${timeStr}`, { duration: 2500 })
|
||||||
}
|
}
|
||||||
|
video.addEventListener('seeked', onSeeked, { once: true })
|
||||||
|
video.currentTime = resumePosition
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (video.readyState >= 1) {
|
setInitialSeekDone(true)
|
||||||
|
}
|
||||||
|
if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
|
||||||
applyResume()
|
applyResume()
|
||||||
} else {
|
} else {
|
||||||
video.addEventListener('loadedmetadata', applyResume, { once: true })
|
video.addEventListener('canplay', applyResume, { once: true })
|
||||||
return () => video.removeEventListener('loadedmetadata', applyResume)
|
}
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('canplay', applyResume)
|
||||||
|
if (onSeeked) video.removeEventListener('seeked', onSeeked)
|
||||||
}
|
}
|
||||||
}, [resumePosition, initialSeekDone, mode, hlsStartSec])
|
}, [resumePosition, initialSeekDone, mode, hlsStartSec])
|
||||||
|
|
||||||
@@ -614,17 +676,14 @@ export function PlayerPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setHlsStartSec(ref.current?.currentTime || 0)
|
setHlsStartSec(ref.current?.currentTime || 0)
|
||||||
setMode('hls')
|
setPlaybackMode('hls')
|
||||||
params.set('mode', 'hls')
|
|
||||||
setParams(params, { replace: true })
|
|
||||||
}, [
|
}, [
|
||||||
directOnly,
|
directOnly,
|
||||||
directOnlyKnown,
|
directOnlyKnown,
|
||||||
isDirectStream,
|
isDirectStream,
|
||||||
mode,
|
mode,
|
||||||
params,
|
|
||||||
selectedSubtitle?.delivery,
|
selectedSubtitle?.delivery,
|
||||||
setParams,
|
setPlaybackMode,
|
||||||
])
|
])
|
||||||
|
|
||||||
const toggleMode = useCallback(() => {
|
const toggleMode = useCallback(() => {
|
||||||
@@ -636,10 +695,8 @@ export function PlayerPage() {
|
|||||||
if (next === 'hls') {
|
if (next === 'hls') {
|
||||||
setHlsStartSec(0)
|
setHlsStartSec(0)
|
||||||
}
|
}
|
||||||
setMode(next)
|
setPlaybackMode(next)
|
||||||
params.set('mode', next)
|
}, [isDirectStream, mode, setPlaybackMode])
|
||||||
setParams(params, { replace: true })
|
|
||||||
}, [isDirectStream, mode, params, setParams])
|
|
||||||
|
|
||||||
const handleSeekAbsolute = useCallback(
|
const handleSeekAbsolute = useCallback(
|
||||||
(absoluteSec: number) => {
|
(absoluteSec: number) => {
|
||||||
@@ -689,17 +746,47 @@ export function PlayerPage() {
|
|||||||
setSubtitleIndex(index)
|
setSubtitleIndex(index)
|
||||||
if (nextTrack?.delivery === 'burn' && mode !== 'hls' && !directOnly && !isDirectStream) {
|
if (nextTrack?.delivery === 'burn' && mode !== 'hls' && !directOnly && !isDirectStream) {
|
||||||
setHlsStartSec(ref.current?.currentTime || 0)
|
setHlsStartSec(ref.current?.currentTime || 0)
|
||||||
setMode('hls')
|
setPlaybackMode('hls')
|
||||||
params.set('mode', 'hls')
|
|
||||||
setParams(params, { replace: true })
|
|
||||||
}
|
}
|
||||||
}, [directOnly, hlsStartSec, isDirectStream, mode, params, setParams, subs, subtitleIndex])
|
}, [directOnly, hlsStartSec, isDirectStream, mode, setPlaybackMode, subs, subtitleIndex])
|
||||||
|
|
||||||
const handleVideoError = useCallback(() => {
|
const handleVideoError = useCallback(() => {
|
||||||
// 浏览器对 <video src> 的错误描述非常有限,把详细原因
|
const video = ref.current
|
||||||
// 转给开发者控制台 + 一条 toast;常见原因是 codec 不支持。
|
if (mode !== 'direct') {
|
||||||
if (mode === 'direct') {
|
setPlayerError('视频播放失败,请检查文件是否存在,或确认 ffmpeg 已正确配置。')
|
||||||
if (isRemoteEmbyID(media?.id) || isDirectStreamMedia(media)) {
|
toast.error('视频播放失败,请检查文件是否存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryingDirectRef.current) return
|
||||||
|
|
||||||
|
const expectedSrc = mediaId ? new URL(streamURL(mediaId), window.location.href).href : ''
|
||||||
|
const action = classifyDirectPlayError({
|
||||||
|
errorCode: video?.error?.code,
|
||||||
|
readyState: video?.readyState ?? 0,
|
||||||
|
elementSrc: video?.src ?? '',
|
||||||
|
expectedSrc,
|
||||||
|
alreadyRetried: directRetryRef.current,
|
||||||
|
})
|
||||||
|
if (action === 'ignore') return
|
||||||
|
|
||||||
|
if (action === 'retry' && video && mediaId) {
|
||||||
|
directRetryRef.current = true
|
||||||
|
retryingDirectRef.current = true
|
||||||
|
try {
|
||||||
|
video.load()
|
||||||
|
void video.play().catch(() => undefined)
|
||||||
|
} finally {
|
||||||
|
retryingDirectRef.current = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackDirectPlay = () => {
|
||||||
|
if (modeRef.current !== 'direct') return
|
||||||
|
const current = ref.current
|
||||||
|
if (current && current.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return
|
||||||
|
if (isRemoteEmbyID(mediaRef.current?.id) || isDirectStreamMedia(mediaRef.current)) {
|
||||||
setPlayerError('直接播放失败。该媒体为远程 Emby 挂载直连播放(不进行转码);当前浏览器可能不支持该视频编码或音频格式,建议使用外部播放器(如 PotPlayer / VLC / IINA)播放。')
|
setPlayerError('直接播放失败。该媒体为远程 Emby 挂载直连播放(不进行转码);当前浏览器可能不支持该视频编码或音频格式,建议使用外部播放器(如 PotPlayer / VLC / IINA)播放。')
|
||||||
toast.error('直接播放失败,建议使用外部播放器')
|
toast.error('直接播放失败,建议使用外部播放器')
|
||||||
} else if (directOnly) {
|
} else if (directOnly) {
|
||||||
@@ -710,16 +797,13 @@ export function PlayerPage() {
|
|||||||
toast.error('直接播放失败,HLS 转码不可用')
|
toast.error('直接播放失败,HLS 转码不可用')
|
||||||
} else {
|
} else {
|
||||||
toast.error('直接播放失败,切换到 HLS 转码')
|
toast.error('直接播放失败,切换到 HLS 转码')
|
||||||
setMode('hls')
|
setPlaybackMode('hls')
|
||||||
params.set('mode', 'hls')
|
|
||||||
setParams(params, { replace: true })
|
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlayerError('视频播放失败,请检查文件是否存在,或确认 ffmpeg 已正确配置。')
|
clearFallbackTimer()
|
||||||
toast.error('视频播放失败,请检查文件是否存在')
|
fallbackTimerRef.current = setTimeout(fallbackDirectPlay, 1500)
|
||||||
}, [directOnly, hlsUnavailable, media, mode, params, setParams])
|
}, [clearFallbackTimer, directOnly, hlsUnavailable, mediaId, mode, setPlaybackMode])
|
||||||
|
|
||||||
const danmakuAutoTitle =
|
const danmakuAutoTitle =
|
||||||
danmakuInfo?.animeTitle ||
|
danmakuInfo?.animeTitle ||
|
||||||
|
|||||||
@@ -135,6 +135,25 @@ export function PlayerVideoStage({
|
|||||||
path: string
|
path: string
|
||||||
cues: SubtitleCue[]
|
cues: SubtitleCue[]
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
// 直连 302 尚未完成时插入 <track> 会中断加载并误报播放失败;等 canplay 再挂。
|
||||||
|
const [tracksArmed, setTracksArmed] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTracksArmed(false)
|
||||||
|
const video = videoRef.current
|
||||||
|
if (!video || !media?.id) return
|
||||||
|
const arm = () => setTracksArmed(true)
|
||||||
|
if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
|
||||||
|
arm()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
video.addEventListener('canplay', arm)
|
||||||
|
video.addEventListener('playing', arm)
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('canplay', arm)
|
||||||
|
video.removeEventListener('playing', arm)
|
||||||
|
}
|
||||||
|
}, [media?.id, videoRef])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedTrack = subs[subtitleIndex]
|
const selectedTrack = subs[subtitleIndex]
|
||||||
@@ -383,7 +402,8 @@ export function PlayerVideoStage({
|
|||||||
className="h-full w-full object-contain bg-black"
|
className="h-full w-full object-contain bg-black"
|
||||||
onError={onVideoError}
|
onError={onVideoError}
|
||||||
>
|
>
|
||||||
{subs.map((track, index) =>
|
{tracksArmed &&
|
||||||
|
subs.map((track, index) =>
|
||||||
track.delivery === 'burn' ? null : (
|
track.delivery === 'burn' ? null : (
|
||||||
<track
|
<track
|
||||||
key={track.path}
|
key={track.path}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export type DirectPlayErrorAction = 'ignore' | 'retry' | 'fallback'
|
||||||
|
|
||||||
|
const MEDIA_ERR_ABORTED = 1
|
||||||
|
const HAVE_CURRENT_DATA = 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chromium 常把被中断的直连(换 src、插入 track、302 未完成就 seek)
|
||||||
|
* 报成 error。真正不兼容应 fallback;瞬时中断应忽略或静默重试一次。
|
||||||
|
*/
|
||||||
|
export function classifyDirectPlayError(input: {
|
||||||
|
errorCode: number | undefined | null
|
||||||
|
readyState: number
|
||||||
|
elementSrc: string
|
||||||
|
expectedSrc: string
|
||||||
|
alreadyRetried: boolean
|
||||||
|
}): DirectPlayErrorAction {
|
||||||
|
const code = input.errorCode ?? 0
|
||||||
|
if (code === MEDIA_ERR_ABORTED) return 'ignore'
|
||||||
|
if (input.readyState >= HAVE_CURRENT_DATA) return 'ignore'
|
||||||
|
if (!input.elementSrc) return 'ignore'
|
||||||
|
if (input.expectedSrc && input.elementSrc !== input.expectedSrc) return 'ignore'
|
||||||
|
if (!input.alreadyRetried) return 'retry'
|
||||||
|
return 'fallback'
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { classifyDirectPlayError } from './directPlayError.ts'
|
||||||
|
|
||||||
|
function check(name: string, cond: boolean) {
|
||||||
|
if (!cond) throw new Error(`classifyDirectPlayError: ${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const src = 'http://nas.local/api/stream/m1?token=abc'
|
||||||
|
|
||||||
|
check(
|
||||||
|
'aborted load is ignored',
|
||||||
|
classifyDirectPlayError({
|
||||||
|
errorCode: 1,
|
||||||
|
readyState: 0,
|
||||||
|
elementSrc: src,
|
||||||
|
expectedSrc: src,
|
||||||
|
alreadyRetried: false,
|
||||||
|
}) === 'ignore',
|
||||||
|
)
|
||||||
|
|
||||||
|
check(
|
||||||
|
'already-playing error is ignored',
|
||||||
|
classifyDirectPlayError({
|
||||||
|
errorCode: 4,
|
||||||
|
readyState: 3,
|
||||||
|
elementSrc: src,
|
||||||
|
expectedSrc: src,
|
||||||
|
alreadyRetried: false,
|
||||||
|
}) === 'ignore',
|
||||||
|
)
|
||||||
|
|
||||||
|
check(
|
||||||
|
'stale src error is ignored',
|
||||||
|
classifyDirectPlayError({
|
||||||
|
errorCode: 4,
|
||||||
|
readyState: 0,
|
||||||
|
elementSrc: 'http://nas.local/old',
|
||||||
|
expectedSrc: src,
|
||||||
|
alreadyRetried: false,
|
||||||
|
}) === 'ignore',
|
||||||
|
)
|
||||||
|
|
||||||
|
check(
|
||||||
|
'empty src during reload is ignored',
|
||||||
|
classifyDirectPlayError({
|
||||||
|
errorCode: 4,
|
||||||
|
readyState: 0,
|
||||||
|
elementSrc: '',
|
||||||
|
expectedSrc: src,
|
||||||
|
alreadyRetried: false,
|
||||||
|
}) === 'ignore',
|
||||||
|
)
|
||||||
|
|
||||||
|
check(
|
||||||
|
'first decode/network miss retries',
|
||||||
|
classifyDirectPlayError({
|
||||||
|
errorCode: 4,
|
||||||
|
readyState: 0,
|
||||||
|
elementSrc: src,
|
||||||
|
expectedSrc: src,
|
||||||
|
alreadyRetried: false,
|
||||||
|
}) === 'retry',
|
||||||
|
)
|
||||||
|
|
||||||
|
check(
|
||||||
|
'second failure falls back to HLS',
|
||||||
|
classifyDirectPlayError({
|
||||||
|
errorCode: 4,
|
||||||
|
readyState: 0,
|
||||||
|
elementSrc: src,
|
||||||
|
expectedSrc: src,
|
||||||
|
alreadyRetried: true,
|
||||||
|
}) === 'fallback',
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('playerPageModel.test.ts ok')
|
||||||
+2
-1
@@ -20,5 +20,6 @@
|
|||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"],
|
||||||
|
"exclude": ["src/**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user