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

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

View File

@@ -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"
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "@konoplayer/core",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {},
"dependencies": {}
}

View File

@@ -0,0 +1,32 @@
export enum AudioCodec {
Unknown = 0,
AAC = 1,
MP3 = 2,
PCM = 3,
Vorbis = 4,
FLAC = 5,
AMR_NB = 6,
AMR_WB = 7,
PCM_MULAW = 8,
GSM_MS = 9,
PCM_S16BE = 10,
PCM_S24BE = 11,
Opus = 12,
EAC3 = 13,
PCM_ALAW = 14,
ALAC = 15,
AC3 = 16,
MpegHAudio = 17,
DTS = 18,
DTSXP2 = 19,
DTSE = 20,
AC4 = 21,
IAMF = 22,
PCM_S32BE = 23,
PCM_S32LE = 24,
PCM_S24LE = 25,
PCM_S16LE = 26,
PCM_F32BE = 27,
PCM_F32LE = 28,
MaxValue = PCM_F32LE, // Must equal the last "real" codec above.
}

View File

@@ -0,0 +1,2 @@
export { AudioCodec } from './audio-codecs';
export { VideoCodec } from './video-codecs';

View File

@@ -0,0 +1,97 @@
export enum VideoCodec {
Unknown = 0,
H264 = 1,
VC1 = 2,
MPEG2 = 3,
MPEG4 = 4,
Theora = 5,
VP8 = 6,
VP9 = 7,
HEVC = 8,
DolbyVision = 9,
AV1 = 10,
MaxValue = AV1, // Must equal the last "real" codec above.
}
export enum VideoCodecProfile {
VIDEO_CODEC_PROFILE_UNKNOWN = -1,
VIDEO_CODEC_PROFILE_MIN = VIDEO_CODEC_PROFILE_UNKNOWN,
H264PROFILE_MIN = 0,
H264PROFILE_BASELINE = H264PROFILE_MIN,
H264PROFILE_MAIN = 1,
H264PROFILE_EXTENDED = 2,
H264PROFILE_HIGH = 3,
H264PROFILE_HIGH10PROFILE = 4,
H264PROFILE_HIGH422PROFILE = 5,
H264PROFILE_HIGH444PREDICTIVEPROFILE = 6,
H264PROFILE_SCALABLEBASELINE = 7,
H264PROFILE_SCALABLEHIGH = 8,
H264PROFILE_STEREOHIGH = 9,
H264PROFILE_MULTIVIEWHIGH = 10,
H264PROFILE_MAX = H264PROFILE_MULTIVIEWHIGH,
VP8PROFILE_MIN = 11,
VP8PROFILE_ANY = VP8PROFILE_MIN,
VP8PROFILE_MAX = VP8PROFILE_ANY,
VP9PROFILE_MIN = 12,
VP9PROFILE_PROFILE0 = VP9PROFILE_MIN,
VP9PROFILE_PROFILE1 = 13,
VP9PROFILE_PROFILE2 = 14,
VP9PROFILE_PROFILE3 = 15,
VP9PROFILE_MAX = VP9PROFILE_PROFILE3,
HEVCPROFILE_MIN = 16,
HEVCPROFILE_MAIN = HEVCPROFILE_MIN,
HEVCPROFILE_MAIN10 = 17,
HEVCPROFILE_MAIN_STILL_PICTURE = 18,
HEVCPROFILE_MAX = HEVCPROFILE_MAIN_STILL_PICTURE,
DOLBYVISION_PROFILE0 = 19,
// Deprecated: DOLBYVISION_PROFILE4 = 20,
DOLBYVISION_PROFILE5 = 21,
DOLBYVISION_PROFILE7 = 22,
THEORAPROFILE_MIN = 23,
THEORAPROFILE_ANY = THEORAPROFILE_MIN,
THEORAPROFILE_MAX = THEORAPROFILE_ANY,
AV1PROFILE_MIN = 24,
AV1PROFILE_PROFILE_MAIN = AV1PROFILE_MIN,
AV1PROFILE_PROFILE_HIGH = 25,
AV1PROFILE_PROFILE_PRO = 26,
AV1PROFILE_MAX = AV1PROFILE_PROFILE_PRO,
DOLBYVISION_PROFILE8 = 27,
DOLBYVISION_PROFILE9 = 28,
HEVCPROFILE_EXT_MIN = 29,
HEVCPROFILE_REXT = HEVCPROFILE_EXT_MIN,
HEVCPROFILE_HIGH_THROUGHPUT = 30,
HEVCPROFILE_MULTIVIEW_MAIN = 31,
HEVCPROFILE_SCALABLE_MAIN = 32,
HEVCPROFILE_3D_MAIN = 33,
HEVCPROFILE_SCREEN_EXTENDED = 34,
HEVCPROFILE_SCALABLE_REXT = 35,
HEVCPROFILE_HIGH_THROUGHPUT_SCREEN_EXTENDED = 36,
HEVCPROFILE_EXT_MAX = HEVCPROFILE_HIGH_THROUGHPUT_SCREEN_EXTENDED,
VVCPROFILE_MIN = 37,
VVCPROFILE_MAIN10 = VVCPROFILE_MIN,
VVCPROFILE_MAIN12 = 38,
VVCPROFILE_MAIN12_INTRA = 39,
VVCPROIFLE_MULTILAYER_MAIN10 = 40,
VVCPROFILE_MAIN10_444 = 41,
VVCPROFILE_MAIN12_444 = 42,
VVCPROFILE_MAIN16_444 = 43,
VVCPROFILE_MAIN12_444_INTRA = 44,
VVCPROFILE_MAIN16_444_INTRA = 45,
VVCPROFILE_MULTILAYER_MAIN10_444 = 46,
VVCPROFILE_MAIN10_STILL_PICTURE = 47,
VVCPROFILE_MAIN12_STILL_PICTURE = 48,
VVCPROFILE_MAIN10_444_STILL_PICTURE = 49,
VVCPROFILE_MAIN12_444_STILL_PICTURE = 50,
VVCPROFILE_MAIN16_444_STILL_PICTURE = 51,
VVCPROFILE_MAX = VVCPROFILE_MAIN16_444_STILL_PICTURE,
VIDEO_CODEC_PROFILE_MAX = VVCPROFILE_MAIN16_444_STILL_PICTURE,
}
export type VideoCodecLevel = number; // uint32
export const NoVideoCodecLevel: VideoCodecLevel = 0;
export type VideoCodecProfileLevel = {
codec: VideoCodec;
profile: VideoCodecProfile;
level: VideoCodecLevel;
};

