|
|
|
@@ -9,6 +9,8 @@ import { createFile } from "mp4box"
|
|
|
|
|
import type { Movie, Sample } from "mp4box"
|
|
|
|
|
|
|
|
|
|
// ── 常量 ──
|
|
|
|
|
/** 初始化预解码最大帧数(约 2 秒 @30fps),后续帧通过 decodeAroundPosition 按需解码 */
|
|
|
|
|
const MAX_INIT_FRAMES = 60
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 规范化 mp4box 提取的 codec 字符串为 WebCodecs 兼容格式
|
|
|
|
@@ -241,11 +243,9 @@ export function useCanvasPlayer(
|
|
|
|
|
|
|
|
|
|
// ── 内部引用 ──
|
|
|
|
|
const decoderRef = useRef<VideoDecoder | null>(null)
|
|
|
|
|
const frameQueueRef = useRef(new FrameQueue(1024))
|
|
|
|
|
const frameQueueRef = useRef(new FrameQueue(600))
|
|
|
|
|
/** 已解码的片段索引集合,用于按需解码(先标记防重入,失败时移除允许重试) */
|
|
|
|
|
const decodedSegmentsRef = useRef(new Set<number>())
|
|
|
|
|
/** 每个片段已解码的帧数,用于 decodeAroundPosition 继续未完成解码 */
|
|
|
|
|
const decodedFrameCountsRef = useRef(new Map<number, number>())
|
|
|
|
|
/** 解码代数计数器,seek 时递增以作废正在进行的异步解码 */
|
|
|
|
|
const decodeGenerationRef = useRef(0)
|
|
|
|
|
const rafRef = useRef<number>(0)
|
|
|
|
@@ -478,10 +478,8 @@ export function useCanvasPlayer(
|
|
|
|
|
// ── 初始化 VideoDecoder 并解码指定片段 ──
|
|
|
|
|
const decodeSegment = useCallback(
|
|
|
|
|
async (_buffer: ArrayBuffer, meta: SegmentMeta, maxFrames?: number): Promise<void> => {
|
|
|
|
|
console.log(`[DIAG_v2] >>> decodeSegment ENTER assetId=${meta.assetId}`)
|
|
|
|
|
if (isDestroyedRef.current) return
|
|
|
|
|
|
|
|
|
|
const gen = decodeGenerationRef.current
|
|
|
|
|
let decoderReady = false
|
|
|
|
|
|
|
|
|
|
// 配置解码器(每个片段可能需要不同的 codec/分辨率)
|
|
|
|
@@ -573,26 +571,12 @@ export function useCanvasPlayer(
|
|
|
|
|
let decodedCount = 0
|
|
|
|
|
let skippedCount = 0
|
|
|
|
|
let decodeErrors = 0
|
|
|
|
|
// 永远从 0 开始,确保第一个 sample 是关键帧
|
|
|
|
|
for (let si = 0; si < samplesCollected.length; si++) {
|
|
|
|
|
const sample = samplesCollected[si]
|
|
|
|
|
// seek/重播时旧的 decodeSegment 立即退出,不往帧队列推废帧
|
|
|
|
|
if (decodeGenerationRef.current !== gen) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[useCanvasPlayer] decodeSegment generation changed, aborting ${meta.assetId}`,
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
for (const sample of samplesCollected) {
|
|
|
|
|
if (!sample.data || isDestroyedRef.current) {
|
|
|
|
|
skippedCount++
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Strict Mode 双执行时,destroy() 会关闭 decoder
|
|
|
|
|
// 如果 decoder 已关闭,直接返回,让第二轮 init 重新解码
|
|
|
|
|
if (decoder.state === "closed") {
|
|
|
|
|
console.warn("[useCanvasPlayer] Decoder closed during decode, skipping segment")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (decoder.state === "closed") break
|
|
|
|
|
// 初始化阶段限制解码帧数,避免帧缓冲溢出
|
|
|
|
|
if (maxFrames && decodedCount >= maxFrames) {
|
|
|
|
|
console.log(
|
|
|
|
@@ -609,20 +593,9 @@ export function useCanvasPlayer(
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await Promise.race([
|
|
|
|
|
decoder.decode(chunk),
|
|
|
|
|
new Promise((_, reject) =>
|
|
|
|
|
setTimeout(() => reject(new Error("decode chunk timeout 3s")), 3000),
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
await decoder.decode(chunk) // 修复:await 捕获异步错误
|
|
|
|
|
decodedCount++
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (e instanceof Error && e.message === "decode chunk timeout 3s") {
|
|
|
|
|
console.warn(
|
|
|
|
|
`[useCanvasPlayer] Decode chunk timeout at sample ${decodedCount}, aborting segment ${meta.assetId}`,
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
decodeErrors++
|
|
|
|
|
console.warn(`[useCanvasPlayer] Decode chunk error (${decodeErrors}):`, e)
|
|
|
|
|
// 连续 3 次解码失败,放弃当前片段并报告错误
|
|
|
|
@@ -644,26 +617,71 @@ export function useCanvasPlayer(
|
|
|
|
|
`[useCanvasPlayer] Segment ${meta.assetId}: decoded ${decodedCount}, skipped ${skippedCount}, errors ${decodeErrors}, decoder.state=${decoder.state}`,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 记录该片段已解码帧数(用于 decodeAroundPosition 续解码)
|
|
|
|
|
const totalDecoded = decodedCount
|
|
|
|
|
const metaArr = segmentMetaRef.current
|
|
|
|
|
for (let idx = 0; idx < metaArr.length; idx++) {
|
|
|
|
|
if (metaArr[idx].assetId === meta.assetId) {
|
|
|
|
|
if (totalDecoded >= samplesCollected.length) {
|
|
|
|
|
decodedFrameCountsRef.current.set(idx, Infinity)
|
|
|
|
|
} else {
|
|
|
|
|
decodedFrameCountsRef.current.set(idx, totalDecoded)
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
// flush 仅在解码器状态正常时执行
|
|
|
|
|
if (decoder.state === "configured") {
|
|
|
|
|
try {
|
|
|
|
|
await decoder.flush()
|
|
|
|
|
console.log(`[useCanvasPlayer] Segment ${meta.assetId}: flush complete`)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn("[useCanvasPlayer] Decoder flush error:", e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 不做 flush,每个片段独立解码器,flush 在某些 Chromium 版本下会永久挂起
|
|
|
|
|
console.log(`[useCanvasPlayer] Segment ${meta.assetId}: decode done, skip flush`)
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按需解码当前播放位置 ±1 个片段。
|
|
|
|
|
* 在渲染循环中定期调用,避免一次性解码所有片段导致环形缓冲区溢出丢帧。
|
|
|
|
|
* 使用"先标记再解码"模式防止并发重复解码,失败时移除标记允许重试。
|
|
|
|
|
*/
|
|
|
|
|
const decodeAroundPosition = useCallback(
|
|
|
|
|
async (currentTime: number) => {
|
|
|
|
|
const metas = segmentMetaRef.current
|
|
|
|
|
if (!metas || metas.length === 0) return
|
|
|
|
|
|
|
|
|
|
// 记录当前代数,seek 后代数变化则中止
|
|
|
|
|
const gen = decodeGenerationRef.current
|
|
|
|
|
|
|
|
|
|
let targetIdx = -1
|
|
|
|
|
let acc = 0
|
|
|
|
|
for (let i = 0; i < metas.length; i++) {
|
|
|
|
|
const dur = metas[i].globalEndTime - metas[i].globalStartTime
|
|
|
|
|
if (currentTime < acc + dur) {
|
|
|
|
|
targetIdx = i
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
acc += dur
|
|
|
|
|
}
|
|
|
|
|
if (targetIdx === -1) targetIdx = metas.length - 1
|
|
|
|
|
|
|
|
|
|
for (
|
|
|
|
|
let i = Math.max(0, targetIdx - 1);
|
|
|
|
|
i <= Math.min(metas.length - 1, targetIdx + 1);
|
|
|
|
|
i++
|
|
|
|
|
) {
|
|
|
|
|
// seek 已作废当前解码任务
|
|
|
|
|
if (decodeGenerationRef.current !== gen) return
|
|
|
|
|
if (decodedSegmentsRef.current.has(i)) continue
|
|
|
|
|
const meta = metas[i]
|
|
|
|
|
const buffer = segmentDataRef.current.get(meta.assetId)
|
|
|
|
|
if (!buffer) continue
|
|
|
|
|
// 先标记为解码中,防止下一帧渲染时重复发起解码
|
|
|
|
|
decodedSegmentsRef.current.add(i)
|
|
|
|
|
try {
|
|
|
|
|
await decodeSegment(buffer, meta, 300)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 解码失败则移除标记,允许后续重试
|
|
|
|
|
decodedSegmentsRef.current.delete(i)
|
|
|
|
|
console.warn(`[useCanvasPlayer] 按需解码片段 ${i} 失败:`, e)
|
|
|
|
|
}
|
|
|
|
|
// await 后再次检查代数,seek 期间不更新标记
|
|
|
|
|
if (decodeGenerationRef.current !== gen) return
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[decodeSegment],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── 标题绘制 ──
|
|
|
|
|
const drawTitle = useCallback(
|
|
|
|
|
(
|
|
|
|
@@ -790,6 +808,8 @@ export function useCanvasPlayer(
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
})
|
|
|
|
|
// 按需解码当前 ±1 片段
|
|
|
|
|
decodeAroundPosition(currentTime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentTime >= totalDuration) {
|
|
|
|
@@ -798,29 +818,29 @@ export function useCanvasPlayer(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rafRef.current = requestAnimationFrame(renderFrame)
|
|
|
|
|
}, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect])
|
|
|
|
|
}, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect, decodeAroundPosition])
|
|
|
|
|
|
|
|
|
|
// ── 播放控制 ──
|
|
|
|
|
const play = useCallback(async () => {
|
|
|
|
|
if (!state.hasSupport || isDestroyedRef.current) return
|
|
|
|
|
|
|
|
|
|
// 播放结束后重播 或 从起点重新播放:重置到起点,清空解码缓存
|
|
|
|
|
if (state.currentTime >= totalDuration - 0.1 || state.currentTime <= 0.1) {
|
|
|
|
|
// 重播:帧已在 init 阶段全部解码并按绝对时间戳存入队列,直接复用
|
|
|
|
|
playStartOffsetRef.current = 0
|
|
|
|
|
setState((s) => ({ ...s, currentTime: 0 }))
|
|
|
|
|
// 重播场景:currentTime 已回到起点但 decodedSegmentsRef 仍有旧标记
|
|
|
|
|
// 此时 FrameQueue 中旧帧已被淘汰,需清空标记让 decodeAroundPosition 重新解码
|
|
|
|
|
if (state.currentTime <= 0.1 && decodedSegmentsRef.current.size > 0) {
|
|
|
|
|
decodeGenerationRef.current++
|
|
|
|
|
decodedSegmentsRef.current.clear()
|
|
|
|
|
// 同步清空帧缓冲,避免旧帧残留导致 getCurrentFrame 返回 null
|
|
|
|
|
frameQueueRef.current.clear()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setState((s) => ({ ...s, isPlaying: true }))
|
|
|
|
|
playStartRef.current = performance.now()
|
|
|
|
|
if (state.currentTime < 0.1) {
|
|
|
|
|
playStartOffsetRef.current = 0
|
|
|
|
|
} else {
|
|
|
|
|
playStartOffsetRef.current = state.currentTime
|
|
|
|
|
}
|
|
|
|
|
playStartOffsetRef.current = state.currentTime
|
|
|
|
|
lastProgressUpdateRef.current = 0
|
|
|
|
|
rafRef.current = requestAnimationFrame(renderFrame)
|
|
|
|
|
}, [state.hasSupport, state.currentTime, totalDuration, renderFrame])
|
|
|
|
|
// 立即触发一次按需解码,不等渲染循环 200ms 节流
|
|
|
|
|
decodeAroundPosition(state.currentTime)
|
|
|
|
|
}, [state.hasSupport, state.currentTime, renderFrame, decodeAroundPosition])
|
|
|
|
|
|
|
|
|
|
const pause = useCallback(() => {
|
|
|
|
|
setState((s) => ({ ...s, isPlaying: false }))
|
|
|
|
@@ -833,9 +853,14 @@ export function useCanvasPlayer(
|
|
|
|
|
setState((s) => ({ ...s, currentTime: clampedTime }))
|
|
|
|
|
playStartOffsetRef.current = clampedTime
|
|
|
|
|
playStartRef.current = performance.now()
|
|
|
|
|
// init 阶段已全量解码,seek 直接定位到对应帧即可
|
|
|
|
|
// seek 时递增解码代数,作废正在进行的异步解码
|
|
|
|
|
decodeGenerationRef.current++
|
|
|
|
|
// 清空帧队列(clear 内部会 close 所有帧)+ 清空已解码标记
|
|
|
|
|
frameQueueRef.current.clear()
|
|
|
|
|
decodedSegmentsRef.current.clear()
|
|
|
|
|
await decodeAroundPosition(clampedTime)
|
|
|
|
|
},
|
|
|
|
|
[totalDuration],
|
|
|
|
|
[totalDuration, decodeAroundPosition],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const destroy = useCallback(() => {
|
|
|
|
@@ -846,13 +871,12 @@ export function useCanvasPlayer(
|
|
|
|
|
decoderRef.current.close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清空帧队列(clear 内部 close 所有帧)
|
|
|
|
|
// ✅ Strict Mode 修复:destroy 不再递增 generation,避免双执行导致 decode loop 误退出
|
|
|
|
|
// 递增代数中止进行中的异步解码,清空帧队列(clear 内部 close 所有帧)
|
|
|
|
|
decodeGenerationRef.current++
|
|
|
|
|
frameQueueRef.current.clear()
|
|
|
|
|
segmentDataRef.current.clear()
|
|
|
|
|
segmentMetaRef.current = []
|
|
|
|
|
decodedSegmentsRef.current.clear()
|
|
|
|
|
decodedFrameCountsRef.current.clear()
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// ── 预加载下一个片段的数据 ──
|
|
|
|
@@ -878,24 +902,10 @@ export function useCanvasPlayer(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
console.log("[useCanvasPlayer] Init start, segments:", segments.length)
|
|
|
|
|
|
|
|
|
|
const init = async () => {
|
|
|
|
|
// ✅ 关键修复:重置销毁标记,允许新的 init 周期正常工作
|
|
|
|
|
// destroy() 在 useEffect cleanup 中被调用,将 isDestroyedRef 设为 true
|
|
|
|
|
// 如果不重置,后续的 loadSegment / decodeSegment 会立即 return
|
|
|
|
|
isDestroyedRef.current = false
|
|
|
|
|
// ✅ Strict Mode 修复:init 不再递增 generation
|
|
|
|
|
// seek() 和 play() 仍保留 generation 递增用于中止异步解码
|
|
|
|
|
// 重置错误状态,避免上一轮的解码错误影响新的 init 周期
|
|
|
|
|
setState((s) => ({
|
|
|
|
|
...s,
|
|
|
|
|
isBuffering: true,
|
|
|
|
|
hasDecodeError: false,
|
|
|
|
|
errorMessage: "",
|
|
|
|
|
isReady: false,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
console.log("[useCanvasPlayer] Init start v2_DIAG, segments:", segments.length)
|
|
|
|
|
setState((s) => ({ ...s, isBuffering: true }))
|
|
|
|
|
|
|
|
|
|
// 1. 加载所有片段数据
|
|
|
|
|
for (const seg of segments) {
|
|
|
|
@@ -940,61 +950,29 @@ export function useCanvasPlayer(
|
|
|
|
|
// 3. 按需解码:初始只解码前 3 个片段,后续通过 decodeAroundPosition 动态加载
|
|
|
|
|
// 避免一次性全量解码导致 frameQueue 环形缓冲区旧帧被丢弃引发黑屏
|
|
|
|
|
decodedSegmentsRef.current.clear()
|
|
|
|
|
decodedFrameCountsRef.current.clear()
|
|
|
|
|
const initGen = decodeGenerationRef.current
|
|
|
|
|
const initialDecodeCount = Math.min(metas.length, 3)
|
|
|
|
|
console.log(
|
|
|
|
|
`[useCanvasPlayer] Starting decode loop: ${initialDecodeCount} segments, metas: ${metas.length}`,
|
|
|
|
|
)
|
|
|
|
|
for (let i = 0; i < initialDecodeCount; i++) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[DIAG_v2] Loop i=${i}, cancelled=${cancelled}, gen=${decodeGenerationRef.current}, initGen=${initGen}`,
|
|
|
|
|
)
|
|
|
|
|
if (cancelled) {
|
|
|
|
|
console.log("[DIAG_v2] Break: cancelled")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if (cancelled) break
|
|
|
|
|
// seek 或 destroy 已作废当前初始化
|
|
|
|
|
if (decodeGenerationRef.current !== initGen) {
|
|
|
|
|
console.log("[DIAG_v2] Break: gen mismatch")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if (decodeGenerationRef.current !== initGen) break
|
|
|
|
|
const meta = metas[i]
|
|
|
|
|
const buffer = segmentDataRef.current.get(meta.assetId)
|
|
|
|
|
if (!buffer) {
|
|
|
|
|
console.log("[DIAG_v2] Skip: no buffer for", meta.assetId)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if (!buffer) continue
|
|
|
|
|
// 先标记为解码中,防止重复解码
|
|
|
|
|
decodedSegmentsRef.current.add(i)
|
|
|
|
|
console.log(`[DIAG_v2] Calling decodeSegment(${meta.assetId})...`)
|
|
|
|
|
try {
|
|
|
|
|
await decodeSegment(buffer, meta)
|
|
|
|
|
console.log(`[DIAG_v2] decodeSegment(${meta.assetId}) returned OK`)
|
|
|
|
|
await decodeSegment(buffer, meta, MAX_INIT_FRAMES)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 解码失败则移除标记,允许后续重试
|
|
|
|
|
decodedSegmentsRef.current.delete(i)
|
|
|
|
|
console.warn(`[useCanvasPlayer] 初始化解码片段 ${i} 失败:`, e)
|
|
|
|
|
}
|
|
|
|
|
// ✅ 每次 decode 后也检查 cancelled,防止组件已卸载仍继续
|
|
|
|
|
if (cancelled) {
|
|
|
|
|
console.log("[DIAG_v2] Break after decode: cancelled")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
`[useCanvasPlayer] Decode loop finished, decodedSegments:`,
|
|
|
|
|
decodedSegmentsRef.current,
|
|
|
|
|
"cancelled:",
|
|
|
|
|
cancelled,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
console.log("[useCanvasPlayer] Init complete, isReady = true, duration:", totalDuration)
|
|
|
|
|
console.log("[useCanvasPlayer] Init complete, isReady = true")
|
|
|
|
|
setState((s) => ({ ...s, duration: totalDuration, isReady: true, isBuffering: false }))
|
|
|
|
|
} else {
|
|
|
|
|
console.warn("[useCanvasPlayer] Init was cancelled before completion")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|