fix: demuxSegment改为异步,等待onSamples完成后返回,修复黑屏根因 #1446

Closed
xiaoxia wants to merge 1 commits from fix/demux-async into develop
@@ -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)
}