From 0b681d4fd1231e76ea33ff41ba8110eb989b19d8 Mon Sep 17 00:00:00 2001 From: lonelyhentxi Date: Sat, 22 Mar 2025 03:11:40 +0800 Subject: [PATCH] feat: add codegen and refactor mkv models --- apps/playground/src/media/mkv/model.ts | 303 +++++++++++++++------- apps/playground/src/media/mkv/reactive.ts | 70 +++-- apps/playground/src/media/mkv/schema.ts | 232 ++++++++--------- apps/playground/src/media/mkv/util.ts | 11 +- scripts/codegen-mkv.ts | 3 + 5 files changed, 345 insertions(+), 274 deletions(-) diff --git a/apps/playground/src/media/mkv/model.ts b/apps/playground/src/media/mkv/model.ts index 6f02bda..42ce80a 100644 --- a/apps/playground/src/media/mkv/model.ts +++ b/apps/playground/src/media/mkv/model.ts @@ -1,135 +1,229 @@ import { - type EbmlTagType, - EbmlTagIdEnum, - EbmlTagPosition, - type EbmlTracksTagType, - type EbmlInfoTagType, + type EbmlClusterTagType, + type EbmlCuePointTagType, type EbmlCuesTagType, + type EbmlInfoTagType, + type EbmlMasterTagType, type EbmlSeekHeadTagType, type EbmlSegmentTagType, - type EbmlCuePointTagType, - type EbmlMasterTagType, + EbmlTagIdEnum, + EbmlTagPosition, + 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 { - convertEbmlTagToModelShape, - type InferType, - isTagIdPos, - SEEK_ID_KAX_CUES, - SEEK_ID_KAX_INFO, - SEEK_ID_KAX_TRACKS, -} from './util'; -import { isEqual } from 'lodash-es'; -import type { Type } from 'arktype'; -import { CuePointSchema, type CuePointType } from './schema'; + ClusterSchema, + type ClusterType, + CuePointSchema, + type CuePointType, + type CueTrackPositionsType, + InfoSchema, + type InfoType, + SeekHeadSchema, + type SeekHeadType, + TrackEntrySchema, + type TrackEntryType +} from './schema'; -export abstract class StandardComponentSystem< - E extends EbmlMasterTagType, - S extends Type, -> { - abstract get schema(): S; +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]); - componentFromTag(tag: E): InferType { - const extracted = convertEbmlTagToModelShape(tag); - return this.schema.assert(extracted) as InferType; +export class SegmentSystem { + startTag: EbmlSegmentTagType; + headTags: EbmlTagType[] = []; + + cue: CueSystem; + cluster: ClusterSystem; + seek: SeekSystem; + info: InfoSystem; + track: TrackSystem; + + + 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); + } + + get dataStartOffset() { + return this.startTag.startOffset + this.startTag.headerLength; + } + + get startOffset () { + return this.startTag.startOffset; + } + + completeHeads () { + 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); + + 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); + } + + return this; + } + + scanHead (tag: EbmlTagType) { + if ( + tag.id === EbmlTagIdEnum.SeekHead && + tag.position === EbmlTagPosition.End + ) { + this.seek.addSeekHeadTag(tag); + } + this.headTags.push(tag); + this.seek.memoTag(tag); + return this; } } -export class EbmlSegment { - startNode: EbmlSegmentTagType; - seekHeadNode?: EbmlSeekHeadTagType; - seekEntries: EbmlSeekEntry[]; - tracksNode?: EbmlTracksTagType; - infoNode?: EbmlInfoTagType; - cuesNode?: EbmlCuesTagType; - metaBuffer: EbmlTagType[] = []; - metaOffsets: Map = new Map(); +export class SegmentComponentSystemTrait> { + segment: SegmentSystem; - constructor(startNode: EbmlSegmentTagType) { - this.startNode = startNode; - this.seekEntries = []; - this.metaBuffer = []; + get schema(): S { + throw new Error("unimplemented!") } - get dataOffset() { - return this.startNode.startOffset + this.startNode.headerLength; + constructor(segment: SegmentSystem) { + this.segment = segment; } - private addSeekHead(node: EbmlSeekHeadTagType) { - this.seekHeadNode = node; - this.seekEntries = this.seekHeadNode.children - .filter(isTagIdPos(EbmlTagIdEnum.Seek, EbmlTagPosition.End)) - .map((c) => { - const seekId = c.children.find( - (item) => item.id === EbmlTagIdEnum.SeekID - )?.data; - const seekPosition = c.children.find( - (item) => item.id === EbmlTagIdEnum.SeekPosition - )?.data as number; - if (seekId && seekPosition) { - return { - seekId, - seekPosition, - }; - } - return null; - }) - .filter((c): c is EbmlSeekEntry => !!c); + componentFromTag(tag: E): InferType { + const extracted = convertEbmlTagToComponent(tag); + const result = this.schema(extracted); + if (result instanceof ArkErrors) { + const errors = result; + console.error('Parse component from tag error:', tag.toDebugRecord(), errors.flatProblemsByPath) + throw errors; + } + return result as InferType + } +} + +export class SeekSystem extends SegmentComponentSystemTrait { + override get schema() { + return SeekHeadSchema; } - findSeekPositionBySeekId(seekId: Uint8Array): number | undefined { - return this.seekEntries.find((c) => isEqual(c.seekId, seekId)) - ?.seekPosition; + seekHeads: SeekHeadType[] = []; + offsetToTagMemo: Map = new Map(); + + memoTag (tag: EbmlTagType) { + this.offsetToTagMemo.set(tag.startOffset, tag); } - findLocalNodeBySeekId(seekId: Uint8Array): EbmlTagType | undefined { - return this.findLocalNodeBySeekPosition( - this.findSeekPositionBySeekId(seekId) - ); + addSeekHeadTag (tag: EbmlSeekHeadTagType) { + const seekHead = this.componentFromTag(tag); + this.seekHeads.push(seekHead); + return seekHead; } - findLocalNodeBySeekPosition( - seekPosition: number | undefined + offsetFromSeekPosition (position: number): number { + return position + this.segment.startOffset; + } + + offsetFromSeekDataPosition (position: number) : number { + return position + this.segment.dataStartOffset; + } + + seekTagByStartOffset ( + startOffset: number | undefined ): EbmlTagType | undefined { - return seekPosition! >= 0 - ? this.metaOffsets.get(seekPosition as number) + return startOffset! >= 0 + ? this.offsetToTagMemo.get(startOffset!) : undefined; } - markMetaEnd() { - this.infoNode = this.findLocalNodeBySeekId( - SEEK_ID_KAX_INFO - ) as EbmlInfoTagType; - this.tracksNode = this.findLocalNodeBySeekId( - SEEK_ID_KAX_TRACKS - ) as EbmlTracksTagType; - this.cuesNode = this.findLocalNodeBySeekId( - SEEK_ID_KAX_CUES - ) as EbmlCuesTagType; + seekOffsetBySeekId(seekId: Uint8Array): number | undefined { + const seekPosition = this.seekHeads[0]?.Seek?.find((c) => isEqual(c.SeekID, seekId)) + ?.SeekPosition; + return seekPosition! >= 0 ? this.offsetFromSeekPosition(seekPosition!) : undefined; } - scanMeta(node: EbmlTagType): boolean { - if ( - node.id === EbmlTagIdEnum.SeekHead && - node.position === EbmlTagPosition.End - ) { - this.addSeekHead(node); - } - this.metaBuffer.push(node); - this.metaOffsets.set(node.startOffset - this.dataOffset, node); - return true; + seekTagBySeekId(seekId: Uint8Array): EbmlTagType | undefined { + return this.seekTagByStartOffset( + this.seekOffsetBySeekId(seekId) + ); } } -export class CuesSystem extends StandardComponentSystem< +export class InfoSystem extends SegmentComponentSystemTrait { + override get schema() { + return InfoSchema; + } + + info!: InfoType; + + prepareWithInfoTag (tag: EbmlInfoTagType) { + this.info = this.componentFromTag(tag); + return this; + } +} + +export class ClusterSystem extends SegmentComponentSystemTrait { + override get schema() { + return ClusterSchema + } + + clustersBuffer: ClusterType[] = []; + + addClusterWithTag (tag: EbmlClusterTagType): ClusterType { + const cluster = this.componentFromTag(tag); + this.clustersBuffer.push(cluster); + return cluster; + } +} + +export class TrackSystem extends SegmentComponentSystemTrait { + override get schema() { + return TrackEntrySchema; + } + + tracks = new Map(); + + prepareTracksWithTag (tag: EbmlTracksTagType) { + this.tracks.clear(); + for (const c of tag.children) { + if (c.id === EbmlTagIdEnum.TrackEntry) { + const trackEntry = this.componentFromTag(c); + this.tracks.set(trackEntry.TrackNumber, trackEntry); + } + } + return this; + } +} + +export class CueSystem extends SegmentComponentSystemTrait< EbmlCuePointTagType, typeof CuePointSchema > { - schema = CuePointSchema; - cues: CuePointType[]; + override get schema () { + return CuePointSchema + }; - constructor(cues: CuePointType[]) { - super(); - this.cues = cues; + cues: 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 { @@ -170,4 +264,19 @@ export class CuesSystem extends StandardComponentSystem< ? 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; + } } diff --git a/apps/playground/src/media/mkv/reactive.ts b/apps/playground/src/media/mkv/reactive.ts index 3579377..9b630a7 100644 --- a/apps/playground/src/media/mkv/reactive.ts +++ b/apps/playground/src/media/mkv/reactive.ts @@ -26,8 +26,9 @@ import { withLatestFrom, } from 'rxjs'; import { createRangedStream } from '@/fetch'; -import { EbmlSegment, Cluster, SEEK_ID_KAX_CUES, CuesSystem } from './model'; +import { SegmentSystem, SEEK_ID_KAX_CUES, type CueSystem } from './model'; import { isTagIdPos } from './util'; +import type { ClusterType } from "./schema"; export function createRangedEbmlStream( url: string, @@ -124,12 +125,14 @@ export function createEbmlController(src: string) { const segments$ = segmentStart$.pipe( map((startTag) => { - const segment = new EbmlSegment(startTag); + const segment = new SegmentSystem(startTag); + const clusterSystem = segment.cluster; + const seekSystem = segment.seek; const continuousReusedCluster$ = ebml$.pipe( filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)), filter((s) => s.id === EbmlTagIdEnum.Cluster), - map(Cluster.fromTag.bind(Cluster)) + map(clusterSystem.addClusterWithTag.bind(clusterSystem)) ); const segmentEnd$ = ebml$.pipe( @@ -154,33 +157,27 @@ export function createEbmlController(src: string) { ); const withMeta$ = meta$.pipe( - reduce((segment, meta) => { - segment.scanMeta(meta); - return segment; - }, segment), - map((segment) => { - segment.markMetaEnd(); - return segment; - }), + reduce((segment, meta) => segment.scanHead(meta), segment), + map(segment.completeHeads.bind(segment)), take(1), shareReplay(1) ); const withRemoteCues$ = withMeta$.pipe( switchMap((s) => { - if (s.cuesNode) { + const cueSystem = s.cue; + const seekSystem = s.seek; + if (cueSystem.prepared) { return EMPTY; } - const cuesStartOffset = - s.dataOffset + - (s.findSeekPositionBySeekId(SEEK_ID_KAX_CUES) ?? Number.NaN); - if (cuesStartOffset >= 0) { - return createRangedEbmlStream(src, cuesStartOffset).pipe( + const remoteCuesTagStartOffset = seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_CUES); + if (remoteCuesTagStartOffset! >= 0) { + return createRangedEbmlStream(src, remoteCuesTagStartOffset).pipe( switchMap((req) => req.ebml$), filter(isTagIdPos(EbmlTagIdEnum.Cues, EbmlTagPosition.End)), withLatestFrom(withMeta$), map(([cues, withMeta]) => { - withMeta.cuesNode = cues; + withMeta.cue.prepareCuesWithTag(cues); return withMeta; }) ); @@ -192,12 +189,7 @@ export function createEbmlController(src: string) { ); const withLocalCues$ = withMeta$.pipe( - switchMap((s) => { - if (s.cuesNode) { - return of(s); - } - return EMPTY; - }), + switchMap((s) => s.cue.prepared ? of(s) : EMPTY), shareReplay(1) ); @@ -210,7 +202,7 @@ export function createEbmlController(src: string) { switchMap((empty) => (empty ? withMeta$ : EMPTY)) ); - const seekWithoutCues = (seekTime: number): Observable => { + const seekWithoutCues = (seekTime: number): Observable => { const cluster$ = continuousReusedCluster$.pipe( isEmpty(), switchMap((empty) => { @@ -223,7 +215,7 @@ export function createEbmlController(src: string) { filter( isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End) ), - map(Cluster.fromTag.bind(Cluster)) + map((tag) => clusterSystem.addClusterWithTag(tag)) ) : continuousReusedCluster$; }) @@ -236,23 +228,23 @@ export function createEbmlController(src: string) { scan( (prev, curr) => [prev?.[1], curr] as [ - Cluster | undefined, - Cluster | undefined, + ClusterType | undefined, + ClusterType | undefined, ], [undefined, undefined] as [ - Cluster | undefined, - Cluster | undefined, + ClusterType | undefined, + ClusterType | undefined, ] ), - filter((c) => c[1]?.timestamp! > seekTime), + filter((c) => c[1]?.Timestamp! > seekTime), map((c) => c[0] ?? c[1]!) ); }; const seekWithCues = ( - cues: CuesSystem, + cues: CueSystem, seekTime: number - ): Observable => { + ): Observable => { if (seekTime === 0) { return seekWithoutCues(seekTime); } @@ -265,29 +257,29 @@ export function createEbmlController(src: string) { return createRangedEbmlStream( src, - cuePoint.position + segment.dataOffset + seekSystem.offsetFromSeekDataPosition(cues.getCueTrackPositions(cuePoint).CueClusterPosition) ).pipe( switchMap((req) => req.ebml$), filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)), - map(Cluster.fromTag.bind(Cluster)) + map(clusterSystem.addClusterWithTag.bind(clusterSystem)) ); }; - const seek = (seekTime: number): Observable => { + const seek = (seekTime: number): Observable => { if (seekTime === 0) { - const subscripton = merge(withCues$, withoutCues$).subscribe(); + const subscription = merge(withCues$, withoutCues$).subscribe(); // if seekTime equals to 0 at start, reuse the initialize stream return seekWithoutCues(seekTime).pipe( finalize(() => { - subscripton.unsubscribe(); + subscription.unsubscribe(); }) ); } return merge( withCues$.pipe( switchMap((s) => - seekWithCues(CuesSystem.fromTag(s.cuesNode!), seekTime) + seekWithCues(s.cue, seekTime) ) ), withoutCues$.pipe(switchMap((_) => seekWithoutCues(seekTime))) diff --git a/apps/playground/src/media/mkv/schema.ts b/apps/playground/src/media/mkv/schema.ts index b7fab35..1a84ffd 100644 --- a/apps/playground/src/media/mkv/schema.ts +++ b/apps/playground/src/media/mkv/schema.ts @@ -1,5 +1,5 @@ import { type, match } from 'arktype'; -import { EbmlTagIdEnum, EbmlSimpleBlockTag, EbmlBlockTag } from 'konoebml'; +import { EbmlTagIdEnum, EbmlSimpleBlockTag ,EbmlBlockTag } from 'konoebml'; export const BinarySchema = type.instanceOf(Uint8Array); export const SimpleBlockSchema = type.instanceOf(EbmlSimpleBlockTag); @@ -33,7 +33,7 @@ export const SeekSchema = type({ export type SeekType = typeof SeekSchema.infer; export const SeekHeadSchema = type({ - Seek: SeekSchema.array(), + Seek: SeekSchema.array().atLeastLength(1), }); export type SeekHeadType = typeof SeekHeadSchema.infer; @@ -79,7 +79,7 @@ export const BlockMoreSchema = type({ export type BlockMoreType = typeof BlockMoreSchema.infer; export const BlockAdditionsSchema = type({ - BlockMore: BlockMoreSchema.array(), + BlockMore: BlockMoreSchema.array().atLeastLength(1), }); export type BlockAdditionsType = typeof BlockAdditionsSchema.infer; @@ -198,12 +198,9 @@ export enum MatrixCoefficientsRestrictionEnum { CHROMA_DERIVED_CONSTANT_LUMINANCE = 13, // ITU-R BT.2100-0 ITU_R_BT_2100_0 = 14, -} -export const MatrixCoefficientsRestriction = type( - '0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14' -); -export type MatrixCoefficientsRestrictionType = - typeof MatrixCoefficientsRestriction.infer; +}; +export const MatrixCoefficientsRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14'); +export type MatrixCoefficientsRestrictionType = typeof MatrixCoefficientsRestriction.infer; export enum ChromaSitingHorzRestrictionEnum { // unspecified @@ -212,10 +209,9 @@ export enum ChromaSitingHorzRestrictionEnum { LEFT_COLLOCATED = 1, // half HALF = 2, -} +}; export const ChromaSitingHorzRestriction = type('0 | 1 | 2'); -export type ChromaSitingHorzRestrictionType = - typeof ChromaSitingHorzRestriction.infer; +export type ChromaSitingHorzRestrictionType = typeof ChromaSitingHorzRestriction.infer; export enum ChromaSitingVertRestrictionEnum { // unspecified @@ -224,10 +220,9 @@ export enum ChromaSitingVertRestrictionEnum { TOP_COLLOCATED = 1, // half HALF = 2, -} +}; export const ChromaSitingVertRestriction = type('0 | 1 | 2'); -export type ChromaSitingVertRestrictionType = - typeof ChromaSitingVertRestriction.infer; +export type ChromaSitingVertRestrictionType = typeof ChromaSitingVertRestriction.infer; export enum RangeRestrictionEnum { // unspecified @@ -238,7 +233,7 @@ export enum RangeRestrictionEnum { FULL_RANGE_NO_CLIPPING = 2, // defined by MatrixCoefficients / TransferCharacteristics DEFINED_BY_MATRIX_COEFFICIENTS_TRANSFER_CHARACTERISTICS = 3, -} +}; export const RangeRestriction = type('0 | 1 | 2 | 3'); export type RangeRestrictionType = typeof RangeRestriction.infer; @@ -281,12 +276,9 @@ export enum TransferCharacteristicsRestrictionEnum { SMPTE_ST_428_1 = 17, // ARIB STD-B67 (HLG) ARIB_STD_B67_HLG = 18, -} -export const TransferCharacteristicsRestriction = type( - '0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18' -); -export type TransferCharacteristicsRestrictionType = - typeof TransferCharacteristicsRestriction.infer; +}; +export const TransferCharacteristicsRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18'); +export type TransferCharacteristicsRestrictionType = typeof TransferCharacteristicsRestriction.infer; export enum PrimariesRestrictionEnum { // reserved @@ -317,10 +309,8 @@ export enum PrimariesRestrictionEnum { SMPTE_EG_432_2 = 12, // EBU Tech. 3213-E - JEDEC P22 phosphors EBU_TECH_3213_E_JEDEC_P22_PHOSPHORS = 22, -} -export const PrimariesRestriction = type( - '0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22' -); +}; +export const PrimariesRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22'); export type PrimariesRestrictionType = typeof PrimariesRestriction.infer; export const ColourSchema = type({ @@ -351,10 +341,9 @@ export enum ProjectionTypeRestrictionEnum { CUBEMAP = 2, // mesh MESH = 3, -} +}; export const ProjectionTypeRestriction = type('0 | 1 | 2 | 3'); -export type ProjectionTypeRestrictionType = - typeof ProjectionTypeRestriction.infer; +export type ProjectionTypeRestrictionType = typeof ProjectionTypeRestriction.infer; export const ProjectionSchema = type({ ProjectionType: ProjectionTypeRestriction.default(0), @@ -373,10 +362,9 @@ export enum FlagInterlacedRestrictionEnum { INTERLACED = 1, // progressive PROGRESSIVE = 2, -} +}; export const FlagInterlacedRestriction = type('0 | 1 | 2'); -export type FlagInterlacedRestrictionType = - typeof FlagInterlacedRestriction.infer; +export type FlagInterlacedRestrictionType = typeof FlagInterlacedRestriction.infer; export enum FieldOrderRestrictionEnum { // progressive @@ -391,7 +379,7 @@ export enum FieldOrderRestrictionEnum { TFF_INTERLEAVED = 9, // bff (interleaved) BFF_INTERLEAVED = 14, -} +}; export const FieldOrderRestriction = type('0 | 1 | 2 | 6 | 9 | 14'); export type FieldOrderRestrictionType = typeof FieldOrderRestriction.infer; @@ -426,10 +414,8 @@ export enum StereoModeRestrictionEnum { BOTH_EYES_LACED_IN_ONE_BLOCK_LEFT_EYE_IS_FIRST = 13, // both eyes laced in one Block (right eye is first) BOTH_EYES_LACED_IN_ONE_BLOCK_RIGHT_EYE_IS_FIRST = 14, -} -export const StereoModeRestriction = type( - '0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14' -); +}; +export const StereoModeRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14'); export type StereoModeRestrictionType = typeof StereoModeRestriction.infer; export enum AlphaModeRestrictionEnum { @@ -437,7 +423,7 @@ export enum AlphaModeRestrictionEnum { NONE = 0, // present PRESENT = 1, -} +}; export const AlphaModeRestriction = type('0 | 1'); export type AlphaModeRestrictionType = typeof AlphaModeRestriction.infer; @@ -450,10 +436,9 @@ export enum OldStereoModeRestrictionEnum { LEFT_EYE = 2, // both eyes BOTH_EYES = 3, -} +}; export const OldStereoModeRestriction = type('0 | 1 | 2 | 3'); -export type OldStereoModeRestrictionType = - typeof OldStereoModeRestriction.infer; +export type OldStereoModeRestrictionType = typeof OldStereoModeRestriction.infer; export enum DisplayUnitRestrictionEnum { // pixels @@ -466,7 +451,7 @@ export enum DisplayUnitRestrictionEnum { DISPLAY_ASPECT_RATIO = 3, // unknown UNKNOWN = 4, -} +}; export const DisplayUnitRestriction = type('0 | 1 | 2 | 3 | 4'); export type DisplayUnitRestrictionType = typeof DisplayUnitRestriction.infer; @@ -477,10 +462,9 @@ export enum AspectRatioTypeRestrictionEnum { KEEP_ASPECT_RATIO = 1, // fixed FIXED = 2, -} +}; export const AspectRatioTypeRestriction = type('0 | 1 | 2'); -export type AspectRatioTypeRestrictionType = - typeof AspectRatioTypeRestriction.infer; +export type AspectRatioTypeRestrictionType = typeof AspectRatioTypeRestriction.infer; export const VideoSchema = type({ FlagInterlaced: FlagInterlacedRestriction.default(0), @@ -534,10 +518,8 @@ export enum EmphasisRestrictionEnum { PHONO_LONDON = 15, // Phono NARTB PHONO_NARTB = 16, -} -export const EmphasisRestriction = type( - '0 | 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16' -); +}; +export const EmphasisRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16'); export type EmphasisRestrictionType = typeof EmphasisRestriction.infer; export const AudioSchema = type({ @@ -558,10 +540,9 @@ export enum TrackPlaneTypeRestrictionEnum { RIGHT_EYE = 1, // background BACKGROUND = 2, -} +}; export const TrackPlaneTypeRestriction = type('0 | 1 | 2'); -export type TrackPlaneTypeRestrictionType = - typeof TrackPlaneTypeRestriction.infer; +export type TrackPlaneTypeRestrictionType = typeof TrackPlaneTypeRestriction.infer; export const TrackPlaneSchema = type({ TrackPlaneUID: type.number, @@ -571,13 +552,13 @@ export const TrackPlaneSchema = type({ export type TrackPlaneType = typeof TrackPlaneSchema.infer; export const TrackCombinePlanesSchema = type({ - TrackPlane: TrackPlaneSchema.array(), + TrackPlane: TrackPlaneSchema.array().atLeastLength(1), }); export type TrackCombinePlanesType = typeof TrackCombinePlanesSchema.infer; export const TrackJoinBlocksSchema = type({ - TrackJoinUID: type.number.array(), + TrackJoinUID: type.number.array().atLeastLength(1), }); export type TrackJoinBlocksType = typeof TrackJoinBlocksSchema.infer; @@ -598,10 +579,9 @@ export enum ContentCompAlgoRestrictionEnum { LZO1X = 2, // Header Stripping HEADER_STRIPPING = 3, -} +}; export const ContentCompAlgoRestriction = type('0 | 1 | 2 | 3'); -export type ContentCompAlgoRestrictionType = - typeof ContentCompAlgoRestriction.infer; +export type ContentCompAlgoRestrictionType = typeof ContentCompAlgoRestriction.infer; export const ContentCompressionSchema = type({ ContentCompAlgo: ContentCompAlgoRestriction.default(0), @@ -615,17 +595,15 @@ export enum AESSettingsCipherModeRestrictionEnum { AES_CTR = 1, // AES-CBC AES_CBC = 2, -} +}; export const AESSettingsCipherModeRestriction = type('1 | 2'); -export type AESSettingsCipherModeRestrictionType = - typeof AESSettingsCipherModeRestriction.infer; +export type AESSettingsCipherModeRestrictionType = typeof AESSettingsCipherModeRestriction.infer; export const ContentEncAESSettingsSchema = type({ AESSettingsCipherMode: AESSettingsCipherModeRestriction, }); -export type ContentEncAESSettingsType = - typeof ContentEncAESSettingsSchema.infer; +export type ContentEncAESSettingsType = typeof ContentEncAESSettingsSchema.infer; export enum ContentEncAlgoRestrictionEnum { // Not encrypted @@ -640,20 +618,18 @@ export enum ContentEncAlgoRestrictionEnum { BLOWFISH = 4, // AES AES = 5, -} +}; export const ContentEncAlgoRestriction = type('0 | 1 | 2 | 3 | 4 | 5'); -export type ContentEncAlgoRestrictionType = - typeof ContentEncAlgoRestriction.infer; +export type ContentEncAlgoRestrictionType = typeof ContentEncAlgoRestriction.infer; export enum ContentSigAlgoRestrictionEnum { // Not signed NOT_SIGNED = 0, // RSA RSA = 1, -} +}; export const ContentSigAlgoRestriction = type('0 | 1'); -export type ContentSigAlgoRestrictionType = - typeof ContentSigAlgoRestriction.infer; +export type ContentSigAlgoRestrictionType = typeof ContentSigAlgoRestriction.infer; export enum ContentSigHashAlgoRestrictionEnum { // Not signed @@ -662,10 +638,9 @@ export enum ContentSigHashAlgoRestrictionEnum { SHA1_160 = 1, // MD5 MD5 = 2, -} +}; export const ContentSigHashAlgoRestriction = type('0 | 1 | 2'); -export type ContentSigHashAlgoRestrictionType = - typeof ContentSigHashAlgoRestriction.infer; +export type ContentSigHashAlgoRestrictionType = typeof ContentSigHashAlgoRestriction.infer; export const ContentEncryptionSchema = type({ ContentEncAlgo: ContentEncAlgoRestriction.default(0), @@ -686,20 +661,18 @@ export enum ContentEncodingScopeRestrictionEnum { PRIVATE = 2, // Next NEXT = 4, -} +}; export const ContentEncodingScopeRestriction = type('1 | 2 | 4'); -export type ContentEncodingScopeRestrictionType = - typeof ContentEncodingScopeRestriction.infer; +export type ContentEncodingScopeRestrictionType = typeof ContentEncodingScopeRestriction.infer; export enum ContentEncodingTypeRestrictionEnum { // Compression COMPRESSION = 0, // Encryption ENCRYPTION = 1, -} +}; export const ContentEncodingTypeRestriction = type('0 | 1'); -export type ContentEncodingTypeRestrictionType = - typeof ContentEncodingTypeRestriction.infer; +export type ContentEncodingTypeRestrictionType = typeof ContentEncodingTypeRestriction.infer; export const ContentEncodingSchema = type({ ContentEncodingOrder: type.number.default(0), @@ -712,7 +685,7 @@ export const ContentEncodingSchema = type({ export type ContentEncodingType = typeof ContentEncodingSchema.infer; export const ContentEncodingsSchema = type({ - ContentEncoding: ContentEncodingSchema.array(), + ContentEncoding: ContentEncodingSchema.array().atLeastLength(1), }); export type ContentEncodingsType = typeof ContentEncodingsSchema.infer; @@ -734,7 +707,7 @@ export enum TrackTypeRestrictionEnum { CONTROL = 32, // metadata METADATA = 33, -} +}; export const TrackTypeRestriction = type('1 | 2 | 3 | 16 | 17 | 18 | 32 | 33'); export type TrackTypeRestrictionType = typeof TrackTypeRestriction.infer; @@ -760,7 +733,7 @@ export const TrackEntrySchema = type({ MaxBlockAdditionID: type.number.default(0), BlockAdditionMapping: BlockAdditionMappingSchema.array().optional(), Name: type.string.optional(), - Language: type.string.default('eng'), + Language: type.string.default("eng"), LanguageBCP47: type.string.optional(), CodecID: type.string, CodecPrivate: BinarySchema.optional(), @@ -788,7 +761,7 @@ export const TrackEntrySchema = type({ export type TrackEntryType = typeof TrackEntrySchema.infer; export const TracksSchema = type({ - TrackEntry: TrackEntrySchema.array(), + TrackEntry: TrackEntrySchema.array().atLeastLength(1), }); export type TracksType = typeof TracksSchema.infer; @@ -816,13 +789,13 @@ export type CueTrackPositionsType = typeof CueTrackPositionsSchema.infer; export const CuePointSchema = type({ CueTime: type.number, - CueTrackPositions: CueTrackPositionsSchema.array(), + CueTrackPositions: CueTrackPositionsSchema.array().atLeastLength(1), }); export type CuePointType = typeof CuePointSchema.infer; export const CuesSchema = type({ - CuePoint: CuePointSchema.array(), + CuePoint: CuePointSchema.array().atLeastLength(1), }); export type CuesType = typeof CuesSchema.infer; @@ -841,7 +814,7 @@ export const AttachedFileSchema = type({ export type AttachedFileType = typeof AttachedFileSchema.infer; export const AttachmentsSchema = type({ - AttachedFile: AttachedFileSchema.array(), + AttachedFile: AttachedFileSchema.array().atLeastLength(1), }); export type AttachmentsType = typeof AttachmentsSchema.infer; @@ -859,38 +832,39 @@ export const EditionEntrySchema = type({ EditionFlagDefault: type.number.default(0), EditionFlagOrdered: type.number.default(0), EditionDisplay: EditionDisplaySchema.array().optional(), + }); export type EditionEntryType = typeof EditionEntrySchema.infer; export const ChaptersSchema = type({ - EditionEntry: EditionEntrySchema.array(), + EditionEntry: EditionEntrySchema.array().atLeastLength(1), }); export type ChaptersType = typeof ChaptersSchema.infer; export const TagTrackUIDSchema = match({ - 'number[]': (v) => (v.length > 0 ? v : [0]), - undefined: () => [0], - default: 'assert', + "number[]": v => v.length > 0 ? v : [0], + "undefined": () => [0], + default: "assert" }); export const TagEditionUIDSchema = match({ - 'number[]': (v) => (v.length > 0 ? v : [0]), - undefined: () => [0], - default: 'assert', + "number[]": v => v.length > 0 ? v : [0], + "undefined": () => [0], + default: "assert" }); export const TagChapterUIDSchema = match({ - 'number[]': (v) => (v.length > 0 ? v : [0]), - undefined: () => [0], - default: 'assert', + "number[]": v => v.length > 0 ? v : [0], + "undefined": () => [0], + default: "assert" }); export const TagAttachmentUIDSchema = match({ - 'number[]': (v) => (v.length > 0 ? v : [0]), - undefined: () => [0], - default: 'assert', + "number[]": v => v.length > 0 ? v : [0], + "undefined": () => [0], + default: "assert" }); export enum TargetTypeValueRestrictionEnum { @@ -908,60 +882,55 @@ export enum TargetTypeValueRestrictionEnum { EDITION_ISSUE_VOLUME_OPUS_SEASON_SEQUEL = 60, // COLLECTION COLLECTION = 70, -} -export const TargetTypeValueRestriction = type( - '10 | 20 | 30 | 40 | 50 | 60 | 70' -); -export type TargetTypeValueRestrictionType = - typeof TargetTypeValueRestriction.infer; +}; +export const TargetTypeValueRestriction = type('10 | 20 | 30 | 40 | 50 | 60 | 70'); +export type TargetTypeValueRestrictionType = typeof TargetTypeValueRestriction.infer; export enum TargetTypeRestrictionEnum { // TargetTypeValue 70 - COLLECTION = 'COLLECTION', + COLLECTION = "COLLECTION", // TargetTypeValue 60 - EDITION = 'EDITION', + EDITION = "EDITION", // TargetTypeValue 60 - ISSUE = 'ISSUE', + ISSUE = "ISSUE", // TargetTypeValue 60 - VOLUME = 'VOLUME', + VOLUME = "VOLUME", // TargetTypeValue 60 - OPUS = 'OPUS', + OPUS = "OPUS", // TargetTypeValue 60 - SEASON = 'SEASON', + SEASON = "SEASON", // TargetTypeValue 60 - SEQUEL = 'SEQUEL', + SEQUEL = "SEQUEL", // TargetTypeValue 50 - ALBUM = 'ALBUM', + ALBUM = "ALBUM", // TargetTypeValue 50 - OPERA = 'OPERA', + OPERA = "OPERA", // TargetTypeValue 50 - CONCERT = 'CONCERT', + CONCERT = "CONCERT", // TargetTypeValue 50 - MOVIE = 'MOVIE', + MOVIE = "MOVIE", // TargetTypeValue 50 - EPISODE = 'EPISODE', + EPISODE = "EPISODE", // TargetTypeValue 40 - PART = 'PART', + PART = "PART", // TargetTypeValue 40 - SESSION = 'SESSION', + SESSION = "SESSION", // TargetTypeValue 30 - TRACK = 'TRACK', + TRACK = "TRACK", // TargetTypeValue 30 - SONG = 'SONG', + SONG = "SONG", // TargetTypeValue 30 - CHAPTER = 'CHAPTER', + CHAPTER = "CHAPTER", // TargetTypeValue 20 - SUBTRACK = 'SUBTRACK', + SUBTRACK = "SUBTRACK", // TargetTypeValue 20 - MOVEMENT = 'MOVEMENT', + MOVEMENT = "MOVEMENT", // TargetTypeValue 20 - SCENE = 'SCENE', + SCENE = "SCENE", // TargetTypeValue 10 - SHOT = 'SHOT', -} -export const TargetTypeRestriction = type( - '"COLLECTION" | "EDITION" | "ISSUE" | "VOLUME" | "OPUS" | "SEASON" | "SEQUEL" | "ALBUM" | "OPERA" | "CONCERT" | "MOVIE" | "EPISODE" | "PART" | "SESSION" | "TRACK" | "SONG" | "CHAPTER" | "SUBTRACK" | "MOVEMENT" | "SCENE" | "SHOT"' -); + SHOT = "SHOT", +}; +export const TargetTypeRestriction = type('"COLLECTION" | "EDITION" | "ISSUE" | "VOLUME" | "OPUS" | "SEASON" | "SEQUEL" | "ALBUM" | "OPERA" | "CONCERT" | "MOVIE" | "EPISODE" | "PART" | "SESSION" | "TRACK" | "SONG" | "CHAPTER" | "SUBTRACK" | "MOVEMENT" | "SCENE" | "SHOT"'); export type TargetTypeRestrictionType = typeof TargetTypeRestriction.infer; export const TargetsSchema = type({ @@ -977,12 +946,13 @@ export type TargetsType = typeof TargetsSchema.infer; export const TagSchema = type({ Targets: TargetsSchema, + }); export type TagType = typeof TagSchema.infer; export const TagsSchema = type({ - Tag: TagSchema.array(), + Tag: TagSchema.array().atLeastLength(1), }); export type TagsType = typeof TagsSchema.infer; @@ -1037,5 +1007,5 @@ export const IdMultiSet = new Set([ EbmlTagIdEnum.Tag, EbmlTagIdEnum.SeekHead, EbmlTagIdEnum.Cluster, - EbmlTagIdEnum.Tags, -]); + EbmlTagIdEnum.Tags +]) \ No newline at end of file diff --git a/apps/playground/src/media/mkv/util.ts b/apps/playground/src/media/mkv/util.ts index 2b5c10e..9d445e4 100644 --- a/apps/playground/src/media/mkv/util.ts +++ b/apps/playground/src/media/mkv/util.ts @@ -2,11 +2,7 @@ import type { Type } from 'arktype'; import { EbmlElementType, EbmlTagIdEnum, type EbmlTagType } from 'konoebml'; import { IdMultiSet } from './schema'; -export type InferType = T extends Type ? U : never; - -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 type InferType> = T['infer']; export type PredicateIdExtract = Extract; @@ -31,13 +27,13 @@ export function isTagPos< pos === '*' || pos === tag.position; } -export function convertEbmlTagToModelShape(tag: EbmlTagType) { +export function convertEbmlTagToComponent (tag: EbmlTagType) { if (tag.type === EbmlElementType.Master) { const obj: Record = {}; const children = tag.children; for (const c of children) { const name = EbmlTagIdEnum[c.id]; - const converted = convertEbmlTagToModelShape(c); + const converted = convertEbmlTagToComponent(c); if (IdMultiSet.has(c.id)) { if (obj[name]) { obj[name].push(converted); @@ -48,6 +44,7 @@ export function convertEbmlTagToModelShape(tag: EbmlTagType) { obj[name] = converted; } } + return obj; } if (tag.id === EbmlTagIdEnum.SimpleBlock || tag.id === EbmlTagIdEnum.Block) { return tag; diff --git a/scripts/codegen-mkv.ts b/scripts/codegen-mkv.ts index 956a484..792e178 100644 --- a/scripts/codegen-mkv.ts +++ b/scripts/codegen-mkv.ts @@ -390,6 +390,9 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) { : meta.primitive(v.name); if (v.maxOccurs !== 1) { expr = `${expr}.array()`; + if (v.maxOccurs !== 1 && v.minOccurs === 1 && !v.default) { + expr = `${expr}.atLeastLength(1)` + } idMulti.add(v.name); } if (v.default) {