feat: refactor folder structure & add new codec parser and gen & add unit tests

This commit is contained in:
2025-03-25 02:38:00 +08:00
parent 42e36e3c68
commit 39a4cf2773
67 changed files with 2211 additions and 514 deletions

View File

View File

@@ -0,0 +1,47 @@
import { SegmentSchema, SegmentType } from '@konoplayer/matroska/schema';
import { VideoCodecId } from '@konoplayer/matroska/codecs';
import {
parseAV1DecoderConfigurationRecord,
genCodecStringByAV1DecoderConfigurationRecord,
} from '@konoplayer/matroska/codecs/av1';
import { loadComponentFromRangedResource } from '../utils/data';
import { EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
import { isTagIdPos } from '@konoplayer/matroska/util';
describe('AV1 code test', () => {
it('should parse av1 meta from track entry', async () => {
const [segment] = await loadComponentFromRangedResource<SegmentType>({
resource: 'video/test-av1.mkv',
predicate: isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End),
schema: SegmentSchema,
});
const av1Track = segment.Tracks?.TrackEntry.find(
(t) => t.CodecID === VideoCodecId.AV1
)!;
expect(av1Track).toBeDefined();
expect(av1Track.CodecPrivate).toBeDefined();
const meta = parseAV1DecoderConfigurationRecord(av1Track)!;
expect(meta).toBeDefined();
const codecStr = genCodecStringByAV1DecoderConfigurationRecord(meta);
expect(meta.marker).toBe(1);
expect(meta.version).toBe(1);
expect(meta.seqProfile).toBe(0);
expect(meta.seqLevelIdx0).toBe(1);
expect(meta.seqTier0).toBe(0);
expect(meta.highBitdepth).toBe(0);
expect(meta.monochrome).toBe(0);
expect(
`${meta.chromaSubsamplingX}${meta.chromaSubsamplingY}${meta.chromaSamplePosition}`
).toBe('110');
expect(meta.initialPresentationDelayMinus1).toBeUndefined();
expect(codecStr).toBe('av01.0.01M.08.0.110');
});
});

View File

@@ -0,0 +1,40 @@
import { SegmentSchema, SegmentType } from '@konoplayer/matroska/schema';
import { VideoCodecId } from '@konoplayer/matroska/codecs';
import {
parseAVCDecoderConfigurationRecord,
genCodecStringByAVCDecoderConfigurationRecord,
} from '@konoplayer/matroska/codecs/avc';
import { loadComponentFromRangedResource } from '../utils/data';
import { EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
import { isTagIdPos } from '@konoplayer/matroska/util';
describe('AVC code test', () => {
it('should parse avc meta from track entry', async () => {
const [segment] = await loadComponentFromRangedResource<SegmentType>({
resource: 'video/test-avc.mkv',
predicate: isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End),
schema: SegmentSchema,
});
const avcTrack = segment.Tracks?.TrackEntry.find(
(t) => t.CodecID === VideoCodecId.H264
)!;
expect(avcTrack).toBeDefined();
expect(avcTrack.CodecPrivate).toBeDefined();
const meta = parseAVCDecoderConfigurationRecord(avcTrack)!;
expect(meta).toBeDefined();
const codecStr = genCodecStringByAVCDecoderConfigurationRecord(meta);
expect(meta.configurationVersion).toBe(1);
expect(meta.avcProfileIndication).toBe(100);
expect(meta.profileCompatibility).toBe(0);
expect(meta.avcLevelIndication).toBe(30);
expect(codecStr).toBe('avc1.64001e');
});
});

View File