View 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);
}
}

View File

@@ -0,0 +1,65 @@
export interface RangedStream {
controller: AbortController;
response: Response;
body: ReadableStream;
totalSize?: number;
}
export interface CreateRangedStreamOptions {
url: string;
byteStart?: number;
byteEnd?: number;
}
export async function createRangedStream({
url,
byteStart = 0,
byteEnd,
}: CreateRangedStreamOptions) {
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,
};
}

View File

@@ -0,0 +1,6 @@
export {
type RangedStream,
type CreateRangedStreamOptions,
createRangedStream,
} from './fetch';
export { BitReader } from './bit';

View File

@@ -0,0 +1,25 @@
export class UnsupportedCodecError extends Error {
constructor(codec: string, context: string) {
super(`codec ${codec} is not supported in ${context} context`);
}
}
export class ParseCodecError extends Error {
constructor(codec: string, detail: string) {
super(`code ${codec} private parse failed: ${detail}`);
}
}
export class UnreachableOrLogicError extends Error {
constructor(detail: string) {
super(`unreachable or logic error: ${detail}`);
}
}
export class ParseCodecErrors extends Error {
cause: Error[] = [];
constructor() {
super('failed to parse codecs');
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist"
},
"include": [
"src"
]
}

View File

@@ -1,5 +1,5 @@
[package]
name = "konoplayer-codecs"
name = "konoplayer-ffmpeg"
version = "0.1.0"
edition = "2024"

View 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"
}
}

View File

@@ -0,0 +1,110 @@
import { ParseCodecError } from '@konoplayer/core/errors';
import { ArkErrors, type } from 'arktype';
export const AAC_CODEC_TYPE = 'AAC';
export const AudioObjectTypeSchema = type('1 | 2 | 3 | 4 | 5 | 29 | 67');
export const SamplingFrequencyIndexSchema = type('1|2|3|4|5|6|7|8|9|10|11|12');
export const ChannelConfigurationSchema = type('1 | 2 | 3 | 4 | 5 | 6 | 7');
export const AudioSpecificConfigSchema = type({
audioObjectType: AudioObjectTypeSchema, // AAC profiles: Main, LC, SSR, LTP, HE, HE v2
samplingFrequencyIndex: SamplingFrequencyIndexSchema.optional(), // Sampling rate index
channelConfiguration: ChannelConfigurationSchema, // Channel config (1-7)
sbrPresent: type.boolean.optional(), // Optional: Indicates SBR presence
psPresent: type.boolean.optional(), // Optional: Indicates PS presence (for HE-AAC v2)
});
export type AudioSpecificConfigType = typeof AudioSpecificConfigSchema.infer;
/**
* Parse AudioSpecificConfig from codec_private Uint8Array
* @param codecPrivate - Uint8Array containing codec_private data
* @returns Parsed AudioSpecificConfig or throws an error if invalid
*/
export function parseAudioSpecificConfig(
codecPrivate: Uint8Array
): AudioSpecificConfigType {
if (codecPrivate.length < 2) {
throw new ParseCodecError(AAC_CODEC_TYPE, 'codec_private data too short');
}
// Create a DataView for bit-level manipulation
const view = new DataView(
codecPrivate.buffer,
codecPrivate.byteOffset,
codecPrivate.byteLength
);
let byteOffset = 0;
let bitOffset = 0;
// Helper function to read specific number of bits
function readBits(bits: number): number {
let value = 0;
for (let i = 0; i < bits; i++) {
const byte = view.getUint8(byteOffset);
const bit = (byte >> (7 - bitOffset)) & 1;
value = (value << 1) | bit;
bitOffset++;
if (bitOffset === 8) {
bitOffset = 0;
byteOffset++;
}
}
return value;
}
// Read 5 bits for audioObjectType
const audioObjectType = readBits(5);
// Read 4 bits for samplingFrequencyIndex
const samplingFrequencyIndex = readBits(4);
// Read 4 bits for channelConfiguration
const channelConfiguration = readBits(4);
// Check for SBR/PS extension (if audioObjectType indicates HE-AAC)
let sbrPresent = false;
let psPresent = false;
if (audioObjectType === 5 || audioObjectType === 29) {
sbrPresent = true;
if (audioObjectType === 29) {
psPresent = true; // HE-AAC v2 includes Parametric Stereo
}
// Skip extension-specific bits if present (simplified here)
// In real cases, additional parsing may be needed
}
// Construct the result object
const config: AudioSpecificConfigType = {
audioObjectType:
audioObjectType as AudioSpecificConfigType['audioObjectType'],
samplingFrequencyIndex:
samplingFrequencyIndex as AudioSpecificConfigType['samplingFrequencyIndex'],
channelConfiguration:
channelConfiguration as AudioSpecificConfigType['channelConfiguration'],
...(sbrPresent && { sbrPresent }),
...(psPresent && { psPresent }),
};
// Validate with arktype
const validation = AudioSpecificConfigSchema(config);
if (validation instanceof ArkErrors) {
const error = new ParseCodecError(
AAC_CODEC_TYPE,
'Invalid AudioSpecificConfig'
);
error.cause = validation;
throw error;
}
return config;
}
export function genCodecIdByAudioSpecificConfig(
config: AudioSpecificConfigType
) {
return `mp4a.40.${config.audioObjectType}`;
}

View 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 Delayoptional
if (
config.initialPresentationDelayPresent === 1 &&
config.initialPresentationDelayMinus1 !== undefined
) {
const delay = (config.initialPresentationDelayMinus1 + 1)
.toString()
.padStart(2, '0');
parts.push(delay);
}
// joined
return parts.join('.');
}

View File

