feat: refactor folder structure & add new codec parser and gen & add unit tests
This commit is contained in:
0
apps/test/src/init-test.ts
Normal file
0
apps/test/src/init-test.ts
Normal file
47
apps/test/src/matroska/codecs/av1.spec.ts
Normal file
47
apps/test/src/matroska/codecs/av1.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
40
apps/test/src/matroska/codecs/avc.spec.ts
Normal file
40
apps/test/src/matroska/codecs/avc.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
106
apps/test/src/matroska/codecs/hevc.spec.ts
Normal file
106
apps/test/src/matroska/codecs/hevc.spec.ts
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
54
apps/test/src/matroska/codecs/vp9.spec.ts
Normal file
54
apps/test/src/matroska/codecs/vp9.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
56
apps/test/src/matroska/utils/data.ts
Normal file
56
apps/test/src/matroska/utils/data.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user