Compare commits

...

7 Commits

Author SHA1 Message Date
张宏杰 afa1f5289b fix: HEVC description偏移修正(70→78)+调试日志
- findCodecConfig: VisualSampleEntry偏移从70改为78字节(ISO 14496-12)
- extractCodecDescription: 添加调试日志(moovOffset/moovSize/searchRange/found/resultByteLength)
- findCodecConfig: 添加stsd和entry的调试日志(entryCount/entryOffset/entrySize/entryType)
2026-08-19 20:06:43 +08:00
张宏杰 7775d5d118 fix: setExtractionOptions 改为 nbSamples: Infinity (#1449)
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m12s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m43s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m25s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 48s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m9s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m7s
CI/CD Pipeline / Unit Tests (push) Successful in 11m36s
CI/CD Pipeline / Integration Tests (push) Successful in 5m49s
CI/CD Pipeline / CI Gate (push) Failing after 667h19m42s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 667h21m8s
CI/CD Pipeline / Production Browser E2E (push) Failing after 667h21m6s
CI/CD Pipeline / Build Production Web Image (push) Failing after 667h21m8s
CI/CD Pipeline / Canary Release to Production (push) Failing after 667h21m6s
CI/CD Pipeline / PR Build Web Image (push) Failing after 667h32m41s
CI/CD Pipeline / PR Build API Image (push) Failing after 667h32m43s
CI/CD Pipeline / Build Production API Image (push) Failing after 667h21m9s
CI/CD Pipeline / Frontend Lint (push) Failing after 667h32m45s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 667h32m45s
CI/CD Pipeline / Deploy Production (push) Failing after 667h54m52s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 668h6m24s
2026-08-19 19:53:35 +08:00
张宏杰 f025514d16 fix: Canvas播放器全量修复 - nbSamples/description/调试日志 (#1448)
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m1s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m20s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m42s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m33s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 49s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 57s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m40s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Integration Tests (push) Successful in 4m15s
CI/CD Pipeline / Unit Tests (push) Successful in 11m59s
CI/CD Pipeline / Production Browser E2E (push) Failing after 667h47m8s
CI/CD Pipeline / Deploy Production (push) Failing after 667h47m9s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 667h47m9s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 667h58m5s
CI/CD Pipeline / PR Build Web Image (push) Failing after 667h58m7s
CI/CD Pipeline / CI Gate (push) Failing after 667h47m10s
CI/CD Pipeline / Frontend Lint (push) Failing after 667h59m9s
CI/CD Pipeline / Build Production Web Image (push) Failing after 667h47m10s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 667h59m12s
CI/CD Pipeline / Canary Release to Production (push) Failing after 668h20m54s
CI/CD Pipeline / Build Production API Image (push) Failing after 668h20m55s
CI/CD Pipeline / PR Build API Image (push) Failing after 668h31m54s
2026-08-19 19:27:09 +08:00
张宏杰 e043b0aa6a fix: demuxSegment添加5秒超时保护+防重复resolve (#1447)
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 53s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m24s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 51s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m37s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 45s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 51s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Failing after 668h6m39s
CI/CD Pipeline / PR Build API Image (push) Failing after 668h6m43s
CI/CD Pipeline / Frontend Lint (push) Failing after 668h6m45s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 668h6m46s
CI/CD Pipeline / PR Build Web Image (push) Failing after 668h40m26s
2026-08-19 19:19:35 +08:00
张宏杰 fdce08356b fix: demuxSegment改为异步,等待onSamples完成后返回,修复黑屏根因 (#1446)
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m56s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m32s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m15s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 42s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 49s
CI/CD Pipeline / Integration Tests (push) Successful in 2m46s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m5s
CI/CD Pipeline / Unit Tests (push) Successful in 11m27s
CI/CD Pipeline / Production Browser E2E (push) Failing after 668h11m39s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 668h11m41s
CI/CD Pipeline / Canary Release to Production (push) Failing after 668h11m40s
CI/CD Pipeline / Build Production Web Image (push) Failing after 668h11m41s
CI/CD Pipeline / Build Production API Image (push) Failing after 668h11m41s
CI/CD Pipeline / CI Gate (push) Failing after 668h11m41s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 668h22m23s
CI/CD Pipeline / PR Build API Image (push) Failing after 668h22m27s
CI/CD Pipeline / Frontend Lint (push) Failing after 668h23m6s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 668h24m55s
CI/CD Pipeline / Deploy Production (push) Failing after 668h45m25s
CI/CD Pipeline / PR Build Web Image (push) Failing after 668h56m10s
- demuxSegment 改为 async 函数,返回 Promise
- onSamples 完成后 resolve(meta),确保 samples 数据就绪
- init 中 await demuxSegment 避免空 samples 导致黑屏
2026-08-19 19:01:17 +08:00
张宏杰 89cccf294c fix: 全量可选链+空值兜底,杜绝 TypeError (#1445)
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m11s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m2s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 50s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 57s
CI/CD Pipeline / Integration Tests (push) Successful in 2m47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Unit Tests (push) Successful in 11m51s
CI/CD Pipeline / Production Browser E2E (push) Failing after 668h30m41s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 668h30m43s
CI/CD Pipeline / Canary Release to Production (push) Failing after 668h30m41s
CI/CD Pipeline / Build Production Web Image (push) Failing after 668h30m43s
CI/CD Pipeline / CI Gate (push) Failing after 668h30m44s
CI/CD Pipeline / Build Production API Image (push) Failing after 668h30m44s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 668h41m34s
CI/CD Pipeline / Frontend Lint (push) Failing after 668h42m5s
CI/CD Pipeline / PR Build Web Image (push) Failing after 668h41m36s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 668h43m59s
CI/CD Pipeline / Deploy Production (push) Failing after 669h4m27s
CI/CD Pipeline / PR Build API Image (push) Failing after 669h15m23s
2026-08-19 18:42:13 +08:00
张宏杰 5f3ff6bb8c feat: 前端自主视频切片,不再依赖后端切片 (#1444)
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m59s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m18s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m49s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m51s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m48s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 41s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 52s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 53s
CI/CD Pipeline / Integration Tests (push) Successful in 2m55s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m6s
CI/CD Pipeline / Unit Tests (push) Successful in 11m36s
CI/CD Pipeline / Production Browser E2E (push) Failing after 668h45m57s
CI/CD Pipeline / Canary Release to Production (push) Failing after 668h45m58s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 668h45m59s
CI/CD Pipeline / Build Production Web Image (push) Failing after 668h45m59s
CI/CD Pipeline / Build Production API Image (push) Failing after 668h46m0s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 668h57m11s
CI/CD Pipeline / Frontend Lint (push) Failing after 668h57m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 668h59m15s
CI/CD Pipeline / PR Build Web Image (push) Failing after 668h57m13s
CI/CD Pipeline / Deploy Production (push) Failing after 669h19m43s
CI/CD Pipeline / CI Gate (push) Failing after 669h19m45s
CI/CD Pipeline / PR Build API Image (push) Failing after 669h31m0s
2026-08-19 18:26:55 +08:00
@@ -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
@@ -136,6 +148,8 @@ interface SegmentMeta {
videoHeight: number
/** 解码器配置数据(HEVC hvcC / H.264 avcC),WebCodecs 必需 */
description?: ArrayBuffer
/** 前端提取的样本数据(已按时间范围过滤,从关键帧开始) */
samples: Sample[]
}
// ── 播放器状态 ──
@@ -244,7 +258,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
@@ -255,11 +277,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 +294,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 +499,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 +738,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)
}