diff --git a/apps/mock/public/video/bear-vp9.webm b/apps/mock/public/video/bear-vp9.webm
new file mode 100644
index 0000000..4f497ae
Binary files /dev/null and b/apps/mock/public/video/bear-vp9.webm differ
diff --git a/apps/playground/src/index.html b/apps/playground/src/index.html
index a31a7ed..98f3f6c 100644
--- a/apps/playground/src/index.html
+++ b/apps/playground/src/index.html
@@ -4,6 +4,6 @@
-
-
+
+
\ No newline at end of file
diff --git a/apps/playground/src/media/base/audio_codecs.ts b/apps/playground/src/media/base/audio_codecs.ts
new file mode 100644
index 0000000..0ce538d
--- /dev/null
+++ b/apps/playground/src/media/base/audio_codecs.ts
@@ -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.
+}
diff --git a/apps/playground/src/media/base/errors.ts b/apps/playground/src/media/base/errors.ts
new file mode 100644
index 0000000..86459b5
--- /dev/null
+++ b/apps/playground/src/media/base/errors.ts
@@ -0,0 +1,11 @@
+export class UnsupportCodecError extends Error {
+ constructor(codec: string, context: string) {
+ super(`codec ${codec} is not supported in ${context} context`);
+ }
+}
+
+export class ParseCodecPrivateError extends Error {
+ constructor(codec: string, detail: string) {
+ super(`code ${codec} private parse failed: ${detail}`);
+ }
+}
diff --git a/apps/playground/src/media/base/video_codecs.ts b/apps/playground/src/media/base/video_codecs.ts
new file mode 100644
index 0000000..a226caa
--- /dev/null
+++ b/apps/playground/src/media/base/video_codecs.ts
@@ -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;
+};
diff --git a/apps/playground/src/media/mkv/codecs/aac.ts b/apps/playground/src/media/mkv/codecs/aac.ts
new file mode 100644
index 0000000..8eed1b0
--- /dev/null
+++ b/apps/playground/src/media/mkv/codecs/aac.ts
@@ -0,0 +1,113 @@
+import { ParseCodecPrivateError } from '@/media/base/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 ParseCodecPrivateError(
+ 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 ParseCodecPrivateError(
+ AAC_CODEC_TYPE,
+ 'Invalid AudioSpecificConfig'
+ );
+ error.cause = validation;
+ throw error;
+ }
+
+ return config;
+}
+
+export function genCodecIdByAudioSpecificConfig(
+ config: AudioSpecificConfigType
+) {
+ return `mp4a.40.${config.audioObjectType}`;
+}
diff --git a/apps/playground/src/media/mkv/codecs/avc.ts b/apps/playground/src/media/mkv/codecs/avc.ts
new file mode 100644
index 0000000..bcb6108
--- /dev/null
+++ b/apps/playground/src/media/mkv/codecs/avc.ts
@@ -0,0 +1,125 @@
+import { ParseCodecPrivateError } from '@/media/base/errors';
+import { type } from 'arktype';
+
+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)
+ .array()
+ .atLeastLength(1), // Sequence Parameter Sets (SPS)
+ pps: type
+ .instanceOf(Uint8Array)
+ .array()
+ .atLeastLength(1), // Picture Parameter Sets (PPS)
+});
+
+export type AVCDecoderConfigurationRecordType =
+ typeof AVCDecoderConfigurationRecordSchema.infer;
+
+/**
+ * Parse AVCDecoderConfigurationRecord from codec_private Uint8Array
+ * @param codecPrivate - Uint8Array containing codec_private data
+ * @returns Parsed AVCDecoderConfigurationRecord or throws an error if invalid
+ */
+export function parseAVCDecoderConfigurationRecord(
+ codecPrivate: Uint8Array
+): AVCDecoderConfigurationRecordType {
+ let offset = 0;
+
+ // Check if data length is sufficient
+ if (codecPrivate.length < 5) {
+ throw new ParseCodecPrivateError(
+ AVC_CODEC_TYPE,
+ 'Input data too short for AVCDecoderConfigurationRecord'
+ );
+ }
+
+ const configurationVersion = codecPrivate[offset++];
+ const avcProfileIndication = codecPrivate[offset++];
+ const profileCompatibility = codecPrivate[offset++];
+ const avcLevelIndication = codecPrivate[offset++];
+
+ // Read lengthSizeMinusOne (first 6 bits are reserved, typically 0xFF, last 2 bits are the value)
+ const lengthSizeMinusOne = codecPrivate[offset++] & 0x03;
+
+ // Read number of SPS (first 3 bits are reserved, typically 0xE0, last 5 bits are SPS count)
+ const numOfSPS = codecPrivate[offset++] & 0x1f;
+ const sps: Uint8Array[] = [];
+
+ // Parse SPS
+ for (let i = 0; i < numOfSPS; i++) {
+ if (offset + 2 > codecPrivate.length) {
+ throw new ParseCodecPrivateError(AVC_CODEC_TYPE, 'Invalid SPS length');
+ }
+
+ const spsLength = (codecPrivate[offset] << 8) | codecPrivate[offset + 1];
+ offset += 2;
+
+ if (offset + spsLength > codecPrivate.length) {
+ throw new ParseCodecPrivateError(
+ 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 ParseCodecPrivateError(AVC_CODEC_TYPE, 'No space for PPS count');
+ }
+ const numOfPPS = codecPrivate[offset++];
+ const pps: Uint8Array[] = [];
+
+ // Parse PPS
+ for (let i = 0; i < numOfPPS; i++) {
+ if (offset + 2 > codecPrivate.length) {
+ throw new ParseCodecPrivateError(AVC_CODEC_TYPE, 'Invalid PPS length');
+ }
+
+ const ppsLength = (codecPrivate[offset] << 8) | codecPrivate[offset + 1];
+ offset += 2;
+
+ if (offset + ppsLength > codecPrivate.length) {
+ throw new ParseCodecPrivateError(
+ 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 genCodecIdByAVCDecoderConfigurationRecord(
+ 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 / 10)
+ .toString(16)
+ .replace(/./g, '')
+ .padStart(2, '0');
+ return `avc1.${profileHex}${profileCompatHex}${levelHex}`;
+}
diff --git a/apps/playground/src/media/mkv/codecs/hevc.ts b/apps/playground/src/media/mkv/codecs/hevc.ts
new file mode 100644
index 0000000..31a7b7f
--- /dev/null
+++ b/apps/playground/src/media/mkv/codecs/hevc.ts
@@ -0,0 +1,144 @@
+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).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[],
+ };
+ 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}`;
+}
diff --git a/apps/playground/src/media/mkv/codecs/index.ts b/apps/playground/src/media/mkv/codecs/index.ts
new file mode 100644
index 0000000..bf8821b
--- /dev/null
+++ b/apps/playground/src/media/mkv/codecs/index.ts
@@ -0,0 +1,229 @@
+import { AudioCodec } from '../../base/audio_codecs';
+import { UnsupportCodecError } from '../../base/errors';
+import { VideoCodec } from '../../base/video_codecs';
+import type { TrackEntryType } from '../schema';
+import {
+ genCodecIdByAudioSpecificConfig,
+ parseAudioSpecificConfig,
+} from './aac';
+import {
+ genCodecIdByAVCDecoderConfigurationRecord,
+ parseAVCDecoderConfigurationRecord,
+} from './avc';
+
+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 function videoCodecIdToWebCodecsVideoDecoder(
+ track: TrackEntryType
+): [VideoCodec, string] {
+ const codecId = track.CodecID;
+ const codecPrivate = track.CodecPrivate;
+ switch (codecId) {
+ case VideoCodecId.HEVC:
+ return [VideoCodec.HEVC, 'hevc'];
+ case VideoCodecId.VP9:
+ return [VideoCodec.VP9, 'vp09'];
+ case VideoCodecId.AV1:
+ return [VideoCodec.AV1, 'av1'];
+ case VideoCodecId.H264:
+ if (!codecPrivate) {
+ throw new UnsupportCodecError(
+ 'h264(without codec_private profile)',
+ 'web codecs audio decoder'
+ );
+ }
+ return [
+ VideoCodec.H264,
+ genCodecIdByAVCDecoderConfigurationRecord(
+ parseAVCDecoderConfigurationRecord(codecPrivate)
+ ),
+ ];
+ case VideoCodecId.THEORA:
+ return [VideoCodec.Theora, 'theora'];
+ case VideoCodecId.VP8:
+ return [VideoCodec.VP8, 'vp8'];
+ case VideoCodecId.MPEG4_ISO_SP:
+ return [VideoCodec.MPEG4, 'mp4v.01.3'];
+ case VideoCodecId.MPEG4_ISO_ASP:
+ return [VideoCodec.MPEG4, 'mp4v.20.9'];
+ case VideoCodecId.MPEG4_ISO_AP:
+ return [VideoCodec.MPEG4, 'mp4v.20.9'];
+ default:
+ throw new UnsupportCodecError(codecId, 'web codecs video decoder');
+ }
+}
+
+export function videoCodecIdToWebCodecsAudioDecoder(
+ track: TrackEntryType
+): [AudioCodec, string] {
+ const codecId = track.CodecID;
+ const codecPrivate = track.CodecPrivate;
+ const bitDepth = track.Audio?.BitDepth;
+ switch (track.CodecID) {
+ case AudioCodecId.AAC_MPEG4_MAIN:
+ case AudioCodecId.AAC_MPEG2_MAIN:
+ return [AudioCodec.AAC, 'mp4a.40.1'];
+ case AudioCodecId.AAC_MPEG2_LC:
+ case AudioCodecId.AAC_MPEG4_LC:
+ return [AudioCodec.AAC, 'mp4a.40.2'];
+ case AudioCodecId.AAC_MPEG2_SSR:
+ case AudioCodecId.AAC_MPEG4_SSR:
+ return [AudioCodec.AAC, 'mp4a.40.3'];
+ case AudioCodecId.AAC_MPEG4_LTP:
+ return [AudioCodec.AAC, 'mp4a.40.4'];
+ case AudioCodecId.AAC_MPEG2_LC_SBR:
+ case AudioCodecId.AAC_MPEG4_SBR:
+ return [AudioCodec.AAC, 'mp4a.40.5'];
+ case AudioCodecId.AAC:
+ return [
+ AudioCodec.AAC,
+ codecPrivate
+ ? genCodecIdByAudioSpecificConfig(
+ parseAudioSpecificConfig(codecPrivate)
+ )
+ : 'mp4a.40.2',
+ ];
+ case AudioCodecId.AC3:
+ case AudioCodecId.AC3_BSID9:
+ return [AudioCodec.AC3, 'ac-3'];
+ case AudioCodecId.EAC3:
+ case AudioCodecId.AC3_BSID10:
+ return [AudioCodec.EAC3, 'ec-3'];
+ case AudioCodecId.MPEG_L3:
+ return [AudioCodec.MP3, 'mp3'];
+ case AudioCodecId.VORBIS:
+ return [AudioCodec.Vorbis, 'vorbis'];
+ case AudioCodecId.FLAC:
+ return [AudioCodec.FLAC, 'flac'];
+ case AudioCodecId.OPUS:
+ return [AudioCodec.Opus, 'opus'];
+ case AudioCodecId.ALAC:
+ return [AudioCodec.ALAC, 'alac'];
+ case AudioCodecId.PCM_INT_BIG:
+ if (bitDepth === 16) {
+ return [AudioCodec.PCM_S16BE, 'pcm-s16be'];
+ }
+ if (bitDepth === 24) {
+ return [AudioCodec.PCM_S24BE, 'pcm-s24be'];
+ }
+ if (bitDepth === 32) {
+ return [AudioCodec.PCM_S32BE, 'pcm-s32be'];
+ }
+ throw new UnsupportCodecError(
+ `${codecId}(${bitDepth}b)`,
+ 'web codecs audio decoder'
+ );
+ case AudioCodecId.PCM_INT_LIT:
+ if (bitDepth === 16) {
+ return [AudioCodec.PCM_S16LE, 'pcm-s16le'];
+ }
+ if (bitDepth === 24) {
+ return [AudioCodec.PCM_S24LE, 'pcm-s24le'];
+ }
+ if (bitDepth === 32) {
+ return [AudioCodec.PCM_S32LE, 'pcm-s32le'];
+ }
+ throw new UnsupportCodecError(
+ `${codecId}(${bitDepth}b)`,
+ 'web codecs audio decoder'
+ );
+ case AudioCodecId.PCM_FLOAT_IEEE:
+ return [AudioCodec.PCM_F32LE, 'pcm-f32le'];
+ default:
+ throw new UnsupportCodecError(codecId, 'web codecs audio decoder');
+ }
+}
diff --git a/apps/playground/src/media/mkv/model.ts b/apps/playground/src/media/mkv/model.ts
index b2ea239..0017e9d 100644
--- a/apps/playground/src/media/mkv/model.ts
+++ b/apps/playground/src/media/mkv/model.ts
@@ -8,8 +8,8 @@ import {
type EbmlSegmentTagType,
EbmlTagIdEnum,
EbmlTagPosition,
- EbmlTagsTagType,
- EbmlTagTagType,
+ type EbmlTagsTagType,
+ type EbmlTagTagType,
type EbmlTagType,
type EbmlTrackEntryTagType,
type EbmlTracksTagType,
@@ -28,7 +28,7 @@ import {
SeekHeadSchema,
type SeekHeadType,
TagSchema,
- TagType,
+ type TagType,
TrackEntrySchema,
type TrackEntryType,
} from './schema';
diff --git a/biome.jsonc b/biome.jsonc
index e02fabf..2e7d645 100644
--- a/biome.jsonc
+++ b/biome.jsonc
@@ -6,6 +6,7 @@
"linter": {
"rules": {
"style": {
+ "useSingleCaseStatement": "off",
"noParameterProperties": "off",
"noNonNullAssertion": "off"
},
diff --git a/package.json b/package.json
index e159c6a..c3aa175 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"@types/lodash-es": "^4.17.12",
"arktype": "^2.1.10",
"lodash-es": "^4.17.21",
+ "media-codecs": "^2.0.2",
"mnemonist": "^0.40.3",
"rxjs": "^7.8.2",
"type-fest": "^4.37.0"
diff --git a/packages/codecs/package.json b/packages/codecs/package.json
new file mode 100644
index 0000000..7aa368c
--- /dev/null
+++ b/packages/codecs/package.json
@@ -0,0 +1,19 @@
+{
+ "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"
+ }
+}
\ No newline at end of file
diff --git a/packages/codecs/src/index.ts b/packages/codecs/src/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eb98867..1df4d20 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,9 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
+ media-codecs:
+ specifier: ^2.0.2
+ version: 2.0.2
mnemonist:
specifier: ^0.40.3
version: 0.40.3
@@ -108,6 +111,22 @@ importers:
specifier: ^2.9.93
version: 2.9.94
+ packages/codecs:
+ dependencies:
+ konoebml:
+ specifier: 0.1.2-rc.5
+ version: 0.1.2-rc.5(arktype@2.1.10)
+ lit:
+ specifier: ^3.2.1
+ version: 3.2.1
+ devDependencies:
+ '@rsbuild/core':
+ specifier: ^1.2.14
+ version: 1.2.15
+ typescript:
+ specifier: ^5.8.2
+ version: 5.8.2
+
packages:
'@angular-devkit/core@19.1.8':
@@ -1923,6 +1942,10 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
+ media-codecs@2.0.2:
+ resolution: {integrity: sha512-D7ygdW7j5yqkDJ9kX5H4UU2iC/fsreU2Vy49GxbN6OUeYso6U2QG6QgcwSn75eFUoM6ttVuTLAOt4qxQvLWdFw==}
+ engines: {node: '>=22.0.0', npm: '>=10.5.1', snowdev: '>=2.2.x'}
+
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@@ -4550,6 +4573,8 @@ snapshots:
math-intrinsics@1.1.0: {}
+ media-codecs@2.0.2: {}
+
media-typer@0.3.0: {}
media-typer@1.1.0: {}