fix: demuxSegment改为异步,等待onSamples完成后再返回,修复黑屏根因
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 46s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m4s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m38s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 17s
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m32s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 54s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m45s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m22s
AI Code Review / AI Code Review (pull_request) Successful in 4m27s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m43s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m32s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m2s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 14m11s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped

- demuxSegment 改为 async 函数,返回 Promise<SegmentMeta | null>
- 用 new Promise 包装 MP4Box.js 回调,在 onSamples 完成后 resolve(meta)
- init 中 await demuxSegment 确保 samples 数据就绪后再进入解码
- onReady 无视频轨道、onSamples meta 为空、onError 均 resolve(null)
- 解决 meta.samples 始终为空数组导致 Canvas 黑屏的根本问题
This commit is contained in:
张宏杰
2026-08-19 19:00:57 +08:00
parent 89cccf294c
commit e20d5a0cf9
@@ -258,8 +258,9 @@ export function useCanvasPlayer(
}, [])
// ── 解封装单个片段,提取轨道元数据 + 按时间范围过滤样本 ──
// ✅ 关键修复:改为异步函数,等待 MP4Box.js 的 onSamples 回调完成后再返回
const demuxSegment = useCallback(
(buffer: ArrayBuffer, segIndex: number): SegmentMeta | null => {
async (buffer: ArrayBuffer, segIndex: number): Promise<SegmentMeta | null> => {
const segment = segments?.[segIndex]
if (!segment) {
console.warn("[useCanvasPlayer] No segment at index", segIndex)
@@ -273,104 +274,113 @@ export function useCanvasPlayer(
}
const mp4File = createFile()
let meta: SegmentMeta | null = null
mp4File.onReady = (info: Movie) => {
const videoTrack = info?.videoTracks?.[0]
if (!videoTrack) {
console.warn("[useCanvasPlayer] No video track found for segment", segIndex)
return
}
return new Promise<SegmentMeta | null>((resolve) => {
let meta: SegmentMeta | null = null
// 提取编解码器配置数据(HEVC 必需,H.264 也需要)
let description = extractCodecDescription(buffer)
// 如果当前分片没有 description,尝试从缓存获取
if (!description) {
for (const cached of descriptionCache.current.values()) {
description = cached
break
mp4File.onReady = (info: Movie) => {
const videoTrack = info?.videoTracks?.[0]
if (!videoTrack) {
console.warn("[useCanvasPlayer] No video track found for segment", segIndex)
resolve(null)
return
}
}
// 缓存 description 供后续分片使用
if (description) {
descriptionCache.current.set(segment.assetId, description)
}
// 提取编解码器配置数据(HEVC 必需,H.264 也需要)
let description = extractCodecDescription(buffer)
meta = {
assetId: segment.assetId,
videoUrl: segment.videoUrl,
globalStartTime: globalStart,
globalEndTime: globalStart + (segment.endTime - segment.startTime),
trackId: videoTrack.id ?? 1,
timescale: videoTrack.timescale ?? 90000,
codec: videoTrack.codec ?? "avc1.42E01E",
videoWidth: videoTrack.track_width || 1280,
videoHeight: videoTrack.track_height || 720,
description,
samples: [],
}
// 提取所有 samples
mp4File.setExtractionOptions(videoTrack.id ?? 1, null, {
nbSamples: videoTrack.nb_samples ?? 0,
})
mp4File.start()
}
mp4File.onSamples = (_trackId: number, _user: unknown, samples: Sample[]) => {
if (!meta) return
// 前端切片:按 [startTime, endTime] 时间范围过滤样本
const timescale = meta.timescale
const startCts = segment.startTime * timescale
const endCts = segment.endTime * timescale
// 过滤出时间范围内的样本
let filtered = samples.filter((s) => (s?.cts ?? 0) >= startCts && (s?.cts ?? 0) < endCts)
// 确保从关键帧开始(跳过第一个 sync 之前的非关键帧)
let foundSync = false
filtered = filtered.filter((s) => {
if (s.is_sync) {
foundSync = true
return true
// 如果当前分片没有 description,尝试从缓存获取
if (!description) {
for (const cached of descriptionCache.current.values()) {
description = cached
break
}
}
return foundSync
})
// Fallback:如果时间范围内没有样本,使用全部样本从第一个关键帧开始
if (filtered.length === 0) {
console.warn(
`[useCanvasPlayer] No samples in range [${segment.startTime}s, ${segment.endTime}s] for segment ${segIndex}, fallback to all from keyframe`,
)
let sync = false
filtered = samples.filter((s) => {
// 缓存 description 供后续分片使用
if (description) {
descriptionCache.current.set(segment.assetId, description)
}
meta = {
assetId: segment.assetId,
videoUrl: segment.videoUrl,
globalStartTime: globalStart,
globalEndTime: globalStart + (segment.endTime - segment.startTime),
trackId: videoTrack.id ?? 1,
timescale: videoTrack.timescale ?? 90000,
codec: videoTrack.codec ?? "avc1.42E01E",
videoWidth: videoTrack.track_width || 1280,
videoHeight: videoTrack.track_height || 720,
description,
samples: [],
}
// 提取所有 samples
mp4File.setExtractionOptions(videoTrack.id ?? 1, null, {
nbSamples: videoTrack.nb_samples ?? 0,
})
mp4File.start()
}
mp4File.onSamples = (_trackId: number, _user: unknown, samples: Sample[]) => {
if (!meta) {
resolve(null)
return
}
// 前端切片:按 [startTime, endTime] 时间范围过滤样本
const timescale = meta.timescale
const startCts = segment.startTime * timescale
const endCts = segment.endTime * timescale
// 过滤出时间范围内的样本
let filtered = samples.filter((s) => (s?.cts ?? 0) >= startCts && (s?.cts ?? 0) < endCts)
// 确保从关键帧开始(跳过第一个 sync 之前的非关键帧)
let foundSync = false
filtered = filtered.filter((s) => {
if (s.is_sync) {
sync = true
foundSync = true
return true
}
return sync
return foundSync
})
// Fallback:如果时间范围内没有样本,使用全部样本从第一个关键帧开始
if (filtered.length === 0) {
console.warn(
`[useCanvasPlayer] No samples in range [${segment.startTime}s, ${segment.endTime}s] for segment ${segIndex}, fallback to all from keyframe`,
)
let sync = false
filtered = samples.filter((s) => {
if (s.is_sync) {
sync = true
return true
}
return sync
})
}
meta.samples = filtered
console.log(
`[useCanvasPlayer] Segment ${segIndex}: ${filtered.length}/${samples.length} samples (range ${segment.startTime}s-${segment.endTime}s)`,
)
// ✅ 关键修复:等待 onSamples 完成后再返回
resolve(meta)
}
meta.samples = filtered
console.log(
`[useCanvasPlayer] Segment ${segIndex}: ${filtered.length}/${samples.length} samples (range ${segment.startTime}s-${segment.endTime}s)`,
)
}
mp4File.onError = (_module: string, message: string) => {
console.error(`[useCanvasPlayer] MP4Box error: ${message}`)
resolve(null)
}
mp4File.onError = (_module: string, message: string) => {
console.error(`[useCanvasPlayer] MP4Box error: ${message}`)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(buffer as any).fileStart = 0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mp4File.appendBuffer(buffer as any)
return meta
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(buffer as any).fileStart = 0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mp4File.appendBuffer(buffer as any)
})
},
[segments, extractCodecDescription],
)
@@ -666,12 +676,12 @@ export function useCanvasPlayer(
if (isDestroyedRef.current) return
// 2. 解析每个片段的轨道元数据
// 2. 解析每个片段的轨道元数据await 等待 onSamples 回调完成)
const metas: SegmentMeta[] = []
for (let i = 0; i < segments.length; i++) {
const buffer = segmentDataRef.current.get(segments[i].assetId)
if (!buffer) continue
const meta = demuxSegment(buffer, i)
const meta = await demuxSegment(buffer, i)
if (meta) metas.push(meta)
}