@@ -0,0 +1,148 @@
import { ParseCodecError } from '@konoplayer/core/errors';
import { type } from 'arktype';
import type { TrackEntryType } from '../schema';
export const AVC_CODEC_TYPE = 'h264(AVC)';
export const AVCDecoderConfigurationRecordSchema = type({
configurationVersion: type.number, // Configuration version, typically 1
avcProfileIndication: type.number, // AVC profile
profileCompatibility: type.number, // Profile compatibility
avcLevelIndication: type.number, // AVC level
lengthSizeMinusOne: type.number, // NAL unit length field size minus 1
sps: type
.instanceOf(Uint8Array<ArrayBufferLike>)
.array()
.atLeastLength(1), // Sequence Parameter Sets (SPS)
pps: type
.instanceOf(Uint8Array<ArrayBufferLike>)
.array()
.atLeastLength(1), // Picture Parameter Sets (PPS)
});
export type AVCDecoderConfigurationRecordType =
typeof AVCDecoderConfigurationRecordSchema.infer;
/**
*
* @see [webkit](https://github.com/movableink/webkit/blob/7e43fe7000b319ce68334c09eed1031642099726/Source/WebCore/platform/graphics/HEVCUtilities.cpp#L84)
*/
export function parseAVCDecoderConfigurationRecord(
track: TrackEntryType
): AVCDecoderConfigurationRecordType {
// ISO/IEC 14496-10:2014
// 7.3.2.1.1 Sequence parameter set data syntax
const codecPrivate = track.CodecPrivate;
if (!codecPrivate) {
throw new ParseCodecError(
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,
'Input data too short for AVCDecoderConfigurationRecord'
);
}
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 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)
const lengthSizeMinusOne = readUint8(true) & 0x03;
// Read number of SPS (first 3 bits are reserved, typically 0xE0, last 5 bits are SPS count)
const numOfSPS = readUint8(true) & 0x1f;
const sps: Uint8Array[] = [];
// Parse SPS
for (let i = 0; i < numOfSPS; i++) {
if (offset + 2 > codecPrivate.length) {
throw new ParseCodecError(AVC_CODEC_TYPE, 'Invalid SPS length');
}
const spsLength = readUint16(true);
if (offset + spsLength > codecPrivate.length) {
throw new ParseCodecError(
AVC_CODEC_TYPE,
'SPS data exceeds buffer length'
);
}
sps.push(codecPrivate.subarray(offset, offset + spsLength));
offset += spsLength;
}
// Read number of PPS
if (offset >= codecPrivate.length) {
throw new ParseCodecError(AVC_CODEC_TYPE, 'No space for PPS count');
}
const numOfPPS = readUint8(true);
const pps: Uint8Array[] = [];
// Parse PPS
for (let i = 0; i < numOfPPS; i++) {
if (offset + 2 > codecPrivate.length) {
throw new ParseCodecError(AVC_CODEC_TYPE, 'Invalid PPS length');
}
const ppsLength = readUint16(true);
if (offset + ppsLength > codecPrivate.length) {
throw new ParseCodecError(
AVC_CODEC_TYPE,
'PPS data exceeds buffer length'
);
}
pps.push(codecPrivate.subarray(offset, offset + ppsLength));
offset += ppsLength;
}
return {
configurationVersion,
avcProfileIndication,
profileCompatibility,
avcLevelIndication,
lengthSizeMinusOne,
sps,
pps,
};
}
export function genCodecStringByAVCDecoderConfigurationRecord(
config: AVCDecoderConfigurationRecordType
): string {
const profileHex = config.avcProfileIndication.toString(16).padStart(2, '0');
const profileCompatHex = config.profileCompatibility
.toString(16)
.padStart(2, '0');
const levelHex = config.avcLevelIndication.toString(16).padStart(2, '0');
return `avc1.${profileHex}${profileCompatHex}${levelHex}`;
}

View 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('.');
}

View File

