Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ef5d0d75b | |||
| f025514d16 | |||
| e043b0aa6a | |||
| fdce08356b | |||
| 89cccf294c | |||
| 5f3ff6bb8c |
@@ -136,6 +136,8 @@ interface SegmentMeta {
|
||||
videoHeight: number
|
||||
/** 解码器配置数据(HEVC hvcC / H.264 avcC),WebCodecs 必需 */
|
||||
description?: ArrayBuffer
|
||||
/** 前端提取的样本数据(已按时间范围过滤,从关键帧开始) */
|
||||
samples: Sample[]
|
||||
}
|
||||
|
||||
// ── 播放器状态 ──
|
||||
@@ -255,11 +257,15 @@ export function useCanvasPlayer(
|
||||
return undefined
|
||||
}, [])
|
||||
|
||||
// ── 解封装单个片段,提取轨道元数据 ──
|
||||
// ── 解封装单个片段,提取轨道元数据 + 按时间范围过滤样本 ──
|
||||
// ✅ 关键修复:改为异步函数,等待 MP4Box.js 的 onSamples 回调完成后再返回
|
||||
const demuxSegment = useCallback(
|
||||
(buffer: ArrayBuffer, segIndex: number): SegmentMeta | null => {
|
||||
const segment = segments[segIndex]
|
||||
if (!segment) return null
|
||||
async (buffer: ArrayBuffer, segIndex: number): Promise<SegmentMeta | null> => {
|
||||
const segment = segments?.[segIndex]
|
||||
if (!segment) {
|
||||
console.warn("[useCanvasPlayer] No segment at index", segIndex)
|
||||
return null
|
||||
}
|
||||
|
||||
// 计算全局偏移
|
||||
let globalStart = 0
|
||||
@@ -268,72 +274,164 @@ export function useCanvasPlayer(
|
||||
}
|
||||
|
||||
const mp4File = createFile()
|
||||
let meta: SegmentMeta | null = null
|
||||
|
||||
mp4File.onReady = (info: Movie) => {
|
||||
const videoTrack = info.videoTracks[0]
|
||||
if (!videoTrack) {
|
||||
console.error("[useCanvasPlayer] No video track found for segment", segIndex)
|
||||
return
|
||||
}
|
||||
return new Promise<SegmentMeta | null>((resolve) => {
|
||||
let meta: SegmentMeta | null = null
|
||||
let resolved = false
|
||||
|
||||
// 提取编解码器配置数据(HEVC 必需,H.264 也需要)
|
||||
let description = extractCodecDescription(buffer)
|
||||
|
||||
// 如果当前分片没有 description,尝试从缓存获取(后续分片可能不含 moov box)
|
||||
if (!description) {
|
||||
for (const cached of descriptionCache.current.values()) {
|
||||
description = cached
|
||||
break
|
||||
// ✅ 超时保护:5秒后如果 onSamples 没有触发,返回 null
|
||||
const timeout = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
console.error(
|
||||
`[useCanvasPlayer] Timeout: onSamples not triggered for segment ${segIndex}`,
|
||||
)
|
||||
resolved = true
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
// 如果找到了 description,缓存起来供后续分片使用
|
||||
if (description) {
|
||||
mp4File.onReady = (info: Movie) => {
|
||||
const videoTrack = info?.videoTracks?.[0]
|
||||
|
||||
console.log("[useCanvasPlayer] demuxSegment:", {
|
||||
segIndex,
|
||||
startTime: segment.startTime,
|
||||
endTime: segment.endTime,
|
||||
nbSamples: videoTrack?.nb_samples,
|
||||
codec: videoTrack?.codec,
|
||||
videoWidth: videoTrack?.track_width,
|
||||
videoHeight: videoTrack?.track_height,
|
||||
})
|
||||
|
||||
if (!videoTrack) {
|
||||
console.warn("[useCanvasPlayer] No video track found for segment", segIndex)
|
||||
clearTimeout(timeout)
|
||||
resolved = true
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
|
||||
// 提取编解码器配置数据(HEVC 必需,H.264 也需要)
|
||||
let description = extractCodecDescription(buffer)
|
||||
|
||||
// 如果当前分片没有 description,尝试从缓存获取
|
||||
if (!description) {
|
||||
for (const cached of descriptionCache.current.values()) {
|
||||
description = cached
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 如果 description 缺失,无法解码 HEVC
|
||||
if (!description) {
|
||||
console.error(
|
||||
`[useCanvasPlayer] No description found for segment ${segIndex}, cannot decode HEVC`,
|
||||
)
|
||||
clearTimeout(timeout)
|
||||
resolved = true
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存 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: Infinity, // 提取所有 sample
|
||||
})
|
||||
mp4File.start()
|
||||
}
|
||||
|
||||
meta = {
|
||||
assetId: segment.assetId,
|
||||
videoUrl: segment.videoUrl,
|
||||
globalStartTime: globalStart,
|
||||
globalEndTime: globalStart + (segment.endTime - segment.startTime),
|
||||
trackId: videoTrack.id,
|
||||
timescale: videoTrack.timescale,
|
||||
codec: videoTrack.codec,
|
||||
videoWidth: videoTrack.track_width || 1280,
|
||||
videoHeight: videoTrack.track_height || 720,
|
||||
description,
|
||||
mp4File.onSamples = (_trackId: number, _user: unknown, samples: Sample[]) => {
|
||||
if (resolved) return // ✅ 防止重复 resolve
|
||||
|
||||
if (!meta) {
|
||||
clearTimeout(timeout)
|
||||
resolved = true
|
||||
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) {
|
||||
foundSync = true
|
||||
return true
|
||||
}
|
||||
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 完成后再返回
|
||||
clearTimeout(timeout)
|
||||
resolved = true
|
||||
resolve(meta)
|
||||
}
|
||||
|
||||
// 提取所有 samples(start() 会同步触发 onSamples)
|
||||
mp4File.setExtractionOptions(videoTrack.id, null, {
|
||||
nbSamples: videoTrack.nb_samples,
|
||||
})
|
||||
mp4File.start()
|
||||
}
|
||||
mp4File.onError = (_module: string, message: string) => {
|
||||
console.error(`[useCanvasPlayer] MP4Box error: ${message}`)
|
||||
clearTimeout(timeout)
|
||||
resolved = true
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
mp4File.onError = (_module: string, message: string) => {
|
||||
console.error(`[useCanvasPlayer] MP4Box error: ${message}`)
|
||||
}
|
||||
|
||||
// mp4box 需要在 buffer 上设置 fileStart 属性
|
||||
// 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],
|
||||
)
|
||||
|
||||
// ── 初始化 VideoDecoder 并解码指定片段 ──
|
||||
const decodeSegment = useCallback(
|
||||
async (buffer: ArrayBuffer, meta: SegmentMeta): Promise<void> => {
|
||||
async (_buffer: ArrayBuffer, meta: SegmentMeta): Promise<void> => {
|
||||
if (isDestroyedRef.current) return
|
||||
|
||||
const mp4File = createFile()
|
||||
let decoderReady = false
|
||||
|
||||
// 配置解码器(每个片段可能需要不同的 codec/分辨率)
|
||||
@@ -381,51 +479,23 @@ export function useCanvasPlayer(
|
||||
|
||||
if (!decoderReady) return
|
||||
|
||||
// 收集 samples
|
||||
const samplesCollected: Sample[] = []
|
||||
|
||||
mp4File.onReady = (info: Movie) => {
|
||||
const videoTrack = info.videoTracks[0]
|
||||
if (!videoTrack) return
|
||||
|
||||
mp4File.setExtractionOptions(videoTrack.id, null, {
|
||||
nbSamples: videoTrack.nb_samples,
|
||||
})
|
||||
mp4File.start()
|
||||
// 使用 demuxSegment 中已提取并过滤的 samples(前端切片)
|
||||
const samplesCollected = meta.samples
|
||||
if (samplesCollected.length === 0) {
|
||||
console.warn("[useCanvasPlayer] No samples to decode for segment", meta.assetId)
|
||||
return
|
||||
}
|
||||
|
||||
mp4File.onSamples = (_trackId: number, _user: unknown, samples: Sample[]) => {
|
||||
samplesCollected.push(...samples)
|
||||
}
|
||||
|
||||
mp4File.onError = (_module: string, message: string) => {
|
||||
console.error(`[useCanvasPlayer] MP4Box decode 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)
|
||||
|
||||
// 等待 samples 收集完成(mp4box start() 是同步的)
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
// 送入解码器(跳过第一个关键帧之前的非关键帧)
|
||||
let hasKeyFrame = false
|
||||
// 送入解码器
|
||||
for (const sample of samplesCollected) {
|
||||
if (!sample.data || isDestroyedRef.current) continue
|
||||
if (decoder.state === "closed") break
|
||||
|
||||
if (!hasKeyFrame && !sample.is_sync) {
|
||||
console.warn("[useCanvasPlayer] Skipping non-keyframe chunk before first sync")
|
||||
continue
|
||||
}
|
||||
if (sample.is_sync) hasKeyFrame = true
|
||||
|
||||
if (!sample.data) continue
|
||||
const chunk = new EncodedVideoChunk({
|
||||
type: sample.is_sync ? "key" : "delta",
|
||||
timestamp: (sample.cts / meta.timescale) * 1_000_000,
|
||||
duration: (sample.duration / meta.timescale) * 1_000_000,
|
||||
timestamp: ((sample.cts ?? 0) / (meta.timescale || 90000)) * 1_000_000,
|
||||
duration: ((sample.duration ?? 0) / (meta.timescale || 90000)) * 1_000_000,
|
||||
data: sample.data,
|
||||
})
|
||||
|
||||
@@ -648,12 +718,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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user