Skip to content

Commit fc541bb

Browse files
committed
fix: preserve SPS/PPS NALUs before IDR frames to ensure proper video decoding
1 parent c7da36f commit fc541bb

2 files changed

Lines changed: 32 additions & 35 deletions

File tree

.changeset/angry-bobcats-fry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@webav/av-cliper': patch
3+
---
4+
5+
fix: preserve SPS/PPS NALUs before IDR frames to ensure proper video decoding

packages/av-cliper/src/clips/mp4-clip.ts

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,6 @@ async function mp4FileToSamples(otFile: OPFSToolFile, opts: MP4ClipOpts = {}) {
527527

528528
let videoDeltaTS = -1;
529529
let audioDeltaTS = -1;
530-
let findedFirstSync = false;
531530
const reader = await otFile.createReader();
532531
await quickParseMP4File(
533532
reader,
@@ -561,18 +560,8 @@ async function mp4FileToSamples(otFile: OPFSToolFile, opts: MP4ClipOpts = {}) {
561560
(_, type, samples) => {
562561
if (type === 'video') {
563562
if (videoDeltaTS === -1) videoDeltaTS = samples[0].dts;
564-
for (let i = 0; i < samples.length; i++) {
565-
const s = samples[i];
566-
if (!findedFirstSync && s.is_sync) {
567-
findedFirstSync = true;
568-
videoSamples.push(
569-
normalizeTimescale(s, videoDeltaTS, 'video', true),
570-
);
571-
} else {
572-
videoSamples.push(
573-
normalizeTimescale(s, videoDeltaTS, 'video', false),
574-
);
575-
}
563+
for (const s of samples) {
564+
videoSamples.push(normalizeTimescale(s, videoDeltaTS, 'video'));
576565
}
577566
} else if (type === 'audio' && opts.audio) {
578567
if (audioDeltaTS === -1) audioDeltaTS = samples[0].dts;
@@ -605,30 +594,25 @@ function normalizeTimescale(
605594
s: MP4Sample,
606595
delta = 0,
607596
sampleType: 'video' | 'audio',
608-
isFirstSync?: boolean,
609597
) {
610598
// todo: perf 丢弃多余字段,小尺寸对象性能更好
611599
let offset = s.offset;
612-
const isVideoSync = sampleType === 'video' && s.is_sync;
613-
const idrOffset = isVideoSync
614-
? idrNALUOffset(s.data, s.description.type, offset)
615-
: -1;
600+
// 当 IDR 帧前面包含非图像帧数据(如 SEI),可能导致解码失败
601+
const idrOffset =
602+
sampleType === 'video' && s.is_sync
603+
? idrNALUOffset(s.data, s.description.type)
604+
: -1;
616605

617-
// 默认信任第一个关键帧 是 IDR 帧,兼容某些异常标注的视频文件
618-
let is_idr = isFirstSync === true && isVideoSync;
619606
let size = s.size;
620-
if (idrOffset >= 0) {
621-
// 当 IDR 帧前面包含非图像帧数据(如 SEI),可能导致解码失败
622-
// 所以此处通过控制 offset、size 字段 跳过非图像帧数据
623-
offset = idrOffset;
624-
size -= idrOffset - offset;
625-
// 非第一个关键帧,如果根据 naluType 判定是 IDR 帧,则设置 is_idr
626-
is_idr = true;
607+
if (idrOffset > 0) {
608+
// 此处通过控制 offset、size 字段 跳过非图像帧数据
609+
offset += idrOffset;
610+
size -= idrOffset;
627611
}
628612

629613
return {
630614
...s,
631-
is_idr,
615+
is_idr: idrOffset >= 0,
632616
offset,
633617
size,
634618
cts: ((s.cts - delta) / s.timescale) * 1e6,
@@ -1352,18 +1336,26 @@ function decodeGoP(
13521336
function idrNALUOffset(
13531337
u8Arr: Uint8Array,
13541338
type: MP4Sample['description']['type'],
1355-
startOffset: number,
13561339
) {
13571340
if (type !== 'avc1' && type !== 'hvc1') return 0;
13581341

13591342
const dv = new DataView(u8Arr.buffer);
1360-
let i = startOffset;
1361-
for (; i < u8Arr.byteLength - 4; ) {
1362-
if (type === 'avc1' && (dv.getUint8(i + 4) & 0x1f) === 5) {
1363-
return i;
1343+
for (let i = 0; i < u8Arr.byteLength - 4; ) {
1344+
if (type === 'avc1') {
1345+
const nalUnitType = dv.getUint8(i + 4) & 0x1f;
1346+
// 5: IDR 帧, 7: SPS, 8: PPS
1347+
if (nalUnitType === 5 || nalUnitType === 7 || nalUnitType === 8) return i;
13641348
} else if (type === 'hvc1') {
13651349
const nalUnitType = (dv.getUint8(i + 4) >> 1) & 0x3f;
1366-
if (nalUnitType === 19 || nalUnitType === 20) return i;
1350+
// 19-20: IDR 帧, 32-34: VPS SPS PPS
1351+
if (
1352+
nalUnitType === 19 ||
1353+
nalUnitType === 20 ||
1354+
nalUnitType === 32 ||
1355+
nalUnitType === 33 ||
1356+
nalUnitType === 34
1357+
)
1358+
return i;
13671359
}
13681360
// 跳至下一个 NALU 继续检查
13691361
i += dv.getUint32(i) + 4;
@@ -1513,7 +1505,7 @@ if (import.meta.vitest) {
15131505
description: { type: 'avc1' },
15141506
is_rap: false,
15151507
};
1516-
const normalized = normalizeTimescale(s, 0, 'video', true);
1508+
const normalized = normalizeTimescale(s, 0, 'video');
15171509
expect(normalized.offset).toBe(48);
15181510
expect(normalized.size).toBe(1000);
15191511
expect(normalized.is_sync).toBe(normalized.is_idr);

0 commit comments

Comments
 (0)