@@ -0,0 +1,347 @@
import { UnsupportedCodecError } from '@konoplayer/core/errors';
import { VideoCodec, AudioCodec } from '@konoplayer/core/codecs';
import type { TrackEntryType } from '../schema';
import {
genCodecIdByAudioSpecificConfig,
parseAudioSpecificConfig,
} from './aac';
import {
genCodecStringByAVCDecoderConfigurationRecord,
parseAVCDecoderConfigurationRecord,
} from './avc';
import {
genCodecStringByAV1DecoderConfigurationRecord,
parseAV1DecoderConfigurationRecord,
} from './av1.ts';
import {
genCodecStringByHEVCDecoderConfigurationRecord,
parseHEVCDecoderConfigurationRecord,
} from './hevc.ts';
import {
genCodecStringByVP9DecoderConfigurationRecord,
parseVP9DecoderConfigurationRecord,
} from './vp9.ts';
export const VideoCodecId = {
VCM: 'V_MS/VFW/FOURCC',
UNCOMPRESSED: 'V_UNCOMPRESSED',
MPEG4_ISO_SP: 'V_MPEG4/ISO/SP',
MPEG4_ISO_ASP: 'V_MPEG4/ISO/ASP',
MPEG4_ISO_AP: 'V_MPEG4/ISO/AP',
MPEG4_MS_V3: 'V_MPEG4/MS/V3',
MPEG1: 'V_MPEG1',
MPEG2: 'V_MPEG2',
H264: 'V_MPEG4/ISO/AVC',
HEVC: 'V_MPEGH/ISO/HEVC',
AVS2: 'V_AVS2',
AVS3: 'V_AVS3',
RV10: 'V_REAL/RV10',
RV20: 'V_REAL/RV20',
RV30: 'V_REAL/RV30',
RV40: 'V_REAL/RV40',
QUICKTIME: 'V_QUICKTIME',
THEORA: 'V_THEORA',
PROPRES: 'V_PRORES',
VP8: 'V_VP8',
VP9: 'V_VP9',
FFV1: 'V_FFV1',
AV1: 'V_AV1',
} as const;
export type VideoCodecIdType =
| `${(typeof VideoCodecId)[keyof typeof VideoCodecId]}`
| string;
export const AudioCodecId = {
MPEG_L3: 'A_MPEG/L3',
MPEG_L2: 'A_MPEG/L2',
MPEG_L1: 'A_MPEG/L1',
PCM_INT_BIG: 'A_PCM/INT/BIG',
PCM_INT_LIT: 'A_PCM/INT/LIT',
PCM_FLOAT_IEEE: 'A_PCM/FLOAT/IEEE',
MPC: 'A_MPC',
AC3: 'A_AC3',
AC3_BSID9: 'A_AC3/BSID9',
AC3_BSID10: 'A_AC3/BSID10',
ALAC: 'A_ALAC',
DTS: 'A_DTS',
DTS_EXPRESS: 'A_DTS/EXPRESS',
DTS_LOSSLESS: 'A_DTS/LOSSLESS',
VORBIS: 'A_VORBIS',
OPUS: 'A_OPUS',
FLAC: 'A_FLAC',
EAC3: 'A_EAC3',
REAL_14_4: 'A_REAL/14_4',
REAL_28_8: 'A_REAL/28_8',
REAL_COOK: 'A_REAL/COOK',
REAL_SIPR: 'A_REAL/SIPR',
REAL_RALF: 'A_REAL/RALF',
REAL_ATRC: 'A_REAL/ATRC',
MS_ACM: 'A_MS/ACM',
AAC: 'A_AAC',
AAC_MPEG2_MAIN: 'A_AAC/MPEG2/MAIN',
AAC_MPEG2_LC: 'A_AAC/MPEG2/LC',
AAC_MPEG2_LC_SBR: 'A_AAC/MPEG2/LC/SBR',
AAC_MPEG2_SSR: 'A_AAC/MPEG2/SSR',
AAC_MPEG4_MAIN: 'A_AAC/MPEG4/MAIN',
AAC_MPEG4_LC: 'A_AAC/MPEG4/LC',
AAC_MPEG4_SBR: 'A_AAC/MPEG4/LC/SBR',
AAC_MPEG4_SSR: 'A_AAC/MPEG4/SSR',
AAC_MPEG4_LTP: 'A_AAC/MPEG4/LTP',
QUICKTIME: 'A_QUICKTIME',
QDMC: 'A_QUICKTIME/QDMC',
QDM2: 'A_QUICKTIME/QDM2',
TTA1: 'A_TTA1',
WAVEPACK4: 'A_WAVPACK4',
ATRAC: 'A_ATRAC/AT1',
} as const;
export type AudioCodecIdType =
| `${(typeof AudioCodecId)[keyof typeof AudioCodecId]}`
| string;
export const SubtitleCodecId = {
UTF8: 'S_TEXT/UTF8',
SSA: 'S_TEXT/SSA',
ASS: 'S_TEXT/ASS',
WEBVTT: 'S_TEXT/WEBVTT',
BMP: 'S_IMAGE/BMP',
DVBSUB: 'S_DVBSUB',
VOBSUB: 'S_VOBSUB',
HDMV_PGS: 'S_HDMV/PGS',
HDMV_TEXTST: 'S_HDMV/TEXTST',
KATE: 'S_KATE',
ARIBSUB: 'S_ARIBSUB',
} as const;
export type SubtitleCodecIdType =
| `${(typeof SubtitleCodecId)[keyof typeof SubtitleCodecId]}`
| string;
export interface VideoDecoderConfigExt extends VideoDecoderConfig {
codecType: VideoCodec;
}
export function videoCodecIdToWebCodecs(
track: TrackEntryType,
keyframe: Uint8Array
): VideoDecoderConfigExt {
const codecId = track.CodecID;
const codecPrivate = track.CodecPrivate;
const shareOptions = {
description: codecPrivate,
};
switch (codecId) {
case VideoCodecId.HEVC:
return {
...shareOptions,
codecType: VideoCodec.HEVC,
codec: genCodecStringByHEVCDecoderConfigurationRecord(
parseHEVCDecoderConfigurationRecord(track)
),
};
case VideoCodecId.VP9:
return {
...shareOptions,
codecType: VideoCodec.VP9,
codec: genCodecStringByVP9DecoderConfigurationRecord(
parseVP9DecoderConfigurationRecord(track, keyframe)
),
};
case VideoCodecId.AV1:
return {
...shareOptions,
codecType: VideoCodec.AV1,
codec: genCodecStringByAV1DecoderConfigurationRecord(
parseAV1DecoderConfigurationRecord(track)
),
};
case VideoCodecId.H264:
return {
...shareOptions,
codecType: VideoCodec.H264,
codec: genCodecStringByAVCDecoderConfigurationRecord(
parseAVCDecoderConfigurationRecord(track)
),
};
case VideoCodecId.THEORA:
return { ...shareOptions, codecType: VideoCodec.Theora, codec: 'theora' };
case VideoCodecId.VP8:
return { ...shareOptions, codecType: VideoCodec.VP8, codec: 'vp8' };
case VideoCodecId.MPEG4_ISO_SP:
return {
...shareOptions,
codecType: VideoCodec.MPEG4,
codec: 'mp4v.01.3',
};
case VideoCodecId.MPEG4_ISO_ASP:
return {
...shareOptions,
codecType: VideoCodec.MPEG4,
codec: 'mp4v.20.9',
};
case VideoCodecId.MPEG4_ISO_AP:
return {
...shareOptions,
codecType: VideoCodec.MPEG4,
codec: 'mp4v.20.9',
};
default:
throw new UnsupportedCodecError(codecId, 'web codecs video decoder');
}
}
export interface AudioDecoderConfigExt extends AudioDecoderConfig {
codecType: AudioCodec;
}
export function audioCodecIdToWebCodecs(
track: TrackEntryType
): AudioDecoderConfigExt {
const codecId = track.CodecID;
const codecPrivate = track.CodecPrivate;
const bitDepth = track.Audio?.BitDepth;
const numberOfChannels = Number(track.Audio?.Channels);
const sampleRate = Number(track.Audio?.SamplingFrequency);
const shareOptions = {
numberOfChannels,
sampleRate,
description: codecPrivate,
};
switch (track.CodecID) {
case AudioCodecId.AAC_MPEG4_MAIN:
case AudioCodecId.AAC_MPEG2_MAIN:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.1',
};
case AudioCodecId.AAC_MPEG2_LC:
case AudioCodecId.AAC_MPEG4_LC:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.2',
};
case AudioCodecId.AAC_MPEG2_SSR:
case AudioCodecId.AAC_MPEG4_SSR:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.3',
};
case AudioCodecId.AAC_MPEG4_LTP:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.4',
};
case AudioCodecId.AAC_MPEG2_LC_SBR:
case AudioCodecId.AAC_MPEG4_SBR:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.5',
};
case AudioCodecId.AAC:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: codecPrivate
? genCodecIdByAudioSpecificConfig(
parseAudioSpecificConfig(codecPrivate)
)
: 'mp4a.40.2',
};
case AudioCodecId.AC3:
case AudioCodecId.AC3_BSID9:
return {
...shareOptions,
codecType: AudioCodec.AC3,
codec: 'ac-3',
};
case AudioCodecId.EAC3:
case AudioCodecId.AC3_BSID10:
return {
...shareOptions,
codecType: AudioCodec.EAC3,
codec: 'ec-3',
};
case AudioCodecId.MPEG_L3:
return {
...shareOptions,
codecType: AudioCodec.MP3,
codec: 'mp3',
};
case AudioCodecId.VORBIS:
return { ...shareOptions, codecType: AudioCodec.Vorbis, codec: 'vorbis' };
case AudioCodecId.FLAC:
return { ...shareOptions, codecType: AudioCodec.FLAC, codec: 'flac' };
case AudioCodecId.OPUS:
return { ...shareOptions, codecType: AudioCodec.Opus, codec: 'opus' };
case AudioCodecId.ALAC:
return { ...shareOptions, codecType: AudioCodec.ALAC, codec: 'alac' };
case AudioCodecId.PCM_INT_BIG:
if (bitDepth === 16) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S16BE,
codec: 'pcm-s16be',
};
}
if (bitDepth === 24) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S24BE,
codec: 'pcm-s24be',
};
}
if (bitDepth === 32) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S32BE,
codec: 'pcm-s32be',
};
}
throw new UnsupportedCodecError(
`${codecId}(${bitDepth}b)`,
'web codecs audio decoder'
);
case AudioCodecId.PCM_INT_LIT:
if (bitDepth === 16) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S16LE,
codec: 'pcm-s16le',
};
}
if (bitDepth === 24) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S24LE,
codec: 'pcm-s24le',
};
}
if (bitDepth === 32) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S32LE,
codec: 'pcm-s32le',
};
}
throw new UnsupportedCodecError(
`${codecId}(${bitDepth}b)`,
'web codecs audio decoder'
);
case AudioCodecId.PCM_FLOAT_IEEE:
return {
...shareOptions,
codecType: AudioCodec.PCM_F32LE,
codec: 'pcm-f32le',
};
default:
throw new UnsupportedCodecError(codecId, 'web codecs audio decoder');
}
}