@@ -0,0 +1,106 @@
import { SegmentSchema, SegmentType } from '@konoplayer/matroska/schema';
import { VideoCodecId } from '@konoplayer/matroska/codecs';
import {
parseHEVCDecoderConfigurationRecord,
genCodecStringByHEVCDecoderConfigurationRecord,
HEVCDecoderConfigurationRecordType,
} from '@konoplayer/matroska/codecs/hevc';
import { loadComponentFromRangedResource } from '../utils/data';
import { EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
import { isTagIdPos } from '@konoplayer/matroska/util';
import { assert } from 'vitest';
describe('HEVC codec test', () => {
it('should parse hevc meta from track entry', async () => {
const [segment] = await loadComponentFromRangedResource<SegmentType>({
resource: 'video/test-hevc.mkv',
predicate: isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End),
schema: SegmentSchema,
});
const hevcTrack = segment.Tracks?.TrackEntry.find(
(t) => t.CodecID === VideoCodecId.HEVC
)!;
expect(hevcTrack).toBeDefined();
expect(hevcTrack.CodecPrivate).toBeDefined();
const meta = parseHEVCDecoderConfigurationRecord(hevcTrack);
expect(meta).toBeDefined();
const codecStr = genCodecStringByHEVCDecoderConfigurationRecord(meta);
expect(codecStr).toBe('hev1.1.6.L63.90');
});
it('should match chrome test suite', () => {
function makeHEVCParameterSet(
generalProfileSpace: number,
generalProfileIDC: number,
generalProfileCompatibilityFlags: number,
generalTierFlag: number,
generalConstraintIndicatorFlags: [
number,
number,
number,
number,
number,
number,
],
generalLevelIDC: number
) {
return {
generalProfileSpace: generalProfileSpace,
generalProfileIdc: generalProfileIDC,
generalProfileCompatibilityFlags: generalProfileCompatibilityFlags,
generalTierFlag: generalTierFlag,
generalConstraintIndicatorFlags: Number(
new DataView(
new Uint8Array([0, 0, ...generalConstraintIndicatorFlags]).buffer
).getBigUint64(0, false)
),
generalLevelIdc: generalLevelIDC,
} as unknown as HEVCDecoderConfigurationRecordType;
}
assert(
genCodecStringByHEVCDecoderConfigurationRecord(
makeHEVCParameterSet(0, 1, 0x60000000, 0, [0, 0, 0, 0, 0, 0], 93)
),
'hev1.1.6.L93'
);
assert(
genCodecStringByHEVCDecoderConfigurationRecord(
makeHEVCParameterSet(1, 4, 0x82000000, 1, [0, 0, 0, 0, 0, 0], 120)
),
'hev1.A4.41.H120'
);
assert(
genCodecStringByHEVCDecoderConfigurationRecord(
makeHEVCParameterSet(0, 1, 0x60000000, 0, [176, 0, 0, 0, 0, 0], 93)
),
'hev1.1.6.L93.B0'
);
assert(
genCodecStringByHEVCDecoderConfigurationRecord(
makeHEVCParameterSet(1, 4, 0x82000000, 1, [176, 35, 0, 0, 0, 0], 120)
),
'hev1.A4.41.H120.B0.23'
);
assert(
genCodecStringByHEVCDecoderConfigurationRecord(
makeHEVCParameterSet(
2,
1,
0xf77db57b,
1,
[18, 52, 86, 120, 154, 188],
254
)
),
'hev1.B1.DEADBEEF.H254.12.34.56.78.9A.BC'
);
});
});

View File

@@ -0,0 +1,54 @@
import { SegmentSchema, SegmentType } from '@konoplayer/matroska/schema';
import { VideoCodecId } from '@konoplayer/matroska/codecs';
import {
genCodecStringByVP9DecoderConfigurationRecord,
parseVP9DecoderConfigurationRecord,
VP9ColorSpaceEnum,
VP9Subsampling,
} from '@konoplayer/matroska/codecs/vp9';
import { loadComponentFromRangedResource } from '../utils/data';
import { EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
import { isTagIdPos } from '@konoplayer/matroska/util';
describe('VP9 code test', () => {
it('should parse vp9 meta from track entry and keyframe', async () => {
const [segment] = await loadComponentFromRangedResource<SegmentType>({
resource: 'video/test-vp9.mkv',
predicate: isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End),
schema: SegmentSchema,
});
const vp9Track = segment.Tracks?.TrackEntry.find(
(t) => t.CodecID === VideoCodecId.VP9
)!;
expect(vp9Track).toBeDefined();
expect(vp9Track.CodecPrivate).toBeFalsy();
const keyframe = segment
.Cluster!.flatMap((c) => c.SimpleBlock || [])
.find((b) => b.keyframe && b.track === vp9Track.TrackNumber)!;
expect(keyframe).toBeDefined();
expect(keyframe.frames.length).toBe(1);
const meta = parseVP9DecoderConfigurationRecord(
vp9Track,
keyframe.frames[0]
)!;
expect(meta).toBeDefined();
expect(meta.bitDepth).toBe(8);
expect(meta.subsampling).toBe(VP9Subsampling.YUV420);
expect(meta.width).toBe(640);
expect(meta.height).toBe(360);
expect(meta.colorSpace).toBe(VP9ColorSpaceEnum.BT_601);
expect(meta.profile).toBe(0);
const codecStr = genCodecStringByVP9DecoderConfigurationRecord(meta);
expect(codecStr).toBe('vp09.00.21.08');
});
});

View File

@@ -0,0 +1,56 @@
import { Type } from 'arktype';
import { EbmlStreamDecoder, EbmlTagPosition, EbmlTagType } from 'konoebml';
import { convertEbmlTagToComponent } from '@konoplayer/matroska/util';
import fs from 'node:fs';
import { Readable } from 'node:stream';
import { TransformStream } from 'node:stream/web';
import path from 'node:path';
export interface LoadRangedResourceOptions<S extends Type<any> = any> {
resource: string;
byteStart?: number;
byteEnd?: number;
schema?: S;
predicate?: (tag: EbmlTagType) => boolean;
}
export async function loadComponentFromRangedResource<
T,
S extends Type<any> = any,
>({
resource,
byteStart,
byteEnd,
predicate = (tag) => !tag?.parent && tag.position !== EbmlTagPosition.Start,
schema,
}: LoadRangedResourceOptions<S>): Promise<T[]> {
const input = Readable.toWeb(
fs.createReadStream(
path.join(import.meta.dirname, '..', '..', '..', 'resources', resource),
{
start: byteStart,
end: byteEnd,
}
)
);
const output = input.pipeThrough(
new EbmlStreamDecoder({
streamStartOffset: byteStart,
collectChild: true,
}) as unknown as TransformStream<Uint8Array, EbmlTagType>
);
const result: T[] = [];
for await (const t of output) {
if (predicate(t)) {
let component = convertEbmlTagToComponent(t) as T;
if (schema) {
component = schema.assert(component);
}
result.push(component);
}
}
return result;
}