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: {}