feat: refactor folder structure & add new codec parser and gen & add unit tests
This commit is contained in:
parent
42e36e3c68
commit
39a4cf2773
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"runem.lit-plugin"
|
"runem.lit-plugin",
|
||||||
|
"vitest.explorer",
|
||||||
|
"biomejs.biome",
|
||||||
|
"hbenl.vscode-test-explorer",
|
||||||
|
"zerotaskx.rust-extension-pack"
|
||||||
]
|
]
|
||||||
}
|
}
|
97
Cargo.lock
generated
97
Cargo.lock
generated
@ -103,6 +103,12 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extended"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ffmpeg-sys-next"
|
name = "ffmpeg-sys-next"
|
||||||
version = "7.1.0"
|
version = "7.1.0"
|
||||||
@ -139,17 +145,17 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "konoplayer-codecs"
|
name = "konoplayer-ffmpeg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ffmpeg-sys-next",
|
"ffmpeg-sys-next",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "konoplayer-demuxing"
|
name = "konoplayer-symphonia"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"symphonia-format-mkv",
|
"symphonia",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -277,6 +283,67 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"symphonia-bundle-flac",
|
||||||
|
"symphonia-codec-adpcm",
|
||||||
|
"symphonia-codec-pcm",
|
||||||
|
"symphonia-codec-vorbis",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-format-mkv",
|
||||||
|
"symphonia-format-ogg",
|
||||||
|
"symphonia-format-riff",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-bundle-flac"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-adpcm"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-pcm"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-vorbis"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "symphonia-core"
|
name = "symphonia-core"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
@ -303,6 +370,30 @@ dependencies = [
|
|||||||
"symphonia-utils-xiph",
|
"symphonia-utils-xiph",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-ogg"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-riff"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50"
|
||||||
|
dependencies = [
|
||||||
|
"extended",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "symphonia-metadata"
|
name = "symphonia-metadata"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["packages/demuxing", "packages/codecs"]
|
members = ["packages/symphonia", "packages/ffmpeg"]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
2
apps/mock/.gitignore
vendored
2
apps/mock/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
public/video/huge/*
|
|
||||||
!public/video/huge/.gitkeep
|
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "mock",
|
"name": "@konoplayer/mock",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
2
apps/mock/public/.gitignore
vendored
Normal file
2
apps/mock/public/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
video/huge/*
|
||||||
|
!video/huge/.gitkeep
|
@ -4,10 +4,8 @@
|
|||||||
"composite": true,
|
"composite": true,
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"declaration": true,
|
|
||||||
"emitDeclarationOnly": false,
|
"emitDeclarationOnly": false,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "playground",
|
"name": "@konoplayer/playground",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -9,11 +9,11 @@
|
|||||||
"preview": "rsbuild preview"
|
"preview": "rsbuild preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"konoebml": "0.1.2",
|
"lit": "^3.2.1",
|
||||||
"lit": "^3.2.1"
|
"@konoplayer/core": "workspace:*",
|
||||||
|
"@konoplayer/matroska": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rsbuild/core": "^1.2.14",
|
"@rsbuild/core": "^1.2.14"
|
||||||
"typescript": "^5.8.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,60 +0,0 @@
|
|||||||
export interface RangedStream {
|
|
||||||
controller: AbortController;
|
|
||||||
response: Response;
|
|
||||||
body: ReadableStream<Uint8Array>;
|
|
||||||
totalSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createRangedStream(
|
|
||||||
url: string,
|
|
||||||
byteStart = 0,
|
|
||||||
byteEnd?: number
|
|
||||||
) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const signal = controller.signal;
|
|
||||||
const headers = new Headers();
|
|
||||||
headers.append(
|
|
||||||
'Range',
|
|
||||||
typeof byteEnd === 'number'
|
|
||||||
? `bytes=${byteStart}-${byteEnd}`
|
|
||||||
: `bytes=${byteStart}-`
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await fetch(url, { signal, headers });
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('fetch video stream failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const acceptRanges = response.headers.get('Accept-Ranges');
|
|
||||||
|
|
||||||
if (acceptRanges !== 'bytes') {
|
|
||||||
throw new Error('video server does not support byte ranges');
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = response.body;
|
|
||||||
|
|
||||||
if (!(body instanceof ReadableStream)) {
|
|
||||||
throw new Error('can not get readable stream from response.body');
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentRange = response.headers.get('Content-Range');
|
|
||||||
|
|
||||||
//
|
|
||||||
// Content-Range Header Syntax:
|
|
||||||
// Content-Range: <unit> <range-start>-<range-end>/<size>
|
|
||||||
// Content-Range: <unit> <range-start>-<range-end>/*
|
|
||||||
// Content-Range: <unit> */<size>
|
|
||||||
//
|
|
||||||
const totalSize = contentRange
|
|
||||||
? Number.parseInt(contentRange.split('/')[1], 10)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
controller,
|
|
||||||
response,
|
|
||||||
body,
|
|
||||||
totalSize,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
|||||||
import { ParseCodecPrivateError } from '@/media/base/errors';
|
|
||||||
import { ArkErrors, type } from 'arktype';
|
|
||||||
|
|
||||||
export const HEVC_CODEC_TYPE = 'h265(HEVC)';
|
|
||||||
|
|
||||||
export const HEVCDecoderConfigurationRecordArraySchema = type({
|
|
||||||
arrayCompleteness: type.boolean,
|
|
||||||
reserved: type.number,
|
|
||||||
NALUnitType: type.number,
|
|
||||||
numNalus: type.number,
|
|
||||||
nalUnits: type.instanceOf(Uint8Array<ArrayBufferLike>).array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type HEVCDecoderConfigurationRecordArrayType =
|
|
||||||
typeof HEVCDecoderConfigurationRecordArraySchema.infer;
|
|
||||||
|
|
||||||
// Define the schema for HEVCDecoderConfigurationRecord
|
|
||||||
export const HEVCDecoderConfigurationRecordSchema = type({
|
|
||||||
configurationVersion: type.number, // Must be 1
|
|
||||||
generalProfileSpace: type.number,
|
|
||||||
generalTierFlag: type.boolean,
|
|
||||||
generalProfileIdc: type.number,
|
|
||||||
generalProfileCompatibilityFlags: type.number,
|
|
||||||
generalConstraintIndicatorFlags: type.number.array().exactlyLength(6), // Fixed 6-byte array
|
|
||||||
generalLevelIdc: type.number,
|
|
||||||
reserved1: type.number, // 4 bits reserved, must be 1111
|
|
||||||
minSpatialSegmentationIdc: type.number,
|
|
||||||
reserved2: type.number, // 6 bits reserved, must be 111111
|
|
||||||
parallelismType: type.number,
|
|
||||||
chromaFormat: type.number,
|
|
||||||
bitDepthLumaMinus8: type.number,
|
|
||||||
bitDepthChromaMinus8: type.number,
|
|
||||||
avgFrameRate: type.number,
|
|
||||||
constantFrameRate: type.number,
|
|
||||||
numTemporalLayers: type.number,
|
|
||||||
temporalIdNested: type.boolean,
|
|
||||||
lengthSizeMinusOne: type.number,
|
|
||||||
numOfArrays: type.number,
|
|
||||||
arrays: HEVCDecoderConfigurationRecordArraySchema.array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type HEVCDecoderConfigurationRecordType =
|
|
||||||
typeof HEVCDecoderConfigurationRecordSchema.infer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse HEVCDecoderConfigurationRecord from codec_private Uint8Array
|
|
||||||
* @param codecPrivate - Uint8Array containing codec_private data
|
|
||||||
* @returns Parsed HEVCDecoderConfigurationRecord or throws an error if invalid
|
|
||||||
*/
|
|
||||||
export function parseHEVCDecoderConfigurationRecord(
|
|
||||||
codecPrivate: Uint8Array
|
|
||||||
): HEVCDecoderConfigurationRecordType {
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
// Read and validate basic fields
|
|
||||||
const config: HEVCDecoderConfigurationRecordType = {
|
|
||||||
configurationVersion: codecPrivate[offset++],
|
|
||||||
generalProfileSpace: codecPrivate[offset] >> 6,
|
|
||||||
generalTierFlag: Boolean(codecPrivate[offset] & 0x20),
|
|
||||||
generalProfileIdc: codecPrivate[offset++] & 0x1f,
|
|
||||||
generalProfileCompatibilityFlags:
|
|
||||||
(codecPrivate[offset] << 24) |
|
|
||||||
(codecPrivate[offset + 1] << 16) |
|
|
||||||
(codecPrivate[offset + 2] << 8) |
|
|
||||||
codecPrivate[offset + 3],
|
|
||||||
generalConstraintIndicatorFlags: Array.from(
|
|
||||||
codecPrivate.subarray(offset + 4, offset + 10)
|
|
||||||
),
|
|
||||||
generalLevelIdc: codecPrivate[offset + 10],
|
|
||||||
reserved1: (codecPrivate[offset + 11] & 0xf0) >> 4, // 4 bits
|
|
||||||
minSpatialSegmentationIdc:
|
|
||||||
((codecPrivate[offset + 11] & 0x0f) << 8) | codecPrivate[offset + 12],
|
|
||||||
reserved2: (codecPrivate[offset + 13] & 0xfc) >> 2, // 6 bits
|
|
||||||
parallelismType: codecPrivate[offset + 13] & 0x03,
|
|
||||||
chromaFormat: (codecPrivate[offset + 14] & 0xe0) >> 5,
|
|
||||||
bitDepthLumaMinus8: (codecPrivate[offset + 14] & 0x1c) >> 2,
|
|
||||||
bitDepthChromaMinus8: codecPrivate[offset + 14] & 0x03,
|
|
||||||
avgFrameRate: (codecPrivate[offset + 15] << 8) | codecPrivate[offset + 16],
|
|
||||||
constantFrameRate: (codecPrivate[offset + 17] & 0xc0) >> 6,
|
|
||||||
numTemporalLayers: (codecPrivate[offset + 17] & 0x38) >> 3,
|
|
||||||
temporalIdNested: Boolean(codecPrivate[offset + 17] & 0x04),
|
|
||||||
lengthSizeMinusOne: codecPrivate[offset + 17] & 0x03,
|
|
||||||
numOfArrays: codecPrivate[offset + 18],
|
|
||||||
arrays: [],
|
|
||||||
};
|
|
||||||
offset += 19;
|
|
||||||
|
|
||||||
// Parse NAL unit arrays
|
|
||||||
const arrays = config.arrays;
|
|
||||||
for (let i = 0; i < config.numOfArrays; i++) {
|
|
||||||
const array: HEVCDecoderConfigurationRecordArrayType = {
|
|
||||||
arrayCompleteness: Boolean(codecPrivate[offset] & 0x80),
|
|
||||||
reserved: (codecPrivate[offset] & 0x40) >> 6,
|
|
||||||
NALUnitType: codecPrivate[offset] & 0x3f,
|
|
||||||
numNalus: (codecPrivate[offset + 1] << 8) | codecPrivate[offset + 2],
|
|
||||||
nalUnits: [] as Uint8Array<ArrayBufferLike>[],
|
|
||||||
};
|
|
||||||
offset += 3;
|
|
||||||
|
|
||||||
for (let j = 0; j < array.numNalus; j++) {
|
|
||||||
const nalUnitLength =
|
|
||||||
(codecPrivate[offset] << 8) | codecPrivate[offset + 1];
|
|
||||||
offset += 2;
|
|
||||||
array.nalUnits.push(
|
|
||||||
codecPrivate.subarray(offset, offset + nalUnitLength)
|
|
||||||
);
|
|
||||||
offset += nalUnitLength;
|
|
||||||
}
|
|
||||||
arrays.push(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = { ...config, arrays };
|
|
||||||
|
|
||||||
// Validate using arktype
|
|
||||||
const validation = HEVCDecoderConfigurationRecordSchema(result);
|
|
||||||
if (validation instanceof ArkErrors) {
|
|
||||||
const error = new ParseCodecPrivateError(
|
|
||||||
HEVC_CODEC_TYPE,
|
|
||||||
'Invalid HEVC configuration record'
|
|
||||||
);
|
|
||||||
error.cause = validation;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function genCodecStringByHEVCDecoderConfigurationRecord(
|
|
||||||
config: HEVCDecoderConfigurationRecordType
|
|
||||||
) {
|
|
||||||
const profileSpace =
|
|
||||||
config.generalProfileSpace === 0
|
|
||||||
? ''
|
|
||||||
: String.fromCharCode(65 + config.generalProfileSpace - 1);
|
|
||||||
const profileIdcHex = config.generalProfileIdc.toString(16);
|
|
||||||
const tier = config.generalTierFlag ? '7' : '6';
|
|
||||||
const levelMajor = Math.floor(config.generalLevelIdc / 30);
|
|
||||||
const levelMinor =
|
|
||||||
config.generalLevelIdc % 30 === 0 ? '0' : (config.generalLevelIdc % 30) / 3;
|
|
||||||
const levelStr = `L${config.generalLevelIdc.toString().padStart(3, '0')}`;
|
|
||||||
|
|
||||||
const constraint = '00';
|
|
||||||
return `hev1.${profileSpace}${profileIdcHex}.${tier}.${levelStr}.${constraint}`;
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import { type } from 'arktype';
|
|
||||||
import type {TrackEntryType} from "@/media/mkv/schema.ts";
|
|
||||||
|
|
||||||
export const VP9DecoderProfileSchema = type('0 | 1 | 2 | 3');
|
|
||||||
|
|
||||||
export const VP9DecoderConfigurationRecordSchema = type({
|
|
||||||
profile: VP9DecoderProfileSchema,
|
|
||||||
level: type.number,
|
|
||||||
bitDepth: type.number,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type VP9DecoderConfigurationRecordType =
|
|
||||||
typeof VP9DecoderConfigurationRecordSchema.infer;
|
|
||||||
|
|
||||||
export function parseVP9DecoderConfigurationRecord(track: TrackEntryType) {
|
|
||||||
const pixelWidth = Number(track.Video?.PixelWidth);
|
|
||||||
const pixelHeight = Number(track.Video?.PixelHeight);
|
|
||||||
const pixels = pixelWidth * pixelHeight;
|
|
||||||
const bitDepth = Number(track.Video?.Colour?.BitsPerChannel) || 10;
|
|
||||||
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
export interface ProbeInfo {
|
|
||||||
|
|
||||||
}
|
|
@ -18,9 +18,12 @@ import {
|
|||||||
fromEvent,
|
fromEvent,
|
||||||
filter,
|
filter,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { createEbmlController } from './media/mkv/reactive';
|
import { createEbmlController } from '@konoplayer/matroska/reactive';
|
||||||
import { TrackTypeRestrictionEnum, type ClusterType } from './media/mkv/schema';
|
import {
|
||||||
import type { SegmentComponent } from './media/mkv/model';
|
TrackTypeRestrictionEnum,
|
||||||
|
type ClusterType,
|
||||||
|
} from '@konoplayer/matroska/schema';
|
||||||
|
import type { SegmentComponent } from '@konoplayer/matroska/model';
|
||||||
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
||||||
import { Queue } from 'mnemonist';
|
import { Queue } from 'mnemonist';
|
||||||
|
|
||||||
@ -113,7 +116,6 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
description: videoTrack.CodecPrivate, // Uint8Array,包含 VPS/SPS/PPS
|
description: videoTrack.CodecPrivate, // Uint8Array,包含 VPS/SPS/PPS
|
||||||
});
|
});
|
||||||
|
|
||||||
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
|
|
||||||
const sub = this.cluster$.subscribe((c) => {
|
const sub = this.cluster$.subscribe((c) => {
|
||||||
if (!isFinalized) {
|
if (!isFinalized) {
|
||||||
for (const b of (c.SimpleBlock || []).filter(
|
for (const b of (c.SimpleBlock || []).filter(
|
||||||
@ -163,7 +165,7 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
const numberOfChannels =
|
const numberOfChannels =
|
||||||
(audioTrack.Audio?.Channels as number) || 2;
|
(audioTrack.Audio?.Channels as number) || 2;
|
||||||
const duration =
|
const duration =
|
||||||
Math.round(Number(audioTrack.DefaultDuration / 1000)) ||
|
Math.round(Number(audioTrack.DefaultDuration) / 1000) ||
|
||||||
Math.round((1024 / sampleRate) * 1000000);
|
Math.round((1024 / sampleRate) * 1000000);
|
||||||
|
|
||||||
decoder.configure({
|
decoder.configure({
|
||||||
|
@ -2,19 +2,25 @@
|
|||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"target": "ES2020",
|
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"experimentalDecorators": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@konoplayer/core/*": [
|
||||||
"./src/*"
|
"../../packages/core/src/*"
|
||||||
|
],
|
||||||
|
"@konoplayer/matroska/*": [
|
||||||
|
"../../packages/matroska/src/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../packages/core"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../packages/matroska"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "proxy",
|
"name": "@konoplayer/proxy",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
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.
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',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
35
biome.jsonc
35
biome.jsonc
@ -17,7 +17,13 @@
|
|||||||
"noSvgWithoutTitle": "off"
|
"noSvgWithoutTitle": "off"
|
||||||
},
|
},
|
||||||
"complexity": {
|
"complexity": {
|
||||||
"noBannedTypes": "off"
|
"noBannedTypes": "off",
|
||||||
|
"noExcessiveCognitiveComplexity": {
|
||||||
|
"level": "warn",
|
||||||
|
"options": {
|
||||||
|
"maxAllowedComplexity": 40
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"nursery": {
|
"nursery": {
|
||||||
"noEnum": "off",
|
"noEnum": "off",
|
||||||
@ -61,6 +67,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"apps/test/**"
|
||||||
|
],
|
||||||
|
"javascript": {
|
||||||
|
"globals": [
|
||||||
|
"describe",
|
||||||
|
"beforeEach",
|
||||||
|
"it",
|
||||||
|
"expect",
|
||||||
|
"afterEach"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"style": {
|
||||||
|
"useImportType": "off"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noConsole": "off"
|
||||||
|
},
|
||||||
|
"performance": {
|
||||||
|
"useTopLevelRegex": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
4
justfile
4
justfile
@ -2,10 +2,10 @@ set windows-shell := ["pwsh.exe", "-c"]
|
|||||||
set dotenv-load := true
|
set dotenv-load := true
|
||||||
|
|
||||||
dev-playground:
|
dev-playground:
|
||||||
pnpm run --filter=playground dev
|
pnpm run --filter=@konoplayer/playground dev
|
||||||
|
|
||||||
dev-proxy:
|
dev-proxy:
|
||||||
pnpm run --filter proxy --filter mock dev
|
pnpm run --filter=@konoplayer/proxy --filter=@konoplayer/mock dev
|
||||||
|
|
||||||
download-samples:
|
download-samples:
|
||||||
pnpm run download-samples
|
pnpm run download-samples
|
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "konoplayer-codecs",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "rsbuild build",
|
|
||||||
"dev": "rsbuild dev",
|
|
||||||
"preview": "rsbuild preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"konoebml": "0.1.2-rc.5",
|
|
||||||
"lit": "^3.2.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@rsbuild/core": "^1.2.14",
|
|
||||||
"typescript": "^5.8.2"
|
|
||||||
}
|
|
||||||
}
|
|
8
packages/core/package.json
Normal file
8
packages/core/package.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "@konoplayer/core",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {},
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
2
packages/core/src/codecs/index.ts
Normal file
2
packages/core/src/codecs/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { AudioCodec } from './audio-codecs';
|
||||||
|
export { VideoCodec } from './video-codecs';
|
39
packages/core/src/data/bit.ts
Normal file
39
packages/core/src/data/bit.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
export class BitReader {
|
||||||
|
private data: Uint8Array;
|
||||||
|
private byteOffset = 0;
|
||||||
|
private bitOffset = 0;
|
||||||
|
|
||||||
|
constructor(data: Uint8Array) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
readBits(numBits: number): number {
|
||||||
|
let value = 0;
|
||||||
|
for (let i = 0; i < numBits; i++) {
|
||||||
|
const bit = (this.data[this.byteOffset] >> (7 - this.bitOffset)) & 1;
|
||||||
|
value = (value << 1) | bit;
|
||||||
|
this.bitOffset++;
|
||||||
|
if (this.bitOffset === 8) {
|
||||||
|
this.bitOffset = 0;
|
||||||
|
this.byteOffset++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
skipBits(numBits: number): void {
|
||||||
|
this.bitOffset += numBits;
|
||||||
|
while (this.bitOffset >= 8) {
|
||||||
|
this.bitOffset -= 8;
|
||||||
|
this.byteOffset++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasData(): boolean {
|
||||||
|
return this.byteOffset < this.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRemainingBytes(): Uint8Array {
|
||||||
|
return this.data.slice(this.byteOffset);
|
||||||
|
}
|
||||||
|
}
|
6
packages/core/src/data/index.ts
Normal file
6
packages/core/src/data/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export {
|
||||||
|
type RangedStream,
|
||||||
|
type CreateRangedStreamOptions,
|
||||||
|
createRangedStream,
|
||||||
|
} from './fetch';
|
||||||
|
export { BitReader } from './bit';
|
@ -4,7 +4,7 @@ export class UnsupportedCodecError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ParseCodecPrivateError extends Error {
|
export class ParseCodecError extends Error {
|
||||||
constructor(codec: string, detail: string) {
|
constructor(codec: string, detail: string) {
|
||||||
super(`code ${codec} private parse failed: ${detail}`);
|
super(`code ${codec} private parse failed: ${detail}`);
|
||||||
}
|
}
|
||||||
@ -12,7 +12,7 @@ export class ParseCodecPrivateError extends Error {
|
|||||||
|
|
||||||
export class UnreachableOrLogicError extends Error {
|
export class UnreachableOrLogicError extends Error {
|
||||||
constructor(detail: string) {
|
constructor(detail: string) {
|
||||||
super(`unreachable or logic error: ${detail}`)
|
super(`unreachable or logic error: ${detail}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,4 +22,4 @@ export class ParseCodecErrors extends Error {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super('failed to parse codecs');
|
super('failed to parse codecs');
|
||||||
}
|
}
|
||||||
}
|
}
|
10
packages/core/tsconfig.json
Normal file
10
packages/core/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "konoplayer-codecs"
|
name = "konoplayer-ffmpeg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
11
packages/matroska/package.json
Normal file
11
packages/matroska/package.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@konoplayer/matroska",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {},
|
||||||
|
"dependencies": {
|
||||||
|
"@konoplayer/core": "workspace:*",
|
||||||
|
"konoebml": "^0.1.2"
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { ParseCodecPrivateError } from '@/media/base/errors';
|
import { ParseCodecError } from '@konoplayer/core/errors';
|
||||||
import { ArkErrors, type } from 'arktype';
|
import { ArkErrors, type } from 'arktype';
|
||||||
|
|
||||||
export const AAC_CODEC_TYPE = 'AAC';
|
export const AAC_CODEC_TYPE = 'AAC';
|
||||||
@ -28,10 +28,7 @@ export function parseAudioSpecificConfig(
|
|||||||
codecPrivate: Uint8Array
|
codecPrivate: Uint8Array
|
||||||
): AudioSpecificConfigType {
|
): AudioSpecificConfigType {
|
||||||
if (codecPrivate.length < 2) {
|
if (codecPrivate.length < 2) {
|
||||||
throw new ParseCodecPrivateError(
|
throw new ParseCodecError(AAC_CODEC_TYPE, 'codec_private data too short');
|
||||||
AAC_CODEC_TYPE,
|
|
||||||
'codec_private data too short'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a DataView for bit-level manipulation
|
// Create a DataView for bit-level manipulation
|
||||||
@ -95,7 +92,7 @@ export function parseAudioSpecificConfig(
|
|||||||
// Validate with arktype
|
// Validate with arktype
|
||||||
const validation = AudioSpecificConfigSchema(config);
|
const validation = AudioSpecificConfigSchema(config);
|
||||||
if (validation instanceof ArkErrors) {
|
if (validation instanceof ArkErrors) {
|
||||||
const error = new ParseCodecPrivateError(
|
const error = new ParseCodecError(
|
||||||
AAC_CODEC_TYPE,
|
AAC_CODEC_TYPE,
|
||||||
'Invalid AudioSpecificConfig'
|
'Invalid AudioSpecificConfig'
|
||||||
);
|
);
|
167
packages/matroska/src/codecs/av1.ts
Normal file
167
packages/matroska/src/codecs/av1.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { BitReader } from '@konoplayer/core/data';
|
||||||
|
import { type } from 'arktype';
|
||||||
|
import type { TrackEntryType } from '../schema';
|
||||||
|
import { ParseCodecError } from '@konoplayer/core/errors';
|
||||||
|
|
||||||
|
export const AV1_CODEC_TYPE = 'AV1';
|
||||||
|
|
||||||
|
export const AV1DecoderConfigurationRecordSchema = type({
|
||||||
|
marker: type.number, // 1 bit, must be 1
|
||||||
|
version: type.number, // 7 bits, must be 1
|
||||||
|
seqProfile: type.number, // 3 bits, seq profile (0-7)
|
||||||
|
seqLevelIdx0: type.number, // 5 bits, seq level (0-31)
|
||||||
|
seqTier0: type.number, // 1 bit, tier (0 or 1)
|
||||||
|
highBitdepth: type.number, // 1 bit, high or low
|
||||||
|
twelveBit: type.number, // 1 bit, if 12-bit
|
||||||
|
monochrome: type.number, // 1 bit, if mono chrome
|
||||||
|
chromaSubsamplingX: type.number, // 1 bit, sub sampling X
|
||||||
|
chromaSubsamplingY: type.number, // 1 bit, sub sampling Y
|
||||||
|
chromaSamplePosition: type.number, // 2 bits
|
||||||
|
initialPresentationDelayPresent: type.number, // 1 bit
|
||||||
|
initialPresentationDelayMinus1: type.number.optional(), // 4 bits, optoinal
|
||||||
|
configOBUs: type.instanceOf(Uint8Array<ArrayBufferLike>), // remain OBU data
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AV1DecoderConfigurationRecordType =
|
||||||
|
typeof AV1DecoderConfigurationRecordSchema.infer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [webkit impl](https://github.com/movableink/webkit/blob/7e43fe7000b319ce68334c09eed1031642099726/Source/WebCore/platform/graphics/AV1Utilities.cpp#L48)
|
||||||
|
*/
|
||||||
|
export function parseAV1DecoderConfigurationRecord(
|
||||||
|
track: TrackEntryType
|
||||||
|
): AV1DecoderConfigurationRecordType {
|
||||||
|
const codecPrivate = track.CodecPrivate;
|
||||||
|
|
||||||
|
if (!codecPrivate) {
|
||||||
|
throw new ParseCodecError(
|
||||||
|
AV1_CODEC_TYPE,
|
||||||
|
'CodecPrivate of AVC Track is missing'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codecPrivate.length < 4) {
|
||||||
|
throw new ParseCodecError(
|
||||||
|
AV1_CODEC_TYPE,
|
||||||
|
'Input data too short for AV1DecoderConfigurationRecord'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new BitReader(codecPrivate);
|
||||||
|
|
||||||
|
// Byte 0
|
||||||
|
const marker = reader.readBits(1);
|
||||||
|
const version = reader.readBits(7);
|
||||||
|
if (marker !== 1 || version !== 1) {
|
||||||
|
throw new ParseCodecError(
|
||||||
|
AV1_CODEC_TYPE,
|
||||||
|
`Invalid marker (${marker}) or version (${version})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seqProfile = reader.readBits(3);
|
||||||
|
const seqLevelIdx0 = reader.readBits(5);
|
||||||
|
|
||||||
|
// Byte 1
|
||||||
|
const seqTier0 = reader.readBits(1);
|
||||||
|
const highBitdepth = reader.readBits(1);
|
||||||
|
const twelveBit = reader.readBits(1);
|
||||||
|
const monochrome = reader.readBits(1);
|
||||||
|
const chromaSubsamplingX = reader.readBits(1);
|
||||||
|
const chromaSubsamplingY = reader.readBits(1);
|
||||||
|
const chromaSamplePosition = reader.readBits(2);
|
||||||
|
|
||||||
|
// Byte 2
|
||||||
|
const reserved1 = reader.readBits(3);
|
||||||
|
if (reserved1 !== 0) {
|
||||||
|
throw new ParseCodecError(
|
||||||
|
AV1_CODEC_TYPE,
|
||||||
|
`Reserved bits must be 0, got ${reserved1}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const initialPresentationDelayPresent = reader.readBits(1);
|
||||||
|
let initialPresentationDelayMinus1: number | undefined;
|
||||||
|
if (initialPresentationDelayPresent) {
|
||||||
|
initialPresentationDelayMinus1 = reader.readBits(4);
|
||||||
|
} else {
|
||||||
|
const reserved2 = reader.readBits(4);
|
||||||
|
if (reserved2 !== 0) {
|
||||||
|
throw new ParseCodecError(
|
||||||
|
AV1_CODEC_TYPE,
|
||||||
|
`Reserved bits must be 0, got ${reserved2}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remain bytes as configOBUs
|
||||||
|
const configOBUs = reader.getRemainingBytes();
|
||||||
|
|
||||||
|
return {
|
||||||
|
marker,
|
||||||
|
version,
|
||||||
|
seqProfile,
|
||||||
|
seqLevelIdx0,
|
||||||
|
seqTier0,
|
||||||
|
highBitdepth,
|
||||||
|
twelveBit,
|
||||||
|
monochrome,
|
||||||
|
chromaSubsamplingX,
|
||||||
|
chromaSubsamplingY,
|
||||||
|
chromaSamplePosition,
|
||||||
|
initialPresentationDelayPresent,
|
||||||
|
initialPresentationDelayMinus1,
|
||||||
|
configOBUs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [webkit impl](https://github.com/movableink/webkit/blob/7e43fe7000b319ce68334c09eed1031642099726/Source/WebCore/platform/graphics/AV1Utilities.cpp#L197)
|
||||||
|
*/
|
||||||
|
export function genCodecStringByAV1DecoderConfigurationRecord(
|
||||||
|
config: AV1DecoderConfigurationRecordType
|
||||||
|
): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
// Prefix
|
||||||
|
parts.push('av01');
|
||||||
|
|
||||||
|
// Profile
|
||||||
|
parts.push(config.seqProfile.toString());
|
||||||
|
|
||||||
|
// Level and Tier
|
||||||
|
const levelStr = config.seqLevelIdx0.toString().padStart(2, '0');
|
||||||
|
const tierStr = config.seqTier0 === 0 ? 'M' : 'H';
|
||||||
|
parts.push(`${levelStr}${tierStr}`);
|
||||||
|
|
||||||
|
// Bit Depth
|
||||||
|
let bitDepthStr: string;
|
||||||
|
if (config.highBitdepth === 0) {
|
||||||
|
bitDepthStr = '08'; // 8-bit
|
||||||
|
} else if (config.twelveBit === 0) {
|
||||||
|
bitDepthStr = '10'; // 10-bit
|
||||||
|
} else {
|
||||||
|
bitDepthStr = '12'; // 12-bit
|
||||||
|
}
|
||||||
|
parts.push(bitDepthStr);
|
||||||
|
|
||||||
|
// Monochrome
|
||||||
|
parts.push(config.monochrome.toString());
|
||||||
|
|
||||||
|
// Chroma Subsampling
|
||||||
|
const chromaSubsampling = `${config.chromaSubsamplingX}${config.chromaSubsamplingY}${config.chromaSamplePosition}`;
|
||||||
|
parts.push(chromaSubsampling);
|
||||||
|
|
||||||
|
// Initial Presentation Delay(optional)
|
||||||
|
if (
|
||||||
|
config.initialPresentationDelayPresent === 1 &&
|
||||||
|
config.initialPresentationDelayMinus1 !== undefined
|
||||||
|
) {
|
||||||
|
const delay = (config.initialPresentationDelayMinus1 + 1)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0');
|
||||||
|
parts.push(delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// joined
|
||||||
|
return parts.join('.');
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { ParseCodecPrivateError } from '@/media/base/errors';
|
import { ParseCodecError } from '@konoplayer/core/errors';
|
||||||
import { type } from 'arktype';
|
import { type } from 'arktype';
|
||||||
|
import type { TrackEntryType } from '../schema';
|
||||||
|
|
||||||
export const AVC_CODEC_TYPE = 'h264(AVC)';
|
export const AVC_CODEC_TYPE = 'h264(AVC)';
|
||||||
|
|
||||||
@ -23,46 +24,72 @@ export type AVCDecoderConfigurationRecordType =
|
|||||||
typeof AVCDecoderConfigurationRecordSchema.infer;
|
typeof AVCDecoderConfigurationRecordSchema.infer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse AVCDecoderConfigurationRecord from codec_private Uint8Array
|
*
|
||||||
* @param codecPrivate - Uint8Array containing codec_private data
|
* @see [webkit](https://github.com/movableink/webkit/blob/7e43fe7000b319ce68334c09eed1031642099726/Source/WebCore/platform/graphics/HEVCUtilities.cpp#L84)
|
||||||
* @returns Parsed AVCDecoderConfigurationRecord or throws an error if invalid
|
|
||||||
*/
|
*/
|
||||||
export function parseAVCDecoderConfigurationRecord(
|
export function parseAVCDecoderConfigurationRecord(
|
||||||
codecPrivate: Uint8Array
|
track: TrackEntryType
|
||||||
): AVCDecoderConfigurationRecordType {
|
): AVCDecoderConfigurationRecordType {
|
||||||
let offset = 0;
|
// ISO/IEC 14496-10:2014
|
||||||
|
// 7.3.2.1.1 Sequence parameter set data syntax
|
||||||
|
const codecPrivate = track.CodecPrivate;
|
||||||
|
|
||||||
// Check if data length is sufficient
|
if (!codecPrivate) {
|
||||||
if (codecPrivate.length < 5) {
|
throw new ParseCodecError(
|
||||||
throw new ParseCodecPrivateError(
|
AVC_CODEC_TYPE,
|
||||||
|
'CodecPrivate of AVC Track is missing'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AVCDecoderConfigurationRecord is at a minimum 24 bytes long
|
||||||
|
if (codecPrivate.length < 24) {
|
||||||
|
throw new ParseCodecError(
|
||||||
AVC_CODEC_TYPE,
|
AVC_CODEC_TYPE,
|
||||||
'Input data too short for AVCDecoderConfigurationRecord'
|
'Input data too short for AVCDecoderConfigurationRecord'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const configurationVersion = codecPrivate[offset++];
|
const view = new DataView(codecPrivate.buffer);
|
||||||
const avcProfileIndication = codecPrivate[offset++];
|
let offset = 0;
|
||||||
const profileCompatibility = codecPrivate[offset++];
|
|
||||||
const avcLevelIndication = codecPrivate[offset++];
|
const readUint8 = (move: boolean) => {
|
||||||
|
const result = view.getUint8(offset);
|
||||||
|
if (move) {
|
||||||
|
offset += 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readUint16 = (move: boolean) => {
|
||||||
|
const result = view.getUint16(offset, false);
|
||||||
|
if (move) {
|
||||||
|
offset += 2;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const configurationVersion = readUint8(true);
|
||||||
|
const avcProfileIndication = readUint8(true);
|
||||||
|
const profileCompatibility = readUint8(true);
|
||||||
|
const avcLevelIndication = readUint8(true);
|
||||||
|
|
||||||
// Read lengthSizeMinusOne (first 6 bits are reserved, typically 0xFF, last 2 bits are the value)
|
// Read lengthSizeMinusOne (first 6 bits are reserved, typically 0xFF, last 2 bits are the value)
|
||||||
const lengthSizeMinusOne = codecPrivate[offset++] & 0x03;
|
const lengthSizeMinusOne = readUint8(true) & 0x03;
|
||||||
|
|
||||||
// Read number of SPS (first 3 bits are reserved, typically 0xE0, last 5 bits are SPS count)
|
// Read number of SPS (first 3 bits are reserved, typically 0xE0, last 5 bits are SPS count)
|
||||||
const numOfSPS = codecPrivate[offset++] & 0x1f;
|
const numOfSPS = readUint8(true) & 0x1f;
|
||||||
const sps: Uint8Array[] = [];
|
const sps: Uint8Array[] = [];
|
||||||
|
|
||||||
// Parse SPS
|
// Parse SPS
|
||||||
for (let i = 0; i < numOfSPS; i++) {
|
for (let i = 0; i < numOfSPS; i++) {
|
||||||
if (offset + 2 > codecPrivate.length) {
|
if (offset + 2 > codecPrivate.length) {
|
||||||
throw new ParseCodecPrivateError(AVC_CODEC_TYPE, 'Invalid SPS length');
|
throw new ParseCodecError(AVC_CODEC_TYPE, 'Invalid SPS length');
|
||||||
}
|
}
|
||||||
|
|
||||||
const spsLength = (codecPrivate[offset] << 8) | codecPrivate[offset + 1];
|
const spsLength = readUint16(true);
|
||||||
offset += 2;
|
|
||||||
|
|
||||||
if (offset + spsLength > codecPrivate.length) {
|
if (offset + spsLength > codecPrivate.length) {
|
||||||
throw new ParseCodecPrivateError(
|
throw new ParseCodecError(
|
||||||
AVC_CODEC_TYPE,
|
AVC_CODEC_TYPE,
|
||||||
'SPS data exceeds buffer length'
|
'SPS data exceeds buffer length'
|
||||||
);
|
);
|
||||||
@ -74,22 +101,21 @@ export function parseAVCDecoderConfigurationRecord(
|
|||||||
|
|
||||||
// Read number of PPS
|
// Read number of PPS
|
||||||
if (offset >= codecPrivate.length) {
|
if (offset >= codecPrivate.length) {
|
||||||
throw new ParseCodecPrivateError(AVC_CODEC_TYPE, 'No space for PPS count');
|
throw new ParseCodecError(AVC_CODEC_TYPE, 'No space for PPS count');
|
||||||
}
|
}
|
||||||
const numOfPPS = codecPrivate[offset++];
|
const numOfPPS = readUint8(true);
|
||||||
const pps: Uint8Array[] = [];
|
const pps: Uint8Array[] = [];
|
||||||
|
|
||||||
// Parse PPS
|
// Parse PPS
|
||||||
for (let i = 0; i < numOfPPS; i++) {
|
for (let i = 0; i < numOfPPS; i++) {
|
||||||
if (offset + 2 > codecPrivate.length) {
|
if (offset + 2 > codecPrivate.length) {
|
||||||
throw new ParseCodecPrivateError(AVC_CODEC_TYPE, 'Invalid PPS length');
|
throw new ParseCodecError(AVC_CODEC_TYPE, 'Invalid PPS length');
|
||||||
}
|
}
|
||||||
|
|
||||||
const ppsLength = (codecPrivate[offset] << 8) | codecPrivate[offset + 1];
|
const ppsLength = readUint16(true);
|
||||||
offset += 2;
|
|
||||||
|
|
||||||
if (offset + ppsLength > codecPrivate.length) {
|
if (offset + ppsLength > codecPrivate.length) {
|
||||||
throw new ParseCodecPrivateError(
|
throw new ParseCodecError(
|
||||||
AVC_CODEC_TYPE,
|
AVC_CODEC_TYPE,
|
||||||
'PPS data exceeds buffer length'
|
'PPS data exceeds buffer length'
|
||||||
);
|
);
|
||||||
@ -110,16 +136,13 @@ export function parseAVCDecoderConfigurationRecord(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function genCodecIdByAVCDecoderConfigurationRecord(
|
export function genCodecStringByAVCDecoderConfigurationRecord(
|
||||||
config: AVCDecoderConfigurationRecordType
|
config: AVCDecoderConfigurationRecordType
|
||||||
): string {
|
): string {
|
||||||
const profileHex = config.avcProfileIndication.toString(16).padStart(2, '0');
|
const profileHex = config.avcProfileIndication.toString(16).padStart(2, '0');
|
||||||
const profileCompatHex = config.profileCompatibility
|
const profileCompatHex = config.profileCompatibility
|
||||||
.toString(16)
|
.toString(16)
|
||||||
.padStart(2, '0');
|
.padStart(2, '0');
|
||||||
const levelHex = (config.avcLevelIndication / 10)
|
const levelHex = config.avcLevelIndication.toString(16).padStart(2, '0');
|
||||||
.toString(16)
|
|
||||||
.replace(/./g, '')
|
|
||||||
.padStart(2, '0');
|
|
||||||
return `avc1.${profileHex}${profileCompatHex}${levelHex}`;
|
return `avc1.${profileHex}${profileCompatHex}${levelHex}`;
|
||||||
}
|
}
|
214
packages/matroska/src/codecs/hevc.ts
Normal file
214
packages/matroska/src/codecs/hevc.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import { ParseCodecError } from '@konoplayer/core/errors';
|
||||||
|
import { ArkErrors, type } from 'arktype';
|
||||||
|
import type { TrackEntryType } from '../schema';
|
||||||
|
|
||||||
|
export const HEVC_CODEC_TYPE = 'h265(HEVC)';
|
||||||
|
|
||||||
|
export const HEVCDecoderConfigurationRecordArraySchema = type({
|
||||||
|
arrayCompleteness: type.number,
|
||||||
|
nalUnitType: type.number,
|
||||||
|
numNalus: type.number,
|
||||||
|
nalUnit: type.instanceOf(Uint8Array<ArrayBufferLike>).array(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type HEVCDecoderConfigurationRecordArrayType =
|
||||||
|
typeof HEVCDecoderConfigurationRecordArraySchema.infer;
|
||||||
|
|
||||||
|
// Define the schema for HEVCDecoderConfigurationRecord
|
||||||
|
export const HEVCDecoderConfigurationRecordSchema = type({
|
||||||
|
configurationVersion: type.number, // Must be 1
|
||||||
|
generalProfileSpace: type.number,
|
||||||
|
generalTierFlag: type.number,
|
||||||
|
generalProfileIdc: type.number,
|
||||||
|
generalProfileCompatibilityFlags: type.number,
|
||||||
|
generalConstraintIndicatorFlags: type.number,
|
||||||
|
generalLevelIdc: type.number,
|
||||||
|
minSpatialSegmentationIdc: type.number,
|
||||||
|
parallelismType: type.number,
|
||||||
|
chromaFormat: type.number,
|
||||||
|
bitDepthLumaMinus8: type.number,
|
||||||
|
bitDepthChromaMinus8: type.number,
|
||||||
|
avgFrameRate: type.number,
|
||||||
|
constantFrameRate: type.number,
|
||||||
|
numTemporalLayers: type.number,
|
||||||
|
temporalIdNested: type.number,
|
||||||
|
lengthSizeMinusOne: type.number,
|
||||||
|
numOfArrays: type.number,
|
||||||
|
nalUnits: HEVCDecoderConfigurationRecordArraySchema.array(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type HEVCDecoderConfigurationRecordType =
|
||||||
|
typeof HEVCDecoderConfigurationRecordSchema.infer;
|
||||||
|
|
||||||
|
export function parseHEVCDecoderConfigurationRecord(
|
||||||
|
track: TrackEntryType
|
||||||
|
): HEVCDecoderConfigurationRecordType {
|
||||||
|
const codecPrivate = track.CodecPrivate;
|
||||||
|
if (!codecPrivate) {
|
||||||
|
throw new ParseCodecError(
|
||||||
|
HEVC_CODEC_TYPE,
|
||||||
|
'CodecPrivate of HEVC Track is missing'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const view = new DataView(codecPrivate.buffer);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
const readUint8 = (move: boolean) => {
|
||||||
|
const result = view.getUint8(offset);
|
||||||
|
if (move) {
|
||||||
|
offset += 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readUint16 = (move: boolean) => {
|
||||||
|
const result = view.getUint16(offset, false);
|
||||||
|
if (move) {
|
||||||
|
offset += 2;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readUint48 = (move: boolean) => {
|
||||||
|
const result =
|
||||||
|
view.getUint16(offset, false) * 2 ** 32 +
|
||||||
|
view.getUint32(offset + 2, false);
|
||||||
|
if (move) {
|
||||||
|
offset += 6;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readUint32 = (move: boolean) => {
|
||||||
|
const result = view.getUint32(offset, false);
|
||||||
|
if (move) {
|
||||||
|
offset += 4;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read and validate basic fields
|
||||||
|
const config: HEVCDecoderConfigurationRecordType = {
|
||||||
|
configurationVersion: readUint8(true),
|
||||||
|
generalProfileSpace: (readUint8(false) & 0xc0) >> 6,
|
||||||
|
generalTierFlag: (readUint8(false) & 0x20) >> 5,
|
||||||
|
generalProfileIdc: readUint8(true) & 0x1f,
|
||||||
|
generalProfileCompatibilityFlags: readUint32(true),
|
||||||
|
generalConstraintIndicatorFlags: readUint48(true),
|
||||||
|
generalLevelIdc: readUint8(true),
|
||||||
|
minSpatialSegmentationIdc: readUint16(true) & 0x0fff,
|
||||||
|
parallelismType: readUint8(true) & 0x03,
|
||||||
|
chromaFormat: readUint8(true) & 0x03,
|
||||||
|
bitDepthLumaMinus8: readUint8(true) & 0x07,
|
||||||
|
bitDepthChromaMinus8: readUint8(true) & 0x07,
|
||||||
|
avgFrameRate: readUint16(true),
|
||||||
|
constantFrameRate: (readUint8(false) & 0xc0) >> 6,
|
||||||
|
numTemporalLayers: (readUint8(false) & 0x38) >> 3,
|
||||||
|
temporalIdNested: (readUint8(false) & 0x04) >> 2,
|
||||||
|
lengthSizeMinusOne: readUint8(true) & 0x03,
|
||||||
|
numOfArrays: readUint8(true),
|
||||||
|
nalUnits: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse NAL unit arrays
|
||||||
|
const arrays = config.nalUnits;
|
||||||
|
|
||||||
|
for (let i = 0; i < config.numOfArrays; i++) {
|
||||||
|
const array: HEVCDecoderConfigurationRecordArrayType = {
|
||||||
|
arrayCompleteness: (readUint8(false) & 0x80) >> 7,
|
||||||
|
nalUnitType: readUint8(true) & 0x3f,
|
||||||
|
numNalus: readUint16(true),
|
||||||
|
nalUnit: [] as Uint8Array<ArrayBufferLike>[],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let j = 0; j < array.numNalus; j++) {
|
||||||
|
const nalUnitLength = readUint16(true);
|
||||||
|
array.nalUnit.push(codecPrivate.subarray(offset, offset + nalUnitLength));
|
||||||
|
offset += nalUnitLength;
|
||||||
|
}
|
||||||
|
arrays.push(array);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate using arktype
|
||||||
|
const validation = HEVCDecoderConfigurationRecordSchema(config);
|
||||||
|
if (validation instanceof ArkErrors) {
|
||||||
|
const error = new ParseCodecError(
|
||||||
|
HEVC_CODEC_TYPE,
|
||||||
|
'Invalid HEVC configuration record'
|
||||||
|
);
|
||||||
|
error.cause = validation;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reverseBits32(value: number): number {
|
||||||
|
let result = 0;
|
||||||
|
for (let i = 0; i < 32; i++) {
|
||||||
|
result = (result << 1) | ((value >> i) & 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see[webkit implementation](https://github.com/movableink/webkit/blob/7e43fe7000b319ce68334c09eed1031642099726/Source/WebCore/platform/graphics/HEVCUtilities.cpp#L204)
|
||||||
|
*/
|
||||||
|
export function genCodecStringByHEVCDecoderConfigurationRecord(
|
||||||
|
config: HEVCDecoderConfigurationRecordType
|
||||||
|
) {
|
||||||
|
const result: string[] = [];
|
||||||
|
|
||||||
|
// prefix
|
||||||
|
result.push(`hev${config.configurationVersion}`);
|
||||||
|
|
||||||
|
// Profile Space
|
||||||
|
if (config.generalProfileSpace > 0) {
|
||||||
|
const profileSpaceChar = String.fromCharCode(
|
||||||
|
'A'.charCodeAt(0) + config.generalProfileSpace - 1
|
||||||
|
);
|
||||||
|
result.push(profileSpaceChar + config.generalProfileIdc.toString());
|
||||||
|
} else {
|
||||||
|
result.push(config.generalProfileIdc.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile Compatibility Flags
|
||||||
|
const compatFlags = reverseBits32(config.generalProfileCompatibilityFlags)
|
||||||
|
.toString(16)
|
||||||
|
.toUpperCase();
|
||||||
|
result.push(compatFlags);
|
||||||
|
|
||||||
|
// Tier Flag and Level IDC
|
||||||
|
const tierPrefix = config.generalTierFlag ? 'H' : 'L';
|
||||||
|
result.push(tierPrefix + config.generalLevelIdc.toString());
|
||||||
|
|
||||||
|
// Constraint Indicator Flags
|
||||||
|
let constraintBytes: number[];
|
||||||
|
if (Array.isArray(config.generalConstraintIndicatorFlags)) {
|
||||||
|
constraintBytes = config.generalConstraintIndicatorFlags as number[];
|
||||||
|
} else {
|
||||||
|
// split 48 bit integer into 6 byte
|
||||||
|
const flags = BigInt(config.generalConstraintIndicatorFlags);
|
||||||
|
constraintBytes = [];
|
||||||
|
for (let i = 5; i >= 0; i--) {
|
||||||
|
constraintBytes.push(Number((flags >> BigInt(8 * i)) & BigInt(0xff)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find last non-zero byte
|
||||||
|
const lastNonZeroIndex = constraintBytes.reduce(
|
||||||
|
(last, byte, i) => (byte ? i : last),
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
if (lastNonZeroIndex >= 0) {
|
||||||
|
for (let i = 0; i <= lastNonZeroIndex; i++) {
|
||||||
|
const byteHex = constraintBytes[i]
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, '0')
|
||||||
|
.toUpperCase();
|
||||||
|
result.push(byteHex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.join('.');
|
||||||
|
}
|
@ -1,16 +1,26 @@
|
|||||||
import { AudioCodec } from '../../base/audio_codecs';
|
import { UnsupportedCodecError } from '@konoplayer/core/errors';
|
||||||
import { UnsupportedCodecError } from '../../base/errors';
|
import { VideoCodec, AudioCodec } from '@konoplayer/core/codecs';
|
||||||
import { VideoCodec } from '../../base/video_codecs';
|
|
||||||
import type { TrackEntryType } from '../schema';
|
import type { TrackEntryType } from '../schema';
|
||||||
import {
|
import {
|
||||||
genCodecIdByAudioSpecificConfig,
|
genCodecIdByAudioSpecificConfig,
|
||||||
parseAudioSpecificConfig,
|
parseAudioSpecificConfig,
|
||||||
} from './aac';
|
} from './aac';
|
||||||
import {
|
import {
|
||||||
genCodecIdByAVCDecoderConfigurationRecord,
|
genCodecStringByAVCDecoderConfigurationRecord,
|
||||||
parseAVCDecoderConfigurationRecord,
|
parseAVCDecoderConfigurationRecord,
|
||||||
} from './avc';
|
} from './avc';
|
||||||
import type {ProbeInfo} from "@/media/mkv/enhance/probe.ts";
|
import {
|
||||||
|
genCodecStringByAV1DecoderConfigurationRecord,
|
||||||
|
parseAV1DecoderConfigurationRecord,
|
||||||
|
} from './av1.ts';
|
||||||
|
import {
|
||||||
|
genCodecStringByHEVCDecoderConfigurationRecord,
|
||||||
|
parseHEVCDecoderConfigurationRecord,
|
||||||
|
} from './hevc.ts';
|
||||||
|
import {
|
||||||
|
genCodecStringByVP9DecoderConfigurationRecord,
|
||||||
|
parseVP9DecoderConfigurationRecord,
|
||||||
|
} from './vp9.ts';
|
||||||
|
|
||||||
export const VideoCodecId = {
|
export const VideoCodecId = {
|
||||||
VCM: 'V_MS/VFW/FOURCC',
|
VCM: 'V_MS/VFW/FOURCC',
|
||||||
@ -108,63 +118,85 @@ export type SubtitleCodecIdType =
|
|||||||
| `${(typeof SubtitleCodecId)[keyof typeof SubtitleCodecId]}`
|
| `${(typeof SubtitleCodecId)[keyof typeof SubtitleCodecId]}`
|
||||||
| string;
|
| string;
|
||||||
|
|
||||||
|
|
||||||
export interface VideoDecoderConfigExt extends VideoDecoderConfig {
|
export interface VideoDecoderConfigExt extends VideoDecoderConfig {
|
||||||
codecType: VideoCodec,
|
codecType: VideoCodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function videoCodecIdToWebCodecs(
|
export function videoCodecIdToWebCodecs(
|
||||||
track: TrackEntryType,
|
track: TrackEntryType,
|
||||||
_probeInfo?: ProbeInfo
|
keyframe: Uint8Array
|
||||||
): VideoDecoderConfigExt {
|
): VideoDecoderConfigExt {
|
||||||
const codecId = track.CodecID;
|
const codecId = track.CodecID;
|
||||||
const codecPrivate = track.CodecPrivate;
|
const codecPrivate = track.CodecPrivate;
|
||||||
const shareOptions = {
|
const shareOptions = {
|
||||||
description: codecPrivate
|
description: codecPrivate,
|
||||||
}
|
};
|
||||||
switch (codecId) {
|
switch (codecId) {
|
||||||
case VideoCodecId.HEVC:
|
case VideoCodecId.HEVC:
|
||||||
return { ...shareOptions, codecType: VideoCodec.HEVC, codec: 'hevc' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: VideoCodec.HEVC,
|
||||||
|
codec: genCodecStringByHEVCDecoderConfigurationRecord(
|
||||||
|
parseHEVCDecoderConfigurationRecord(track)
|
||||||
|
),
|
||||||
|
};
|
||||||
case VideoCodecId.VP9:
|
case VideoCodecId.VP9:
|
||||||
return { ...shareOptions, codecType: VideoCodec.VP9, codec: 'vp09' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: VideoCodec.VP9,
|
||||||
|
codec: genCodecStringByVP9DecoderConfigurationRecord(
|
||||||
|
parseVP9DecoderConfigurationRecord(track, keyframe)
|
||||||
|
),
|
||||||
|
};
|
||||||
case VideoCodecId.AV1:
|
case VideoCodecId.AV1:
|
||||||
return { ...shareOptions, codecType: VideoCodec.AV1, codec: 'av1' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: VideoCodec.AV1,
|
||||||
|
codec: genCodecStringByAV1DecoderConfigurationRecord(
|
||||||
|
parseAV1DecoderConfigurationRecord(track)
|
||||||
|
),
|
||||||
|
};
|
||||||
case VideoCodecId.H264:
|
case VideoCodecId.H264:
|
||||||
if (!codecPrivate) {
|
|
||||||
throw new UnsupportedCodecError(
|
|
||||||
'h264(without codec_private profile)',
|
|
||||||
'web codecs audio decoder'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: VideoCodec.H264,
|
codecType: VideoCodec.H264,
|
||||||
codec: genCodecIdByAVCDecoderConfigurationRecord(
|
codec: genCodecStringByAVCDecoderConfigurationRecord(
|
||||||
parseAVCDecoderConfigurationRecord(codecPrivate)
|
parseAVCDecoderConfigurationRecord(track)
|
||||||
)
|
),
|
||||||
};
|
};
|
||||||
case VideoCodecId.THEORA:
|
case VideoCodecId.THEORA:
|
||||||
return { ...shareOptions, codecType: VideoCodec.Theora, codec: 'theora' };
|
return { ...shareOptions, codecType: VideoCodec.Theora, codec: 'theora' };
|
||||||
case VideoCodecId.VP8:
|
case VideoCodecId.VP8:
|
||||||
return { ...shareOptions, codecType: VideoCodec.VP8, codec: 'vp8' };
|
return { ...shareOptions, codecType: VideoCodec.VP8, codec: 'vp8' };
|
||||||
case VideoCodecId.MPEG4_ISO_SP:
|
case VideoCodecId.MPEG4_ISO_SP:
|
||||||
return { ...shareOptions, codecType: VideoCodec.MPEG4, codec: 'mp4v.01.3' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: VideoCodec.MPEG4,
|
||||||
|
codec: 'mp4v.01.3',
|
||||||
|
};
|
||||||
case VideoCodecId.MPEG4_ISO_ASP:
|
case VideoCodecId.MPEG4_ISO_ASP:
|
||||||
return { ...shareOptions, codecType: VideoCodec.MPEG4, codec: 'mp4v.20.9' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: VideoCodec.MPEG4,
|
||||||
|
codec: 'mp4v.20.9',
|
||||||
|
};
|
||||||
case VideoCodecId.MPEG4_ISO_AP:
|
case VideoCodecId.MPEG4_ISO_AP:
|
||||||
return { ...shareOptions, codecType: VideoCodec.MPEG4, codec: 'mp4v.20.9' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: VideoCodec.MPEG4,
|
||||||
|
codec: 'mp4v.20.9',
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedCodecError(codecId, 'web codecs video decoder');
|
throw new UnsupportedCodecError(codecId, 'web codecs video decoder');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AudioDecoderConfigExt extends AudioDecoderConfig {
|
export interface AudioDecoderConfigExt extends AudioDecoderConfig {
|
||||||
codecType: AudioCodec,
|
codecType: AudioCodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function audioCodecIdToWebCodecs(
|
export function audioCodecIdToWebCodecs(
|
||||||
track: TrackEntryType,
|
track: TrackEntryType
|
||||||
_probeInfo?: ProbeInfo
|
|
||||||
): AudioDecoderConfigExt {
|
): AudioDecoderConfigExt {
|
||||||
const codecId = track.CodecID;
|
const codecId = track.CodecID;
|
||||||
const codecPrivate = track.CodecPrivate;
|
const codecPrivate = track.CodecPrivate;
|
||||||
@ -175,8 +207,8 @@ export function audioCodecIdToWebCodecs(
|
|||||||
const shareOptions = {
|
const shareOptions = {
|
||||||
numberOfChannels,
|
numberOfChannels,
|
||||||
sampleRate,
|
sampleRate,
|
||||||
description: codecPrivate
|
description: codecPrivate,
|
||||||
}
|
};
|
||||||
|
|
||||||
switch (track.CodecID) {
|
switch (track.CodecID) {
|
||||||
case AudioCodecId.AAC_MPEG4_MAIN:
|
case AudioCodecId.AAC_MPEG4_MAIN:
|
||||||
@ -184,85 +216,94 @@ export function audioCodecIdToWebCodecs(
|
|||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: AudioCodec.AAC,
|
codecType: AudioCodec.AAC,
|
||||||
codec: 'mp4a.40.1'
|
codec: 'mp4a.40.1',
|
||||||
};
|
};
|
||||||
case AudioCodecId.AAC_MPEG2_LC:
|
case AudioCodecId.AAC_MPEG2_LC:
|
||||||
case AudioCodecId.AAC_MPEG4_LC:
|
case AudioCodecId.AAC_MPEG4_LC:
|
||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: AudioCodec.AAC,
|
codecType: AudioCodec.AAC,
|
||||||
codec: 'mp4a.40.2'
|
codec: 'mp4a.40.2',
|
||||||
};
|
};
|
||||||
case AudioCodecId.AAC_MPEG2_SSR:
|
case AudioCodecId.AAC_MPEG2_SSR:
|
||||||
case AudioCodecId.AAC_MPEG4_SSR:
|
case AudioCodecId.AAC_MPEG4_SSR:
|
||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: AudioCodec.AAC,
|
codecType: AudioCodec.AAC,
|
||||||
codec: 'mp4a.40.3'
|
codec: 'mp4a.40.3',
|
||||||
};
|
};
|
||||||
case AudioCodecId.AAC_MPEG4_LTP:
|
case AudioCodecId.AAC_MPEG4_LTP:
|
||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: AudioCodec.AAC,
|
codecType: AudioCodec.AAC,
|
||||||
codec: 'mp4a.40.4'
|
codec: 'mp4a.40.4',
|
||||||
};
|
};
|
||||||
case AudioCodecId.AAC_MPEG2_LC_SBR:
|
case AudioCodecId.AAC_MPEG2_LC_SBR:
|
||||||
case AudioCodecId.AAC_MPEG4_SBR:
|
case AudioCodecId.AAC_MPEG4_SBR:
|
||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: AudioCodec.AAC,
|
codecType: AudioCodec.AAC,
|
||||||
codec: 'mp4a.40.5'
|
codec: 'mp4a.40.5',
|
||||||
};
|
};
|
||||||
case AudioCodecId.AAC:
|
case AudioCodecId.AAC:
|
||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: AudioCodec.AAC,
|
codecType: AudioCodec.AAC,
|
||||||
codec: codecPrivate
|
codec: codecPrivate
|
||||||
? genCodecIdByAudioSpecificConfig(
|
? genCodecIdByAudioSpecificConfig(
|
||||||
parseAudioSpecificConfig(codecPrivate)
|
parseAudioSpecificConfig(codecPrivate)
|
||||||
) : 'mp4a.40.2',
|
)
|
||||||
|
: 'mp4a.40.2',
|
||||||
};
|
};
|
||||||
case AudioCodecId.AC3:
|
case AudioCodecId.AC3:
|
||||||
case AudioCodecId.AC3_BSID9:
|
case AudioCodecId.AC3_BSID9:
|
||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: AudioCodec.AC3,
|
codecType: AudioCodec.AC3,
|
||||||
codec: 'ac-3'
|
codec: 'ac-3',
|
||||||
};
|
};
|
||||||
case AudioCodecId.EAC3:
|
case AudioCodecId.EAC3:
|
||||||
case AudioCodecId.AC3_BSID10:
|
case AudioCodecId.AC3_BSID10:
|
||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: AudioCodec.EAC3,
|
codecType: AudioCodec.EAC3,
|
||||||
codec: 'ec-3'
|
codec: 'ec-3',
|
||||||
};
|
};
|
||||||
case AudioCodecId.MPEG_L3:
|
case AudioCodecId.MPEG_L3:
|
||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: AudioCodec.MP3,
|
codecType: AudioCodec.MP3,
|
||||||
codec: 'mp3'
|
codec: 'mp3',
|
||||||
};
|
};
|
||||||
case AudioCodecId.VORBIS:
|
case AudioCodecId.VORBIS:
|
||||||
return { ...shareOptions, codecType: AudioCodec.Vorbis, codec: 'vorbis' }
|
return { ...shareOptions, codecType: AudioCodec.Vorbis, codec: 'vorbis' };
|
||||||
;
|
|
||||||
case AudioCodecId.FLAC:
|
case AudioCodecId.FLAC:
|
||||||
return { ...shareOptions, codecType: AudioCodec.FLAC, codec: 'flac' }
|
return { ...shareOptions, codecType: AudioCodec.FLAC, codec: 'flac' };
|
||||||
;
|
|
||||||
case AudioCodecId.OPUS:
|
case AudioCodecId.OPUS:
|
||||||
return { ...shareOptions, codecType: AudioCodec.Opus, codec: 'opus' }
|
return { ...shareOptions, codecType: AudioCodec.Opus, codec: 'opus' };
|
||||||
;
|
|
||||||
case AudioCodecId.ALAC:
|
case AudioCodecId.ALAC:
|
||||||
return { ...shareOptions, codecType: AudioCodec.ALAC, codec: 'alac' }
|
return { ...shareOptions, codecType: AudioCodec.ALAC, codec: 'alac' };
|
||||||
;
|
|
||||||
case AudioCodecId.PCM_INT_BIG:
|
case AudioCodecId.PCM_INT_BIG:
|
||||||
if (bitDepth === 16) {
|
if (bitDepth === 16) {
|
||||||
return { ...shareOptions, codecType: AudioCodec.PCM_S16BE, codec: 'pcm-s16be' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: AudioCodec.PCM_S16BE,
|
||||||
|
codec: 'pcm-s16be',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (bitDepth === 24) {
|
if (bitDepth === 24) {
|
||||||
return { ...shareOptions, codecType: AudioCodec.PCM_S24BE, codec: 'pcm-s24be' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: AudioCodec.PCM_S24BE,
|
||||||
|
codec: 'pcm-s24be',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (bitDepth === 32) {
|
if (bitDepth === 32) {
|
||||||
return { ...shareOptions, codecType: AudioCodec.PCM_S32BE, codec: 'pcm-s32be' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: AudioCodec.PCM_S32BE,
|
||||||
|
codec: 'pcm-s32be',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
throw new UnsupportedCodecError(
|
throw new UnsupportedCodecError(
|
||||||
`${codecId}(${bitDepth}b)`,
|
`${codecId}(${bitDepth}b)`,
|
||||||
@ -270,20 +311,36 @@ export function audioCodecIdToWebCodecs(
|
|||||||
);
|
);
|
||||||
case AudioCodecId.PCM_INT_LIT:
|
case AudioCodecId.PCM_INT_LIT:
|
||||||
if (bitDepth === 16) {
|
if (bitDepth === 16) {
|
||||||
return { ...shareOptions, codecType: AudioCodec.PCM_S16LE, codec: 'pcm-s16le' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: AudioCodec.PCM_S16LE,
|
||||||
|
codec: 'pcm-s16le',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (bitDepth === 24) {
|
if (bitDepth === 24) {
|
||||||
return { ...shareOptions, codecType: AudioCodec.PCM_S24LE, codec: 'pcm-s24le' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: AudioCodec.PCM_S24LE,
|
||||||
|
codec: 'pcm-s24le',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (bitDepth === 32) {
|
if (bitDepth === 32) {
|
||||||
return { ...shareOptions, codecType: AudioCodec.PCM_S32LE, codec: 'pcm-s32le' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: AudioCodec.PCM_S32LE,
|
||||||
|
codec: 'pcm-s32le',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
throw new UnsupportedCodecError(
|
throw new UnsupportedCodecError(
|
||||||
`${codecId}(${bitDepth}b)`,
|
`${codecId}(${bitDepth}b)`,
|
||||||
'web codecs audio decoder'
|
'web codecs audio decoder'
|
||||||
);
|
);
|
||||||
case AudioCodecId.PCM_FLOAT_IEEE:
|
case AudioCodecId.PCM_FLOAT_IEEE:
|
||||||
return { ...shareOptions, codecType: AudioCodec.PCM_F32LE, codec: 'pcm-f32le' };
|
return {
|
||||||
|
...shareOptions,
|
||||||
|
codecType: AudioCodec.PCM_F32LE,
|
||||||
|
codec: 'pcm-f32le',
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedCodecError(codecId, 'web codecs audio decoder');
|
throw new UnsupportedCodecError(codecId, 'web codecs audio decoder');
|
||||||
}
|
}
|
232
packages/matroska/src/codecs/vp9.ts
Normal file
232
packages/matroska/src/codecs/vp9.ts
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import { type } from 'arktype';
|
||||||
|
import type { TrackEntryType } from '../schema';
|
||||||
|
import { BitReader } from '@konoplayer/core/data';
|
||||||
|
import { ParseCodecError } from '@konoplayer/core/errors';
|
||||||
|
|
||||||
|
export const VP9_CODEC_TYPE = 'vp9';
|
||||||
|
|
||||||
|
export enum VP9ColorSpaceEnum {
|
||||||
|
UNKNOWN = 0,
|
||||||
|
BT_601 = 1, // eq bt_470bg
|
||||||
|
BT_709 = 2,
|
||||||
|
SMPTE_170 = 3,
|
||||||
|
SMPTE_240 = 4,
|
||||||
|
BT_2020 = 5,
|
||||||
|
RESERVED = 6,
|
||||||
|
SRGB = 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VP9YUVRange {
|
||||||
|
STUDIO_SWING = 0,
|
||||||
|
FULL_SWING = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VP9Subsampling {
|
||||||
|
UNKNOWN = 0,
|
||||||
|
YUV420 = 1,
|
||||||
|
YUV422 = 2,
|
||||||
|
YUV440 = 3,
|
||||||
|
YUV444 = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VP9PerformenceLevel = [
|
||||||
|
{ level: '10', maxSampleRate: 829440, maxResolution: 36864 }, // Level 1
|
||||||
|
{ level: '11', maxSampleRate: 2764800, maxResolution: 73728 }, // Level 1
|
||||||
|
{ level: '20', maxSampleRate: 4608000, maxResolution: 122880 }, // Level 2
|
||||||
|
{ level: '21', maxSampleRate: 9216000, maxResolution: 245760 }, // Level 2.1
|
||||||
|
{ level: '30', maxSampleRate: 20736000, maxResolution: 552960 }, // Level 3
|
||||||
|
{ level: '31', maxSampleRate: 36864000, maxResolution: 983040 }, // Level 3.1
|
||||||
|
{ level: '40', maxSampleRate: 83558400, maxResolution: 2228224 }, // Level 4
|
||||||
|
{ level: '41', maxSampleRate: 160432128, maxResolution: 2228224 }, // Level 4.1
|
||||||
|
{ level: '50', maxSampleRate: 311951360, maxResolution: 8912896 }, // Level 5
|
||||||
|
{ level: '51', maxSampleRate: 588251136, maxResolution: 8912896 }, // Level 5.1
|
||||||
|
{ level: '52', maxSampleRate: 1176502272, maxResolution: 8912896 }, // Level 5.2
|
||||||
|
{ level: '60', maxSampleRate: 1176502272, maxResolution: 35651584 }, // Level 6
|
||||||
|
{ level: '61', maxSampleRate: 2353004544, maxResolution: 35651584 }, // Level 6.1
|
||||||
|
{ level: '62', maxSampleRate: 4706009088, maxResolution: 35651584 }, // Level 6.2
|
||||||
|
];
|
||||||
|
|
||||||
|
export const VP9DecoderConfigurationRecordSchema = type({
|
||||||
|
profile: type.number, // 0 | 1 | 2 | 3,
|
||||||
|
bitDepth: type.number, // 8 | 10 | 12
|
||||||
|
colorSpace: type.number,
|
||||||
|
subsampling: type.number, // 420 | 422 | 444
|
||||||
|
width: type.number,
|
||||||
|
height: type.number,
|
||||||
|
yuvRangeFlag: type.number.optional(),
|
||||||
|
hasScaling: type.boolean,
|
||||||
|
renderWidth: type.number,
|
||||||
|
renderHeight: type.number,
|
||||||
|
frameRate: type.number, // frame per second
|
||||||
|
estimateLevel: type.string,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type VP9DecoderConfigurationRecordType =
|
||||||
|
typeof VP9DecoderConfigurationRecordSchema.infer;
|
||||||
|
|
||||||
|
export function parseVP9DecoderConfigurationRecord(
|
||||||
|
track: TrackEntryType,
|
||||||
|
keyframe: Uint8Array
|
||||||
|
): VP9DecoderConfigurationRecordType {
|
||||||
|
const reader = new BitReader(keyframe);
|
||||||
|
const frameRate = 1_000_000_000 / Number(track.DefaultDuration) || 30;
|
||||||
|
|
||||||
|
// Frame Marker: 2 bits, must be 0b10
|
||||||
|
const frameMarker = reader.readBits(2);
|
||||||
|
if (frameMarker !== 2) {
|
||||||
|
throw new ParseCodecError(VP9_CODEC_TYPE, 'invalid frame marker');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile: 2 bits
|
||||||
|
const version = reader.readBits(1);
|
||||||
|
const high = reader.readBits(1);
|
||||||
|
|
||||||
|
const profile = (high << 1) + version;
|
||||||
|
|
||||||
|
let reservedZero = 0;
|
||||||
|
if (profile === 3) {
|
||||||
|
reservedZero = reader.readBits(1);
|
||||||
|
if (reservedZero !== 0) {
|
||||||
|
throw new ParseCodecError(
|
||||||
|
VP9_CODEC_TYPE,
|
||||||
|
'Invalid reserved zero bit for profile 3'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Existing Frame: 1 bit
|
||||||
|
const showExistingFrame = reader.readBits(1);
|
||||||
|
if (showExistingFrame === 1) {
|
||||||
|
throw new ParseCodecError(VP9_CODEC_TYPE, 'not a keyframe to parse');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame Type: 1 bit (0 = keyframe)
|
||||||
|
const frameType = reader.readBits(1);
|
||||||
|
if (frameType !== 0) {
|
||||||
|
throw new ParseCodecError(VP9_CODEC_TYPE, 'not a keyframe to parse');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Frame and Error Resilient
|
||||||
|
const _showFrame = reader.readBits(1);
|
||||||
|
const _errorResilient = reader.readBits(1);
|
||||||
|
|
||||||
|
// Sync Code: 3 bytes (0x49, 0x83, 0x42)
|
||||||
|
const syncCode =
|
||||||
|
(reader.readBits(8) << 16) | (reader.readBits(8) << 8) | reader.readBits(8);
|
||||||
|
if (syncCode !== 0x498342) {
|
||||||
|
throw new ParseCodecError(VP9_CODEC_TYPE, 'Invalid sync code');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bit Depth
|
||||||
|
let bitDepth: number;
|
||||||
|
if (profile >= 2) {
|
||||||
|
const tenOrTwelveBit = reader.readBits(1);
|
||||||
|
bitDepth = tenOrTwelveBit === 0 ? 10 : 12;
|
||||||
|
} else {
|
||||||
|
bitDepth = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorSpace = reader.readBits(3);
|
||||||
|
|
||||||
|
let subsamplingX: number;
|
||||||
|
let subsamplingY: number;
|
||||||
|
let yuvRangeFlag: number | undefined;
|
||||||
|
if (colorSpace !== VP9ColorSpaceEnum.SRGB) {
|
||||||
|
yuvRangeFlag = reader.readBits(1);
|
||||||
|
if (profile === 1 || profile === 3) {
|
||||||
|
subsamplingX = reader.readBits(1);
|
||||||
|
subsamplingY = reader.readBits(1);
|
||||||
|
reservedZero = reader.readBits(1);
|
||||||
|
} else {
|
||||||
|
subsamplingX = 1;
|
||||||
|
subsamplingY = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (profile !== 1 && profile !== 3) {
|
||||||
|
throw new ParseCodecError(
|
||||||
|
VP9_CODEC_TYPE,
|
||||||
|
'VP9 profile with sRGB ColorSpace must be 1 or 3'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
subsamplingX = 0;
|
||||||
|
subsamplingY = 0;
|
||||||
|
reservedZero = reader.readBits(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let subsampling: VP9Subsampling;
|
||||||
|
|
||||||
|
if (!subsamplingX && subsamplingY) {
|
||||||
|
subsampling = VP9Subsampling.YUV440;
|
||||||
|
} else if (subsamplingX && !subsamplingY) {
|
||||||
|
subsampling = VP9Subsampling.YUV422;
|
||||||
|
} else if (subsamplingX && subsamplingY) {
|
||||||
|
subsampling = VP9Subsampling.YUV420;
|
||||||
|
} else if (!subsamplingX && !subsamplingY) {
|
||||||
|
subsampling = VP9Subsampling.YUV444;
|
||||||
|
} else {
|
||||||
|
subsampling = VP9Subsampling.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame Size (resolution)
|
||||||
|
const widthMinus1 = reader.readBits(16);
|
||||||
|
const heightMinus1 = reader.readBits(16);
|
||||||
|
const hasScaling = !!reader.readBits(1);
|
||||||
|
let renderWidthMinus1 = widthMinus1;
|
||||||
|
let renderHeightMinus1 = heightMinus1;
|
||||||
|
if (hasScaling) {
|
||||||
|
renderWidthMinus1 = reader.readBits(16);
|
||||||
|
renderHeightMinus1 = reader.readBits(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = widthMinus1 + 1;
|
||||||
|
const height = heightMinus1 + 1;
|
||||||
|
|
||||||
|
const sampleRate = width * height * frameRate;
|
||||||
|
const resolution = width * height;
|
||||||
|
|
||||||
|
let estimateLevel = '62';
|
||||||
|
for (const { level, maxSampleRate, maxResolution } of VP9PerformenceLevel) {
|
||||||
|
if (sampleRate <= maxSampleRate && resolution <= maxResolution) {
|
||||||
|
// 检查 profile 和 bitDepth 的额外要求
|
||||||
|
if (profile >= 2 && bitDepth > 8 && Number.parseFloat(level) < 20) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
estimateLevel = level;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile,
|
||||||
|
bitDepth,
|
||||||
|
colorSpace,
|
||||||
|
subsampling,
|
||||||
|
yuvRangeFlag,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
hasScaling,
|
||||||
|
renderWidth: renderWidthMinus1 + 1,
|
||||||
|
renderHeight: renderHeightMinus1 + 1,
|
||||||
|
frameRate,
|
||||||
|
estimateLevel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The format of the 'vp09' codec string is specified in the webm GitHub repo:
|
||||||
|
// <https://github.com/webmproject/vp9-dash/blob/master/VPCodecISOMediaFileFormatBinding.md#codecs-parameter-string>
|
||||||
|
//
|
||||||
|
// The codecs parameter string for the VP codec family is as follows:
|
||||||
|
// <sample entry 4CC>.<profile>.<level>.<bitDepth>.<chromaSubsampling>.
|
||||||
|
// <colourPrimaries>.<transferCharacteristics>.<matrixCoefficients>.
|
||||||
|
// <videoFullRangeFlag>
|
||||||
|
// All parameter values are expressed as double-digit decimals.
|
||||||
|
// sample entry 4CC, profile, level, and bitDepth are all mandatory fields.
|
||||||
|
export function genCodecStringByVP9DecoderConfigurationRecord(
|
||||||
|
config: VP9DecoderConfigurationRecordType
|
||||||
|
): string {
|
||||||
|
const profileStr = config.profile.toString().padStart(2, '0');
|
||||||
|
const bitDepthStr = config.bitDepth.toString().padStart(2, '0');
|
||||||
|
const levelStr = config.estimateLevel;
|
||||||
|
|
||||||
|
return `vp09.${profileStr}.${levelStr}.${bitDepthStr}`;
|
||||||
|
}
|
@ -30,14 +30,18 @@ import {
|
|||||||
TagSchema,
|
TagSchema,
|
||||||
type TagType,
|
type TagType,
|
||||||
TrackEntrySchema,
|
TrackEntrySchema,
|
||||||
type TrackEntryType, TrackTypeRestrictionEnum,
|
type TrackEntryType,
|
||||||
|
TrackTypeRestrictionEnum,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
import {concatBufs} from "konoebml/lib/tools";
|
import { concatBufs } from 'konoebml/lib/tools';
|
||||||
import {ParseCodecErrors, UnreachableOrLogicError, UnsupportedCodecError} from "@/media/base/errors.ts";
|
import {
|
||||||
import type {ProbeInfo} from "@/media/mkv/enhance/probe.ts";
|
ParseCodecErrors,
|
||||||
import {audioCodecIdToWebCodecs, videoCodecIdToWebCodecs} from "@/media/mkv/codecs";
|
UnreachableOrLogicError,
|
||||||
import {Queue} from "mnemonist";
|
UnsupportedCodecError,
|
||||||
import {BehaviorSubject} from "rxjs";
|
} from '@konoplayer/core/errors';
|
||||||
|
import { audioCodecIdToWebCodecs, videoCodecIdToWebCodecs } from './codecs';
|
||||||
|
import { Queue } from 'mnemonist';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
export const SEEK_ID_KAX_INFO = new Uint8Array([0x15, 0x49, 0xa9, 0x66]);
|
export const SEEK_ID_KAX_INFO = new Uint8Array([0x15, 0x49, 0xa9, 0x66]);
|
||||||
export const SEEK_ID_KAX_TRACKS = new Uint8Array([0x16, 0x54, 0xae, 0x6b]);
|
export const SEEK_ID_KAX_TRACKS = new Uint8Array([0x16, 0x54, 0xae, 0x6b]);
|
||||||
@ -47,10 +51,7 @@ export const SEEK_ID_KAX_TAGS = new Uint8Array([0x12, 0x54, 0xc3, 0x67]);
|
|||||||
export class SegmentSystem {
|
export class SegmentSystem {
|
||||||
startTag: EbmlSegmentTagType;
|
startTag: EbmlSegmentTagType;
|
||||||
headTags: EbmlTagType[] = [];
|
headTags: EbmlTagType[] = [];
|
||||||
teeStream: ReadableStream<Uint8Array>
|
|
||||||
teeBufferTask: Promise<Uint8Array>;
|
|
||||||
firstCluster: EbmlClusterTagType | undefined;
|
firstCluster: EbmlClusterTagType | undefined;
|
||||||
probInfo?: ProbeInfo;
|
|
||||||
|
|
||||||
cue: CueSystem;
|
cue: CueSystem;
|
||||||
cluster: ClusterSystem;
|
cluster: ClusterSystem;
|
||||||
@ -59,7 +60,7 @@ export class SegmentSystem {
|
|||||||
track: TrackSystem;
|
track: TrackSystem;
|
||||||
tag: TagSystem;
|
tag: TagSystem;
|
||||||
|
|
||||||
constructor(startNode: EbmlSegmentTagType, teeStream: ReadableStream<Uint8Array>) {
|
constructor(startNode: EbmlSegmentTagType) {
|
||||||
this.startTag = startNode;
|
this.startTag = startNode;
|
||||||
this.cue = new CueSystem(this);
|
this.cue = new CueSystem(this);
|
||||||
this.cluster = new ClusterSystem(this);
|
this.cluster = new ClusterSystem(this);
|
||||||
@ -67,35 +68,13 @@ export class SegmentSystem {
|
|||||||
this.info = new InfoSystem(this);
|
this.info = new InfoSystem(this);
|
||||||
this.track = new TrackSystem(this);
|
this.track = new TrackSystem(this);
|
||||||
this.tag = new TagSystem(this);
|
this.tag = new TagSystem(this);
|
||||||
this.teeStream = teeStream;
|
|
||||||
this.teeBufferTask = this.teeWaitingProbingData(teeStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async teeWaitingProbingData (teeStream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
|
|
||||||
const reader = teeStream.getReader();
|
|
||||||
const list: Uint8Array<ArrayBufferLike>[] = [];
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
list.push(value);
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.name === 'AbortError') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return concatBufs(...list)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get contentStartOffset() {
|
get contentStartOffset() {
|
||||||
return this.startTag.startOffset + this.startTag.headerLength;
|
return this.startTag.startOffset + this.startTag.headerLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
private seekLocal () {
|
private seekLocal() {
|
||||||
const infoTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_INFO);
|
const infoTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_INFO);
|
||||||
const tracksTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_TRACKS);
|
const tracksTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_TRACKS);
|
||||||
const cuesTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_CUES);
|
const cuesTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_CUES);
|
||||||
@ -131,7 +110,7 @@ export class SegmentSystem {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
async completeMeta () {
|
async completeMeta() {
|
||||||
this.seekLocal();
|
this.seekLocal();
|
||||||
|
|
||||||
await this.parseCodes();
|
await this.parseCodes();
|
||||||
@ -139,41 +118,19 @@ export class SegmentSystem {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchProbeInfo (_payload: Uint8Array): Promise<ProbeInfo> {
|
async parseCodes() {
|
||||||
// call local or remote ff-probe
|
const candidates = this.track.tracks.filter(
|
||||||
return {}
|
(c) =>
|
||||||
}
|
c.TrackType === TrackTypeRestrictionEnum.AUDIO ||
|
||||||
|
c.TrackType === TrackTypeRestrictionEnum.VIDEO
|
||||||
async parseCodes () {
|
);
|
||||||
const candidates = this.track.tracks.filter(c => c.TrackType === TrackTypeRestrictionEnum.AUDIO || c.TrackType === TrackTypeRestrictionEnum.VIDEO);
|
|
||||||
const parseErrors = new ParseCodecErrors();
|
const parseErrors = new ParseCodecErrors();
|
||||||
|
|
||||||
if (!this.probInfo) {
|
|
||||||
for (const t of candidates) {
|
|
||||||
try {
|
|
||||||
await this.track.initTrack(t, undefined)
|
|
||||||
} catch (e: unknown) {
|
|
||||||
parseErrors.cause.push(e as Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parseErrors.cause.length > 0) {
|
|
||||||
try {
|
|
||||||
const teeBuffer = await this.teeBufferTask;
|
|
||||||
this.probInfo = await this.fetchProbeInfo(teeBuffer);
|
|
||||||
} catch (e) {
|
|
||||||
parseErrors.cause.push(e as Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const t of candidates) {
|
for (const t of candidates) {
|
||||||
try {
|
try {
|
||||||
await this.track.initTrack(t, this.probInfo)
|
await this.track.initTrack(t, this.);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
parseErrors.cause.push(e as Error)
|
parseErrors.cause.push(e as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (parseErrors.cause.length > 0) {
|
if (parseErrors.cause.length > 0) {
|
||||||
@ -274,9 +231,9 @@ export class SeekSystem extends SegmentComponentSystemTrait<
|
|||||||
return this.seekTagByStartOffset(this.seekOffsetBySeekId(seekId));
|
return this.seekTagByStartOffset(this.seekOffsetBySeekId(seekId));
|
||||||
}
|
}
|
||||||
|
|
||||||
get firstClusterOffset () {
|
get firstClusterOffset() {
|
||||||
if (!this.segment.firstCluster) {
|
if (!this.segment.firstCluster) {
|
||||||
throw new UnreachableOrLogicError("first cluster not found")
|
throw new UnreachableOrLogicError('first cluster not found');
|
||||||
}
|
}
|
||||||
return this.segment.firstCluster.startOffset;
|
return this.segment.firstCluster.startOffset;
|
||||||
}
|
}
|
||||||
@ -320,11 +277,10 @@ export interface GetTrackEntryOptions {
|
|||||||
predicate?: (v: SegmentComponent<TrackEntryType>) => boolean;
|
predicate?: (v: SegmentComponent<TrackEntryType>) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface TrackState<Decoder, Config, Frame> {
|
export interface TrackState<Decoder, Config, Frame> {
|
||||||
decoder: Decoder,
|
decoder: Decoder;
|
||||||
configuration?: Config,
|
configuration?: Config;
|
||||||
frameBuffer$: BehaviorSubject<Queue<Frame>>
|
frameBuffer$: BehaviorSubject<Queue<Frame>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TrackSystem extends SegmentComponentSystemTrait<
|
export class TrackSystem extends SegmentComponentSystemTrait<
|
||||||
@ -336,8 +292,14 @@ export class TrackSystem extends SegmentComponentSystemTrait<
|
|||||||
}
|
}
|
||||||
|
|
||||||
tracks: SegmentComponent<TrackEntryType>[] = [];
|
tracks: SegmentComponent<TrackEntryType>[] = [];
|
||||||
videoTrackState = new WeakMap<TrackEntryType, TrackState< VideoDecoder, VideoDecoderConfig, VideoFrame>>();
|
videoTrackState = new WeakMap<
|
||||||
audioTrackState = new WeakMap<TrackEntryType, TrackState<AudioDecoder, AudioDecoderConfig, AudioData>>();
|
TrackEntryType,
|
||||||
|
TrackState<VideoDecoder, VideoDecoderConfig, VideoFrame>
|
||||||
|
>();
|
||||||
|
audioTrackState = new WeakMap<
|
||||||
|
TrackEntryType,
|
||||||
|
TrackState<AudioDecoder, AudioDecoderConfig, AudioData>
|
||||||
|
>();
|
||||||
|
|
||||||
getTrackEntry({
|
getTrackEntry({
|
||||||
priority = (track) =>
|
priority = (track) =>
|
||||||
@ -357,11 +319,11 @@ export class TrackSystem extends SegmentComponentSystemTrait<
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initTrack (track: TrackEntryType, probe?: ProbeInfo) {
|
async initTrack(track: TrackEntryType) {
|
||||||
if (track.TrackType === TrackTypeRestrictionEnum.AUDIO) {
|
if (track.TrackType === TrackTypeRestrictionEnum.AUDIO) {
|
||||||
const configuration = audioCodecIdToWebCodecs(track, probe);
|
const configuration = audioCodecIdToWebCodecs(track);
|
||||||
if (await AudioDecoder.isConfigSupported(configuration)) {
|
if (await AudioDecoder.isConfigSupported(configuration)) {
|
||||||
throw new UnsupportedCodecError(configuration.codec, 'audio decoder')
|
throw new UnsupportedCodecError(configuration.codec, 'audio decoder');
|
||||||
}
|
}
|
||||||
|
|
||||||
const queue$ = new BehaviorSubject(new Queue<AudioData>());
|
const queue$ = new BehaviorSubject(new Queue<AudioData>());
|
||||||
@ -378,11 +340,11 @@ export class TrackSystem extends SegmentComponentSystemTrait<
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
frameBuffer$: queue$,
|
frameBuffer$: queue$,
|
||||||
})
|
});
|
||||||
} else if (track.TrackType === TrackTypeRestrictionEnum.VIDEO) {
|
} else if (track.TrackType === TrackTypeRestrictionEnum.VIDEO) {
|
||||||
const configuration = videoCodecIdToWebCodecs(track, probe);
|
const configuration = videoCodecIdToWebCodecs(track, this.keyframe);
|
||||||
if (await VideoDecoder.isConfigSupported(configuration)) {
|
if (await VideoDecoder.isConfigSupported(configuration)) {
|
||||||
throw new UnsupportedCodecError(configuration.codec, 'audio decoder')
|
throw new UnsupportedCodecError(configuration.codec, 'audio decoder');
|
||||||
}
|
}
|
||||||
|
|
||||||
const queue$ = new BehaviorSubject(new Queue<VideoFrame>());
|
const queue$ = new BehaviorSubject(new Queue<VideoFrame>());
|
||||||
@ -399,7 +361,7 @@ export class TrackSystem extends SegmentComponentSystemTrait<
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
frameBuffer$: queue$,
|
frameBuffer$: queue$,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,9 @@
|
|||||||
import {EbmlStreamDecoder, EbmlTagIdEnum, EbmlTagPosition, type EbmlTagType,} from 'konoebml';
|
import {
|
||||||
|
EbmlStreamDecoder,
|
||||||
|
EbmlTagIdEnum,
|
||||||
|
EbmlTagPosition,
|
||||||
|
type EbmlTagType,
|
||||||
|
} from 'konoebml';
|
||||||
import {
|
import {
|
||||||
defer,
|
defer,
|
||||||
EMPTY,
|
EMPTY,
|
||||||
@ -19,10 +24,19 @@ import {
|
|||||||
takeWhile,
|
takeWhile,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {createRangedStream, type CreateRangedStreamOptions} from '@/fetch';
|
import {
|
||||||
import {type CueSystem, SEEK_ID_KAX_CUES, SEEK_ID_KAX_TAGS, type SegmentComponent, SegmentSystem,} from './model';
|
createRangedStream,
|
||||||
import {isTagIdPos, waitTick} from './util';
|
type CreateRangedStreamOptions,
|
||||||
import type {ClusterType} from './schema';
|
} from '@konoplayer/core/data';
|
||||||
|
import {
|
||||||
|
type CueSystem,
|
||||||
|
SEEK_ID_KAX_CUES,
|
||||||
|
SEEK_ID_KAX_TAGS,
|
||||||
|
type SegmentComponent,
|
||||||
|
SegmentSystem,
|
||||||
|
} from './model';
|
||||||
|
import { isTagIdPos, waitTick } from './util';
|
||||||
|
import type { ClusterType } from './schema';
|
||||||
|
|
||||||
export interface CreateRangedEbmlStreamOptions
|
export interface CreateRangedEbmlStreamOptions
|
||||||
extends CreateRangedStreamOptions {
|
extends CreateRangedStreamOptions {
|
||||||
@ -33,7 +47,7 @@ export function createRangedEbmlStream({
|
|||||||
url,
|
url,
|
||||||
byteStart = 0,
|
byteStart = 0,
|
||||||
byteEnd,
|
byteEnd,
|
||||||
tee = false
|
tee = false,
|
||||||
}: CreateRangedEbmlStreamOptions): Observable<{
|
}: CreateRangedEbmlStreamOptions): Observable<{
|
||||||
ebml$: Observable<EbmlTagType>;
|
ebml$: Observable<EbmlTagType>;
|
||||||
totalSize?: number;
|
totalSize?: number;
|
||||||
@ -135,12 +149,11 @@ export function createEbmlController({
|
|||||||
...options,
|
...options,
|
||||||
url,
|
url,
|
||||||
byteStart: 0,
|
byteStart: 0,
|
||||||
tee: true
|
tee: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const controller$ = metaRequest$.pipe(
|
const controller$ = metaRequest$.pipe(
|
||||||
map(({ totalSize, ebml$, response, controller, teeBody }) => {
|
map(({ totalSize, ebml$, response, controller, teeBody }) => {
|
||||||
|
|
||||||
const head$ = ebml$.pipe(
|
const head$ = ebml$.pipe(
|
||||||
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
|
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
|
||||||
take(1),
|
take(1),
|
||||||
@ -166,18 +179,30 @@ export function createEbmlController({
|
|||||||
const seekSystem = segment.seek;
|
const seekSystem = segment.seek;
|
||||||
|
|
||||||
const meta$ = ebml$.pipe(
|
const meta$ = ebml$.pipe(
|
||||||
scan((acc, tag) => {
|
scan(
|
||||||
// avoid object recreation
|
(acc, tag) => {
|
||||||
acc.hasKeyframe = acc.hasKeyframe || (tag.id === EbmlTagIdEnum.SimpleBlock && tag.keyframe) || (tag.id === EbmlTagIdEnum.BlockGroup && tag.children.every(c => c.id !== EbmlTagIdEnum.ReferenceBlock));
|
// avoid object recreation
|
||||||
acc.tag = tag;
|
acc.hasKeyframe =
|
||||||
return acc;
|
acc.hasKeyframe ||
|
||||||
}, { hasKeyframe: false, tag: undefined as unknown as EbmlTagType }),
|
(tag.id === EbmlTagIdEnum.SimpleBlock && tag.keyframe) ||
|
||||||
takeWhile(
|
(tag.id === EbmlTagIdEnum.BlockGroup &&
|
||||||
({ tag, hasKeyframe }) => {
|
tag.children.every(
|
||||||
return !isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End)(tag) && !(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)(tag) && hasKeyframe);
|
(c) => c.id !== EbmlTagIdEnum.ReferenceBlock
|
||||||
|
));
|
||||||
|
acc.tag = tag;
|
||||||
|
return acc;
|
||||||
},
|
},
|
||||||
true
|
{ hasKeyframe: false, tag: undefined as unknown as EbmlTagType }
|
||||||
),
|
),
|
||||||
|
takeWhile(({ tag, hasKeyframe }) => {
|
||||||
|
return (
|
||||||
|
!isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End)(tag) &&
|
||||||
|
!(
|
||||||
|
isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)(tag) &&
|
||||||
|
hasKeyframe
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, true),
|
||||||
map(({ tag }) => tag),
|
map(({ tag }) => tag),
|
||||||
share({
|
share({
|
||||||
resetOnComplete: false,
|
resetOnComplete: false,
|
||||||
@ -312,7 +337,10 @@ export function createEbmlController({
|
|||||||
acc.next = curr;
|
acc.next = curr;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
({ prev: undefined as (SegmentComponent<ClusterType> | undefined), next: undefined as SegmentComponent<ClusterType> | undefined })
|
{
|
||||||
|
prev: undefined as SegmentComponent<ClusterType> | undefined,
|
||||||
|
next: undefined as SegmentComponent<ClusterType> | undefined,
|
||||||
|
}
|
||||||
),
|
),
|
||||||
filter((c) => c.next?.Timestamp! > seekTime),
|
filter((c) => c.next?.Timestamp! > seekTime),
|
||||||
map((c) => c.prev ?? c.next!)
|
map((c) => c.prev ?? c.next!)
|
20
packages/matroska/tsconfig.json
Normal file
20
packages/matroska/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"paths": {
|
||||||
|
"@konoplayer/core/*": [
|
||||||
|
"../core/src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../core"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "konoplayer-demuxing"
|
name = "konoplayer-symphonia"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
symphonia-format-mkv = "0.5.4"
|
symphonia = "0.5.4"
|
709
pnpm-lock.yaml
generated
709
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -363,7 +363,6 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) {
|
|||||||
|
|
||||||
const selfSchema = [
|
const selfSchema = [
|
||||||
`export const ${el.name}Schema = type({`,
|
`export const ${el.name}Schema = type({`,
|
||||||
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
|
|
||||||
...associated.map((v) => {
|
...associated.map((v) => {
|
||||||
let meta: any;
|
let meta: any;
|
||||||
const restriction = generateRestriction(v);
|
const restriction = generateRestriction(v);
|
||||||
|
@ -24,9 +24,8 @@
|
|||||||
"target": "ES2021",
|
"target": "ES2021",
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"useDefineForClassFields": false,
|
||||||
"noUnusedParameters": true,
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"exactOptionalPropertyTypes": false,
|
"exactOptionalPropertyTypes": false,
|
||||||
|
"experimentalDecorators": true
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,6 +9,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.scripts.json"
|
"path": "./tsconfig.scripts.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./packages/matroska"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./packages/core"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./apps/test"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user