Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03b6e0e89a | |||
| 58a7cc20c9 | |||
| 60d95e64ab | |||
| 7775d5d118 |
@@ -36,15 +36,27 @@ function findCodecConfig(buffer: ArrayBuffer, start: number, end: number): Array
|
||||
// SampleDescriptionBox 是 fullbox: 8 header + 4 version/flags + 4 entry_count
|
||||
const entryCount = view.getUint32(offset + 12)
|
||||
let entryOffset = offset + 16
|
||||
console.log("[findCodecConfig] stsd found:", {
|
||||
entryCount,
|
||||
stsdOffset: offset,
|
||||
stsdSize: size,
|
||||
})
|
||||
for (let i = 0; i < entryCount && entryOffset < offset + size; i++) {
|
||||
const entrySize = view.getUint32(entryOffset)
|
||||
const entryType = String.fromCharCode(
|
||||
view.getUint8(entryOffset + 4),
|
||||
view.getUint8(entryOffset + 5),
|
||||
view.getUint8(entryOffset + 6),
|
||||
view.getUint8(entryOffset + 7),
|
||||
)
|
||||
console.log("[findCodecConfig] entry:", { index: i, entryOffset, entrySize, entryType })
|
||||
// 视觉样本条目: 8 header + 6 reserved + 2 data_ref_index + remaining
|
||||
// 子 box 从 entryOffset + 16 + 62 开始 (skip reserved + data_ref_index + predefined)
|
||||
// 实际结构: 8(header) + 6(reserved) + 2(data_ref_index) + 16(predefined+reserved) + 2(width) + 2(height) + ...
|
||||
// 子 box 从 entryOffset + 8 + 6 + 2 + 16 + 2 + 2 + 2 + 2 + 4 + 2 + 2 + 2 + 2 = entryOffset + 78
|
||||
// 更简单的做法:扫描 entry 内的子 box
|
||||
const entryEnd = entryOffset + entrySize
|
||||
const subBoxStart = entryOffset + 8 + 70 // VisualSampleEntry 固定字段共 70 字节
|
||||
const subBoxStart = entryOffset + 8 + 78 // VisualSampleEntry 固定字段共 78 字节(ISO 14496-12)
|
||||
const result = findCodecConfig(buffer, subBoxStart, entryEnd)
|
||||
if (result) return result
|
||||
entryOffset += entrySize
|
||||
@@ -207,6 +219,8 @@ export function useCanvasPlayer(
|
||||
const isDestroyedRef = useRef(false)
|
||||
const lastProgressUpdateRef = useRef<number>(0)
|
||||
const descriptionCache = useRef<Map<string, ArrayBuffer>>(new Map())
|
||||
const decodedSegmentsRef = useRef(new Set<number>())
|
||||
const lastDrawnFrameRef = useRef<VideoFrame | null>(null)
|
||||
|
||||
// 计算总时长
|
||||
const totalDuration = segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0)
|
||||
@@ -246,7 +260,15 @@ export function useCanvasPlayer(
|
||||
view.getUint8(offset + 7),
|
||||
)
|
||||
if (type === "moov") {
|
||||
return findCodecConfig(buffer, offset + 8, offset + size)
|
||||
const result = findCodecConfig(buffer, offset + 8, offset + size)
|
||||
console.log("[useCanvasPlayer] extractCodecDescription:", {
|
||||
moovOffset: offset,
|
||||
moovSize: size,
|
||||
searchRange: [offset + 8, offset + size],
|
||||
found: !!result,
|
||||
resultByteLength: result?.byteLength,
|
||||
})
|
||||
return result
|
||||
}
|
||||
if (size === 0) break
|
||||
offset += size
|
||||
@@ -352,7 +374,7 @@ export function useCanvasPlayer(
|
||||
|
||||
// 提取所有 samples
|
||||
mp4File.setExtractionOptions(videoTrack.id ?? 1, null, {
|
||||
nbSamples: videoTrack.nb_samples || 10000,
|
||||
nbSamples: Infinity, // 提取所有 sample
|
||||
})
|
||||
mp4File.start()
|
||||
}
|
||||
@@ -605,6 +627,43 @@ export function useCanvasPlayer(
|
||||
[],
|
||||
)
|
||||
|
||||
// ── 按需解码:根据当前播放位置解码附近片段 ──
|
||||
const decodeAroundPosition = useCallback(
|
||||
async (currentTime: number) => {
|
||||
const metas = segmentMetaRef.current
|
||||
if (metas.length === 0) return
|
||||
|
||||
// 找到当前时间对应的片段
|
||||
let currentIdx = 0
|
||||
for (let i = 0; i < metas.length; i++) {
|
||||
if (currentTime >= metas[i].globalStartTime && currentTime < metas[i].globalEndTime) {
|
||||
currentIdx = i
|
||||
break
|
||||
}
|
||||
if (currentTime >= metas[metas.length - 1].globalEndTime) {
|
||||
currentIdx = metas.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
// 解码当前片段及前后各 1 个片段
|
||||
const startIdx = Math.max(0, currentIdx - 1)
|
||||
const endIdx = Math.min(metas.length - 1, currentIdx + 1)
|
||||
|
||||
for (let i = startIdx; i <= endIdx; i++) {
|
||||
if (decodedSegmentsRef.current.has(i)) continue
|
||||
if (isDestroyedRef.current) break
|
||||
|
||||
const meta = metas[i]
|
||||
const buffer = segmentDataRef.current.get(meta.assetId)
|
||||
if (!buffer) continue
|
||||
|
||||
decodedSegmentsRef.current.add(i)
|
||||
await decodeSegment(buffer, meta)
|
||||
}
|
||||
},
|
||||
[decodeSegment],
|
||||
)
|
||||
|
||||
// ── Canvas 渲染循环 ──
|
||||
const renderFrame = useCallback(() => {
|
||||
if (isDestroyedRef.current) return
|
||||
@@ -622,6 +681,11 @@ export function useCanvasPlayer(
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
if (frame) {
|
||||
// 关闭上一帧,防止 VideoFrame 资源泄漏
|
||||
if (lastDrawnFrameRef.current) {
|
||||
lastDrawnFrameRef.current.close()
|
||||
}
|
||||
lastDrawnFrameRef.current = frame
|
||||
const rect = computeDrawRect(canvas.width, canvas.height)
|
||||
ctx.drawImage(frame, rect.dx, rect.dy, rect.dw, rect.dh)
|
||||
}
|
||||
@@ -640,6 +704,9 @@ export function useCanvasPlayer(
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
// 按需解码后续片段(每 200ms 检查一次,避免阻塞渲染)
|
||||
decodeAroundPosition(currentTime)
|
||||
}
|
||||
|
||||
if (currentTime >= totalDuration) {
|
||||
@@ -648,7 +715,7 @@ export function useCanvasPlayer(
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(renderFrame)
|
||||
}, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect])
|
||||
}, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect, decodeAroundPosition])
|
||||
|
||||
// ── 播放控制 ──
|
||||
const play = useCallback(async () => {
|
||||
@@ -667,15 +734,22 @@ export function useCanvasPlayer(
|
||||
}, [])
|
||||
|
||||
const seek = useCallback(
|
||||
(time: number) => {
|
||||
async (time: number) => {
|
||||
const clampedTime = Math.max(0, Math.min(time, totalDuration))
|
||||
setState((s) => ({ ...s, currentTime: clampedTime }))
|
||||
playStartOffsetRef.current = clampedTime
|
||||
playStartRef.current = performance.now()
|
||||
// seek 后清空帧队列,等待新帧解码
|
||||
// seek 后清空帧队列和已解码标记,触发重新解码
|
||||
frameQueueRef.current.clear()
|
||||
if (lastDrawnFrameRef.current) {
|
||||
lastDrawnFrameRef.current.close()
|
||||
lastDrawnFrameRef.current = null
|
||||
}
|
||||
decodedSegmentsRef.current.clear()
|
||||
// 立即解码 seek 位置附近的片段
|
||||
await decodeAroundPosition(clampedTime)
|
||||
},
|
||||
[totalDuration],
|
||||
[totalDuration, decodeAroundPosition],
|
||||
)
|
||||
|
||||
const destroy = useCallback(() => {
|
||||
@@ -686,10 +760,15 @@ export function useCanvasPlayer(
|
||||
decoderRef.current.close()
|
||||
}
|
||||
|
||||
if (lastDrawnFrameRef.current) {
|
||||
lastDrawnFrameRef.current.close()
|
||||
lastDrawnFrameRef.current = null
|
||||
}
|
||||
frameQueueRef.current.clear()
|
||||
segmentDataRef.current.clear()
|
||||
segmentMetaRef.current = []
|
||||
descriptionCache.current.clear()
|
||||
decodedSegmentsRef.current.clear()
|
||||
}, [])
|
||||
|
||||
// ── 预加载下一个片段的数据 ──
|
||||
@@ -739,10 +818,14 @@ export function useCanvasPlayer(
|
||||
videoDimRef.current = { width: metas[0].videoWidth, height: metas[0].videoHeight }
|
||||
}
|
||||
|
||||
// 4. 依次解码每个片段
|
||||
for (const meta of metas) {
|
||||
// 4. 按需解码:初始只解码前 3 个片段(当前 + 前后各 1),避免环形缓冲区溢出导致黑屏
|
||||
decodedSegmentsRef.current.clear()
|
||||
const initialEnd = Math.min(metas.length, 3)
|
||||
for (let i = 0; i < initialEnd; i++) {
|
||||
const meta = metas[i]
|
||||
const buffer = segmentDataRef.current.get(meta.assetId)
|
||||
if (!buffer) continue
|
||||
decodedSegmentsRef.current.add(i)
|
||||
await decodeSegment(buffer, meta)
|
||||
if (isDestroyedRef.current) break
|
||||
}
|
||||
|
||||
@@ -114,9 +114,22 @@ export function useStep6Cover({
|
||||
const anyErr = err as any
|
||||
const statusCode = anyErr?.response?.status
|
||||
|
||||
// 400 错误:后端缺少预览视频,自动创建后重试
|
||||
if (statusCode === 400) {
|
||||
console.log("[Step6] 后端返回 400,尝试自动创建预览渲染任务...")
|
||||
// 400 错误:仅当确认为"预览缺失"类错误时才执行自动修复
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const errCode = anyErr?.response?.data?.code as string | undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const errMsg = (anyErr?.response?.data?.message ||
|
||||
anyErr?.response?.data?.detail ||
|
||||
"") as string
|
||||
const isPreviewMissing =
|
||||
statusCode === 400 &&
|
||||
(errCode?.includes("PREVIEW") ||
|
||||
errCode?.includes("preview") ||
|
||||
/预览.*(不存在|缺失|未找到|not found)/i.test(errMsg) ||
|
||||
/preview.*(not found|missing|not exist)/i.test(errMsg))
|
||||
|
||||
if (isPreviewMissing) {
|
||||
console.log("[Step6] 后端返回 400(预览缺失),尝试自动创建预览渲染任务...")
|
||||
message.info("正在准备预览视频,请稍候...")
|
||||
try {
|
||||
const previewResp = await createPreview({
|
||||
@@ -124,9 +137,17 @@ export function useStep6Cover({
|
||||
asset_ids: assetIds,
|
||||
duration: duration || 30,
|
||||
})
|
||||
// 轮询等待预览渲染完成
|
||||
// 轮询等待预览渲染完成(最大 60 次 / 总超时 2 分钟)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let pollCount = 0
|
||||
const maxPolls = 60
|
||||
const poll = setInterval(async () => {
|
||||
pollCount++
|
||||
if (pollCount > maxPolls) {
|
||||
clearInterval(poll)
|
||||
reject(new Error("预览生成超时,请稍后重试"))
|
||||
return
|
||||
}
|
||||
try {
|
||||
const status = await getPreviewStatus(previewResp.task_id)
|
||||
if (status.status === "completed") {
|
||||
@@ -141,6 +162,11 @@ export function useStep6Cover({
|
||||
reject(e)
|
||||
}
|
||||
}, 3000)
|
||||
// 总超时保护:2 分钟后强制终止
|
||||
setTimeout(() => {
|
||||
clearInterval(poll)
|
||||
reject(new Error("预览生成超时,请稍后重试"))
|
||||
}, 120_000)
|
||||
})
|
||||
message.success("预览视频就绪,重新生成封面...")
|
||||
// 重试封面生成
|
||||
|
||||
Reference in New Issue
Block a user