Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f58674679 |
@@ -44,15 +44,16 @@ function findCodecConfig(buffer: ArrayBuffer, start: number, end: number): Array
|
||||
// 子 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 + 70 // 跳过 header(8) + visual sample entry fixed fields(70)
|
||||
const result = findCodecConfig(buffer, subBoxStart, entryEnd)
|
||||
if (result) return result
|
||||
entryOffset += entrySize
|
||||
}
|
||||
} else if (type === "avcC" || type === "hvcC") {
|
||||
// 找到目标 box,返回完整 box(含 header)
|
||||
// 返回完整 box(含 size + type header),WebCodecs HEVC decoder 需要
|
||||
return buffer.slice(offset, offset + size)
|
||||
// 找到目标 box,返回其内容(不含 box header)
|
||||
const dataStart = offset + 8
|
||||
const dataSize = size - 8
|
||||
return buffer.slice(dataStart, dataStart + dataSize)
|
||||
}
|
||||
|
||||
if (size === 0) break
|
||||
@@ -136,8 +137,6 @@ interface SegmentMeta {
|
||||
videoHeight: number
|
||||
/** 解码器配置数据(HEVC hvcC / H.264 avcC),WebCodecs 必需 */
|
||||
description?: ArrayBuffer
|
||||
/** 前端提取的样本数据(已按时间范围过滤,从关键帧开始) */
|
||||
samples: Sample[]
|
||||
}
|
||||
|
||||
// ── 播放器状态 ──
|
||||
@@ -206,7 +205,6 @@ export function useCanvasPlayer(
|
||||
const videoDimRef = useRef<{ width: number; height: number }>({ width: 0, height: 0 })
|
||||
const isDestroyedRef = useRef(false)
|
||||
const lastProgressUpdateRef = useRef<number>(0)
|
||||
const descriptionCache = useRef<Map<string, ArrayBuffer>>(new Map())
|
||||
|
||||
// 计算总时长
|
||||
const totalDuration = segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0)
|
||||
@@ -257,7 +255,7 @@ export function useCanvasPlayer(
|
||||
return undefined
|
||||
}, [])
|
||||
|
||||
// ── 解封装单个片段,提取轨道元数据 + 按时间范围过滤样本 ──
|
||||
// ── 解封装单个片段,提取轨道元数据 ──
|
||||
const demuxSegment = useCallback(
|
||||
(buffer: ArrayBuffer, segIndex: number): SegmentMeta | null => {
|
||||
const segment = segments[segIndex]
|
||||
@@ -280,20 +278,7 @@ export function useCanvasPlayer(
|
||||
}
|
||||
|
||||
// 提取编解码器配置数据(HEVC 必需,H.264 也需要)
|
||||
let description = extractCodecDescription(buffer)
|
||||
|
||||
// 如果当前分片没有 description,尝试从缓存获取
|
||||
if (!description) {
|
||||
for (const cached of descriptionCache.current.values()) {
|
||||
description = cached
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存 description 供后续分片使用
|
||||
if (description) {
|
||||
descriptionCache.current.set(segment.assetId, description)
|
||||
}
|
||||
const description = extractCodecDescription(buffer)
|
||||
|
||||
meta = {
|
||||
assetId: segment.assetId,
|
||||
@@ -306,62 +291,20 @@ export function useCanvasPlayer(
|
||||
videoWidth: videoTrack.track_width || 1280,
|
||||
videoHeight: videoTrack.track_height || 720,
|
||||
description,
|
||||
samples: [],
|
||||
}
|
||||
|
||||
// 提取所有 samples
|
||||
// 提取所有 samples(start() 会同步触发 onSamples)
|
||||
mp4File.setExtractionOptions(videoTrack.id, null, {
|
||||
nbSamples: videoTrack.nb_samples,
|
||||
})
|
||||
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 >= startCts && s.cts < 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)`,
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -374,9 +317,10 @@ export function useCanvasPlayer(
|
||||
|
||||
// ── 初始化 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/分辨率)
|
||||
@@ -395,14 +339,6 @@ export function useCanvasPlayer(
|
||||
},
|
||||
})
|
||||
|
||||
console.log("[useCanvasPlayer] configure:", {
|
||||
codec: meta.codec,
|
||||
description: meta.description,
|
||||
descriptionByteLength: meta.description?.byteLength,
|
||||
videoWidth: meta.videoWidth,
|
||||
videoHeight: meta.videoHeight,
|
||||
})
|
||||
|
||||
try {
|
||||
await decoder.configure({
|
||||
codec: meta.codec,
|
||||
@@ -424,13 +360,35 @@ export function useCanvasPlayer(
|
||||
|
||||
if (!decoderReady) return
|
||||
|
||||
// 使用 demuxSegment 中已提取并过滤的 samples(前端切片)
|
||||
const samplesCollected = meta.samples
|
||||
if (samplesCollected.length === 0) {
|
||||
console.warn("[useCanvasPlayer] No samples to decode for segment", meta.assetId)
|
||||
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()
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
// 送入解码器
|
||||
for (const sample of samplesCollected) {
|
||||
if (!sample.data || isDestroyedRef.current) continue
|
||||
@@ -440,7 +398,7 @@ export function useCanvasPlayer(
|
||||
type: sample.is_sync ? "key" : "delta",
|
||||
timestamp: (sample.cts / meta.timescale) * 1_000_000,
|
||||
duration: (sample.duration / meta.timescale) * 1_000_000,
|
||||
data: sample.data,
|
||||
data: sample.data.buffer as ArrayBuffer,
|
||||
})
|
||||
|
||||
try {
|
||||
@@ -473,8 +431,7 @@ export function useCanvasPlayer(
|
||||
ctx.textAlign = "center"
|
||||
|
||||
// 按 "/" 分割为多行("/" 作为手动换行符)
|
||||
const lines = title.text.split(/[//⁄∕]/)
|
||||
console.log("[drawTitle] 原始标题:", JSON.stringify(title.text), "分割后:", lines)
|
||||
const lines = title.text.split(/[//]/)
|
||||
const lineHeight = fontSize * 1.3
|
||||
const totalHeight = lines.length * lineHeight
|
||||
|
||||
@@ -633,7 +590,6 @@ export function useCanvasPlayer(
|
||||
frameQueueRef.current.clear()
|
||||
segmentDataRef.current.clear()
|
||||
segmentMetaRef.current = []
|
||||
descriptionCache.current.clear()
|
||||
}, [])
|
||||
|
||||
// ── 预加载下一个片段的数据 ──
|
||||
|
||||
Reference in New Issue
Block a user