feat: refactor folder structure & add new codec parser and gen & add unit tests
This commit is contained in:
1
apps/test/.vitest/results.json
Normal file
1
apps/test/.vitest/results.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":"3.0.9","results":[[":src/matroska/codecs/av1.spec.ts",{"duration":52.71331099999952,"failed":false}]]}
|
||||
17
apps/test/package.json
Normal file
17
apps/test/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@konoplayer/test",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@konoplayer/core": "workspace:*",
|
||||
"@konoplayer/matroska": "workspace:*",
|
||||
"konoebml": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"unplugin-swc": "^1.5.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.9"
|
||||
}
|
||||
}
|
||||
2
apps/test/resources/.gitignore
vendored
Normal file
2
apps/test/resources/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
video/huge/*
|
||||
!video/huge/.gitkeep
|
||||
BIN
apps/test/resources/video/test-av1.mkv
Normal file
BIN
apps/test/resources/video/test-av1.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test-avc.mkv
Normal file
BIN
apps/test/resources/video/test-avc.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test-hevc.mkv
Normal file
BIN
apps/test/resources/video/test-hevc.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test-theora.mkv
Normal file
BIN
apps/test/resources/video/test-theora.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test-vp8.mkv
Normal file
BIN
apps/test/resources/video/test-vp8.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test-vp9.mkv
Normal file
BIN
apps/test/resources/video/test-vp9.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test.webm
Normal file
BIN
apps/test/resources/video/test.webm
Normal file
Binary file not shown.
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;
|
||||
}
|
||||
30
apps/test/tsconfig.json
Normal file
30
apps/test/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"node"
|
||||
],
|
||||
"paths": {
|
||||
"@konoplayer/core/*": [
|
||||
"../../packages/core/src/*"
|
||||
],
|
||||
"@konoplayer/matroska/*": [
|
||||
"../../packages/matroska/src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/core"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/matroska"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
apps/test/vitest.config.ts
Normal file
33
apps/test/vitest.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import swc from 'unplugin-swc';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
cacheDir: '.vitest',
|
||||
test: {
|
||||
setupFiles: ['src/init-test.ts'],
|
||||
environment: 'happy-dom',
|
||||
include: ['src/**/*.spec.ts'],
|
||||
globals: true,
|
||||
restoreMocks: true,
|
||||
coverage: {
|
||||
// you can include other reporters, but 'json-summary' is required, json is recommended
|
||||
reporter: ['text', 'json-summary', 'json'],
|
||||
// If you want a coverage reports even if your tests are failing, include the reportOnFailure option
|
||||
reportOnFailure: true,
|
||||
include: ['../../packages/core/src/**', '../../packages/matroska/src/**'],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
tsconfigPaths(),
|
||||
swc.vite({
|
||||
include: /\.[mc]?[jt]sx?$/,
|
||||
// for git+ package only
|
||||
exclude: [
|
||||
/node_modules\/(?!@konoplayer|\.pnpm)/,
|
||||
/node_modules\/\.pnpm\/(?!@konoplayer)/,
|
||||
] as any,
|
||||
tsconfigFile: './tsconfig.json',
|
||||
}),
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user