View 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}`;
}

View File

@@ -0,0 +1,469 @@
import {
type EbmlClusterTagType,
type EbmlCuePointTagType,
type EbmlCuesTagType,
type EbmlInfoTagType,
type EbmlMasterTagType,
type EbmlSeekHeadTagType,
type EbmlSegmentTagType,
EbmlTagIdEnum,
EbmlTagPosition,
type EbmlTagsTagType,
type EbmlTagTagType,
type EbmlTagType,
type EbmlTrackEntryTagType,
type EbmlTracksTagType,
} from 'konoebml';
import { convertEbmlTagToComponent, type InferType } from './util';
import { isEqual, maxBy } from 'lodash-es';
import { ArkErrors, type Type } from 'arktype';
import {
ClusterSchema,
type ClusterType,
CuePointSchema,
type CuePointType,
type CueTrackPositionsType,
InfoSchema,
type InfoType,
SeekHeadSchema,
type SeekHeadType,
TagSchema,
type TagType,
TrackEntrySchema,
type TrackEntryType,
TrackTypeRestrictionEnum,
} from './schema';
import { concatBufs } from 'konoebml/lib/tools';
import {
ParseCodecErrors,
UnreachableOrLogicError,
UnsupportedCodecError,
} 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_TRACKS = new Uint8Array([0x16, 0x54, 0xae, 0x6b]);
export const SEEK_ID_KAX_CUES = new Uint8Array([0x1c, 0x53, 0xbb, 0x6b]);
export const SEEK_ID_KAX_TAGS = new Uint8Array([0x12, 0x54, 0xc3, 0x67]);
export class SegmentSystem {
startTag: EbmlSegmentTagType;
headTags: EbmlTagType[] = [];
firstCluster: EbmlClusterTagType | undefined;
cue: CueSystem;
cluster: ClusterSystem;
seek: SeekSystem;
info: InfoSystem;
track: TrackSystem;
tag: TagSystem;
constructor(startNode: EbmlSegmentTagType) {
this.startTag = startNode;
this.cue = new CueSystem(this);
this.cluster = new ClusterSystem(this);
this.seek = new SeekSystem(this);
this.info = new InfoSystem(this);
this.track = new TrackSystem(this);
this.tag = new TagSystem(this);
}
get contentStartOffset() {
return this.startTag.startOffset + this.startTag.headerLength;
}
private seekLocal() {
const infoTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_INFO);
const tracksTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_TRACKS);
const cuesTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_CUES);
const tagsTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_TAGS);
if (cuesTag?.id === EbmlTagIdEnum.Cues) {
this.cue.prepareCuesWithTag(cuesTag);
}
if (infoTag?.id === EbmlTagIdEnum.Info) {
this.info.prepareWithInfoTag(infoTag);
}
if (tracksTag?.id === EbmlTagIdEnum.Tracks) {
this.track.prepareTracksWithTag(tracksTag);
}
if (tagsTag?.id === EbmlTagIdEnum.Tags) {
this.tag.prepareTagsWithTag(tagsTag);
}
}
scanMeta(tag: EbmlTagType) {
if (
tag.id === EbmlTagIdEnum.SeekHead &&
tag.position === EbmlTagPosition.End
) {
this.seek.addSeekHeadTag(tag);
}
this.headTags.push(tag);
this.seek.memoTag(tag);
if (tag.id === EbmlTagIdEnum.Cluster && !this.firstCluster) {
this.firstCluster = tag;
this.seekLocal();
}
return this;
}
async completeMeta() {
this.seekLocal();
await this.parseCodes();
return this;
}
async parseCodes() {
const candidates = this.track.tracks.filter(
(c) =>
c.TrackType === TrackTypeRestrictionEnum.AUDIO ||
c.TrackType === TrackTypeRestrictionEnum.VIDEO
);
const parseErrors = new ParseCodecErrors();
for (const t of candidates) {
try {
await this.track.initTrack(t, this.);
} catch (e) {
parseErrors.cause.push(e as Error);
}
}
if (parseErrors.cause.length > 0) {
console.error(parseErrors);
}
}
}
export type SegmentComponent<T> = T & {
get segment(): SegmentSystem;
};
export function withSegment<T extends object>(
component: T,
segment: SegmentSystem
): SegmentComponent<T> {
const component_ = component as T & { segment: SegmentSystem };
component_.segment = segment;
return component_;
}
export class SegmentComponentSystemTrait<
E extends EbmlMasterTagType,
S extends Type<any>,
> {
segment: SegmentSystem;
get schema(): S {
throw new Error('unimplemented!');
}
constructor(segment: SegmentSystem) {
this.segment = segment;
}
componentFromTag(tag: E): SegmentComponent<InferType<S>> {
const extracted = convertEbmlTagToComponent(tag);
const result = this.schema(extracted) as
| (InferType<S> & { segment: SegmentSystem })
| ArkErrors;
if (result instanceof ArkErrors) {
const errors = result;
console.error(
'Parse component from tag error:',
tag.toDebugRecord(),
errors.flatProblemsByPath
);
throw errors;
}
result.segment = this.segment;
return result;
}
}
export class SeekSystem extends SegmentComponentSystemTrait<
EbmlSeekHeadTagType,
typeof SeekHeadSchema
> {
override get schema() {
return SeekHeadSchema;
}
seekHeads: SeekHeadType[] = [];
private offsetToTagMemo: Map<number, EbmlTagType> = new Map();
memoTag(tag: EbmlTagType) {
this.offsetToTagMemo.set(tag.startOffset, tag);
}
addSeekHeadTag(tag: EbmlSeekHeadTagType) {
const seekHead = this.componentFromTag(tag);
this.seekHeads.push(seekHead);
return seekHead;
}
offsetFromSeekPosition(position: number): number {
return position + this.segment.contentStartOffset;
}
seekTagByStartOffset(
startOffset: number | undefined
): EbmlTagType | undefined {
return startOffset! >= 0
? this.offsetToTagMemo.get(startOffset!)
: undefined;
}
seekOffsetBySeekId(seekId: Uint8Array): number | undefined {
const seekPosition = this.seekHeads[0]?.Seek?.find((c) =>
isEqual(c.SeekID, seekId)
)?.SeekPosition;
return seekPosition! >= 0
? this.offsetFromSeekPosition(seekPosition! as number)
: undefined;
}
seekTagBySeekId(seekId: Uint8Array): EbmlTagType | undefined {
return this.seekTagByStartOffset(this.seekOffsetBySeekId(seekId));
}
get firstClusterOffset() {
if (!this.segment.firstCluster) {
throw new UnreachableOrLogicError('first cluster not found');
}
return this.segment.firstCluster.startOffset;
}
}
export class InfoSystem extends SegmentComponentSystemTrait<
EbmlInfoTagType,
typeof InfoSchema
> {
override get schema() {
return InfoSchema;
}
info!: SegmentComponent<InfoType>;
prepareWithInfoTag(tag: EbmlInfoTagType) {
this.info = this.componentFromTag(tag);
return this;
}
}
export class ClusterSystem extends SegmentComponentSystemTrait<
EbmlClusterTagType,
typeof ClusterSchema
> {
override get schema() {
return ClusterSchema;
}
clustersBuffer: SegmentComponent<ClusterType>[] = [];
addClusterWithTag(tag: EbmlClusterTagType) {
const cluster = this.componentFromTag(tag);
this.clustersBuffer.push(cluster);
return cluster;
}
}
export interface GetTrackEntryOptions {
priority?: (v: SegmentComponent<TrackEntryType>) => number;
predicate?: (v: SegmentComponent<TrackEntryType>) => boolean;
}
export interface TrackState<Decoder, Config, Frame> {
decoder: Decoder;
configuration?: Config;
frameBuffer$: BehaviorSubject<Queue<Frame>>;
}
export class TrackSystem extends SegmentComponentSystemTrait<
EbmlTrackEntryTagType,
typeof TrackEntrySchema
> {
override get schema() {
return TrackEntrySchema;
}
tracks: SegmentComponent<TrackEntryType>[] = [];
videoTrackState = new WeakMap<
TrackEntryType,
TrackState<VideoDecoder, VideoDecoderConfig, VideoFrame>
>();
audioTrackState = new WeakMap<
TrackEntryType,
TrackState<AudioDecoder, AudioDecoderConfig, AudioData>
>();
getTrackEntry({
priority = (track) =>
(Number(!!track.FlagForced) << 4) + Number(!!track.FlagDefault),
predicate = (track) => track.FlagEnabled !== 0,
}: GetTrackEntryOptions) {
return this.tracks
.filter(predicate)
.toSorted((a, b) => priority(b) - priority(a))
.at(0);
}
prepareTracksWithTag(tag: EbmlTracksTagType) {
this.tracks = tag.children
.filter((c) => c.id === EbmlTagIdEnum.TrackEntry)
.map((c) => this.componentFromTag(c));
return this;
}
async initTrack(track: TrackEntryType) {
if (track.TrackType === TrackTypeRestrictionEnum.AUDIO) {
const configuration = audioCodecIdToWebCodecs(track);
if (await AudioDecoder.isConfigSupported(configuration)) {
throw new UnsupportedCodecError(configuration.codec, 'audio decoder');
}
const queue$ = new BehaviorSubject(new Queue<AudioData>());
this.audioTrackState.set(track, {
configuration,
decoder: new AudioDecoder({
output: (audioData) => {
const queue = queue$.getValue();
queue.enqueue(audioData);
queue$.next(queue);
},
error: (e) => {
queue$.error(e);
},
}),
frameBuffer$: queue$,
});
} else if (track.TrackType === TrackTypeRestrictionEnum.VIDEO) {
const configuration = videoCodecIdToWebCodecs(track, this.keyframe);
if (await VideoDecoder.isConfigSupported(configuration)) {
throw new UnsupportedCodecError(configuration.codec, 'audio decoder');
}
const queue$ = new BehaviorSubject(new Queue<VideoFrame>());
this.videoTrackState.set(track, {
configuration,
decoder: new VideoDecoder({
output: (audioData) => {
const queue = queue$.getValue();
queue.enqueue(audioData);
queue$.next(queue);
},
error: (e) => {
queue$.error(e);
},
}),
frameBuffer$: queue$,
});
}
}
}
export class CueSystem extends SegmentComponentSystemTrait<
EbmlCuePointTagType,
typeof CuePointSchema
> {
override get schema() {
return CuePointSchema;
}
cues: SegmentComponent<CuePointType>[] = [];
prepareCuesWithTag(tag: EbmlCuesTagType) {
this.cues = tag.children
.filter((c) => c.id === EbmlTagIdEnum.CuePoint)
.map(this.componentFromTag.bind(this));
return this;
}
findClosestCue(seekTime: number): CuePointType | undefined {
const cues = this.cues;
if (!cues || cues.length === 0) {
return undefined;
}
let left = 0;
let right = cues.length - 1;
if (seekTime <= cues[0].CueTime) {
return cues[0];
}
if (seekTime >= cues[right].CueTime) {
return cues[right];
}
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (cues[mid].CueTime === seekTime) {
return cues[mid];
}
if (cues[mid].CueTime < seekTime) {
left = mid + 1;
} else {
right = mid - 1;
}
}
const before = cues[right];
const after = cues[left];
return Math.abs((before.CueTime as number) - seekTime) <
Math.abs((after.CueTime as number) - seekTime)
? before
: after;
}
getCueTrackPositions(
cuePoint: CuePointType,
track?: number
): CueTrackPositionsType {
let cueTrackPositions: CueTrackPositionsType | undefined;
if (track! >= 0) {
cueTrackPositions = cuePoint.CueTrackPositions.find(
(c) => c.CueTrack === track
);
}
if (!cueTrackPositions) {
cueTrackPositions = maxBy(
cuePoint.CueTrackPositions,
(c) => c.CueClusterPosition
)!;
}
return cueTrackPositions;
}
get prepared(): boolean {
return this.cues.length > 0;
}
}
export class TagSystem extends SegmentComponentSystemTrait<
EbmlTagTagType,
typeof TagSchema
> {
override get schema() {
return TagSchema;
}
tags: SegmentComponent<TagType>[] = [];
prepareTagsWithTag(tag: EbmlTagsTagType) {
this.tags = tag.children
.filter((c) => c.id === EbmlTagIdEnum.Tag)
.map((c) => this.componentFromTag(c));
return this;
}
get prepared(): boolean {
return this.tags.length > 0;
}
}

View File

@@ -0,0 +1,430 @@
import {
EbmlStreamDecoder,
EbmlTagIdEnum,
EbmlTagPosition,
type EbmlTagType,
} from 'konoebml';
import {
defer,
EMPTY,
filter,
finalize,
from,
isEmpty,
map,
merge,
Observable,
of,
reduce,
scan,
share,
shareReplay,
switchMap,
take,
takeWhile,
withLatestFrom,
} from 'rxjs';
import {
createRangedStream,
type CreateRangedStreamOptions,
} 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
extends CreateRangedStreamOptions {
tee?: boolean;
}
export function createRangedEbmlStream({
url,
byteStart = 0,
byteEnd,
tee = false,
}: CreateRangedEbmlStreamOptions): Observable<{
ebml$: Observable<EbmlTagType>;
totalSize?: number;
response: Response;
body: ReadableStream<Uint8Array>;
controller: AbortController;
teeBody: ReadableStream<Uint8Array> | undefined;
}> {
const stream$ = from(createRangedStream({ url, byteStart, byteEnd }));
return stream$.pipe(
switchMap(({ controller, body, totalSize, response }) => {
let requestCompleted = false;
let teeStream: ReadableStream<Uint8Array> | undefined;
let stream: ReadableStream<Uint8Array>;
if (tee) {
[stream, teeStream] = body.tee();
} else {
stream = body;
}
const originRequest$ = new Observable<EbmlTagType>((subscriber) => {
stream
.pipeThrough(
new EbmlStreamDecoder({
streamStartOffset: byteStart,
collectChild: (child) => child.id !== EbmlTagIdEnum.Cluster,
backpressure: {
eventLoop: waitTick,
},
})
)
.pipeTo(
new WritableStream({
write: async (tag) => {
await waitTick();
subscriber.next(tag);
},
close: () => {
if (!requestCompleted) {
requestCompleted = true;
subscriber.complete();
}
},
})
)
.catch((error) => {
if (requestCompleted && error?.name === 'AbortError') {
return;
}
requestCompleted = true;
subscriber.error(error);
});
return () => {
requestCompleted = true;
controller.abort();
};
}).pipe(
share({
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: true,
})
);
const ebml$ = defer(() =>
requestCompleted ? EMPTY : originRequest$
).pipe(
share({
resetOnError: false,
resetOnComplete: true,
resetOnRefCountZero: true,
})
);
return of({
ebml$,
totalSize,
response,
body: stream,
teeBody: teeStream,
controller,
});
})
);
}
export interface CreateEbmlControllerOptions
extends Omit<CreateRangedEbmlStreamOptions, 'byteStart' | 'byteEnd'> {}
export function createEbmlController({
url,
...options
}: CreateEbmlControllerOptions) {
const metaRequest$ = createRangedEbmlStream({
...options,
url,
byteStart: 0,
tee: true,
});
const controller$ = metaRequest$.pipe(
map(({ totalSize, ebml$, response, controller, teeBody }) => {
const head$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
take(1),
shareReplay(1)
);
console.debug(
`stream of video "${url}" created, total size is ${totalSize ?? 'unknown'}`
);
const segmentStart$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.Start))
);
/**
* while [matroska v4](https://www.matroska.org/technical/elements.html) doc tell that there is only one segment in a file
* some mkv generated by strange tools will emit several
*/
const segments$ = segmentStart$.pipe(
map((startTag) => {
const segment = new SegmentSystem(startTag, teeBody!);
const clusterSystem = segment.cluster;
const seekSystem = segment.seek;
const meta$ = ebml$.pipe(
scan(
(acc, tag) => {
// avoid object recreation
acc.hasKeyframe =
acc.hasKeyframe ||
(tag.id === EbmlTagIdEnum.SimpleBlock && tag.keyframe) ||
(tag.id === EbmlTagIdEnum.BlockGroup &&
tag.children.every(
(c) => c.id !== EbmlTagIdEnum.ReferenceBlock
));
acc.tag = tag;
return acc;
},
{ 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),
share({
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: true,
})
);
const withMeta$ = meta$.pipe(
reduce((segment, meta) => segment.scanMeta(meta), segment),
switchMap(() => segment.completeMeta()),
take(1),
shareReplay(1)
);
const withRemoteCues$ = withMeta$.pipe(
switchMap((s) => {
const cueSystem = s.cue;
const seekSystem = s.seek;
if (cueSystem.prepared) {
return EMPTY;
}
const remoteCuesTagStartOffset =
seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_CUES);
if (remoteCuesTagStartOffset! >= 0) {
return createRangedEbmlStream({
...options,
url,
byteStart: remoteCuesTagStartOffset,
}).pipe(
switchMap((req) => req.ebml$),
filter(isTagIdPos(EbmlTagIdEnum.Cues, EbmlTagPosition.End)),
withLatestFrom(withMeta$),
map(([cues, withMeta]) => {
withMeta.cue.prepareCuesWithTag(cues);
return withMeta;
})
);
}
return EMPTY;
}),
take(1),
shareReplay(1)
);
const withLocalCues$ = withMeta$.pipe(
switchMap((s) => (s.cue.prepared ? of(s) : EMPTY)),
shareReplay(1)
);
const withRemoteTags$ = withMeta$.pipe(
switchMap((s) => {
const tagSystem = s.tag;
const seekSystem = s.seek;
if (tagSystem.prepared) {
return EMPTY;
}
const remoteTagsTagStartOffset =
seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_TAGS);
if (remoteTagsTagStartOffset! >= 0) {
return createRangedEbmlStream({
...options,
url,
byteStart: remoteTagsTagStartOffset,
}).pipe(
switchMap((req) => req.ebml$),
filter(isTagIdPos(EbmlTagIdEnum.Tags, EbmlTagPosition.End)),
withLatestFrom(withMeta$),
map(([tags, withMeta]) => {
withMeta.tag.prepareTagsWithTag(tags);
return withMeta;
})
);
}
return EMPTY;
}),
take(1),
shareReplay(1)
);
const withLocalTags$ = withMeta$.pipe(
switchMap((s) => (s.tag.prepared ? of(s) : EMPTY)),
shareReplay(1)
);
const withCues$ = merge(withLocalCues$, withRemoteCues$).pipe(
take(1)
);
const withoutCues$ = withCues$.pipe(
isEmpty(),
switchMap((empty) => (empty ? withMeta$ : EMPTY))
);
const withTags$ = merge(withLocalTags$, withRemoteTags$).pipe(
take(1)
);
const withoutTags$ = withTags$.pipe(
isEmpty(),
switchMap((empty) => (empty ? withMeta$ : EMPTY))
);
const seekWithoutCues = (
seekTime: number
): Observable<SegmentComponent<ClusterType>> => {
const request$ = withMeta$.pipe(
switchMap(() =>
createRangedEbmlStream({
...options,
url,
byteStart: seekSystem.firstClusterOffset,
})
)
);
const cluster$ = request$.pipe(
switchMap((req) => req.ebml$),
filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)),
map((tag) => clusterSystem.addClusterWithTag(tag))
);
if (seekTime === 0) {
return cluster$;
}
return cluster$.pipe(
scan(
(acc, curr) => {
// avoid object recreation
acc.prev = acc.next;
acc.next = curr;
return acc;
},
{
prev: undefined as SegmentComponent<ClusterType> | undefined,
next: undefined as SegmentComponent<ClusterType> | undefined,
}
),
filter((c) => c.next?.Timestamp! > seekTime),
map((c) => c.prev ?? c.next!)
);
};
const seekWithCues = (
cueSystem: CueSystem,
seekTime: number
): Observable<SegmentComponent<ClusterType>> => {
if (seekTime === 0) {
return seekWithoutCues(seekTime);
}
const cuePoint = cueSystem.findClosestCue(seekTime);
if (!cuePoint) {
return seekWithoutCues(seekTime);
}
return createRangedEbmlStream({
...options,
url,
byteStart: seekSystem.offsetFromSeekPosition(
cueSystem.getCueTrackPositions(cuePoint)
.CueClusterPosition as number
),
}).pipe(
switchMap((req) => req.ebml$),
filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)),
map(clusterSystem.addClusterWithTag.bind(clusterSystem))
);
};
const seek = (
seekTime: number
): Observable<SegmentComponent<ClusterType>> => {
if (seekTime === 0) {
const subscription = merge(withCues$, withoutCues$).subscribe();
// if seekTime equals to 0 at start, reuse the initialize stream
return seekWithoutCues(seekTime).pipe(
finalize(() => {
subscription.unsubscribe();
})
);
}
return merge(
withCues$.pipe(switchMap((s) => seekWithCues(s.cue, seekTime))),
withoutCues$.pipe(switchMap((_) => seekWithoutCues(seekTime)))
);
};
return {
startTag,
head$,
segment,
meta$,
withMeta$,
withCues$,
withoutCues$,
withTags$,
withoutTags$,
seekWithCues,
seekWithoutCues,
seek,
};
})
);
return {
segments$,
head$,
totalSize,
ebml$,
controller,
response,
};
}),
shareReplay(1)
);
return {
controller$,
request$: metaRequest$,
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
import type { Type } from 'arktype';
import { EbmlElementType, EbmlTagIdEnum, type EbmlTagType } from 'konoebml';
import { IdMultiSet } from './schema';
export type InferType<T extends Type<any>> = T['infer'];
export type PredicateIdExtract<T, K> = Extract<T, { id: K }>;
export type PredicatePositionExtract<
T extends { position: string },
P,
> = P extends T['position'] ? T : never;
export function isTagIdPos<
I extends EbmlTagIdEnum,
P extends PredicateIdExtract<EbmlTagType, I>['position'] | '*' = '*',
>(id: I, pos?: P) {
return (tag: EbmlTagType): tag is PredicateIdExtract<EbmlTagType, I> =>
tag.id === id && (pos === '*' || pos === tag.position);
}
export function isTagPos<
T extends { position: string },
P extends T['position'],
>(pos: P | '*' = '*') {
return (tag: T): tag is PredicatePositionExtract<T, P> =>
pos === '*' || pos === tag.position;
}
export function convertEbmlTagToComponent(tag: EbmlTagType) {
if (tag.type === EbmlElementType.Master) {
const obj: Record<string, any> = {};
const children = tag.children;
for (const c of children) {
const name = EbmlTagIdEnum[c.id];
const converted = convertEbmlTagToComponent(c);
if (IdMultiSet.has(c.id)) {
if (obj[name]) {
obj[name].push(converted);
} else {
obj[name] = [converted];
}
} else {
obj[name] = converted;
}
}
return obj;
}
if (tag.id === EbmlTagIdEnum.SimpleBlock || tag.id === EbmlTagIdEnum.Block) {
return tag;
}
return tag.data;
}
export function waitTick() {
return new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
resolve();
timeout && clearTimeout(timeout);
}, 0);
});
}

View File

@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"paths": {
"@konoplayer/core/*": [
"../core/src/*"
]
}
},
"include": [
"src"
],
"references": [
{
"path": "../core"
}
]
}

View File

@@ -1,7 +1,7 @@
[package]
name = "konoplayer-demuxing"
name = "konoplayer-symphonia"
version = "0.1.0"
edition = "2024"
[dependencies]
symphonia-format-mkv = "0.5.4"
symphonia = "0.5.4"