diff --git a/Cargo.lock b/Cargo.lock index e75d3e8..07324ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,24 +2,75 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + [[package]] name = "bytemuck" version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +[[package]] +name = "cc" +version = "1.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +dependencies = [ + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -27,12 +78,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "demuxing" -version = "0.1.0" +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ - "symphonia-format-mkv", + "glob", + "libc", + "libloading", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -42,18 +103,180 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ffmpeg-sys-next" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc3234d0a4b2f7d083699d0860c6c9dd83713908771b60f94a96f8704adfe45" +dependencies = [ + "bindgen", + "cc", + "libc", + "num_cpus", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "konoplayer-codecs" +version = "0.1.0" +dependencies = [ + "ffmpeg-sys-next", +] + +[[package]] +name = "konoplayer-demuxing" +version = "0.1.0" +dependencies = [ + "symphonia-format-mkv", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets", +] + [[package]] name = "log" version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "symphonia-core" version = "0.5.4" @@ -61,7 +284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" dependencies = [ "arrayvec", - "bitflags", + "bitflags 1.3.2", "bytemuck", "lazy_static", "log", @@ -101,3 +324,90 @@ dependencies = [ "symphonia-core", "symphonia-metadata", ] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index caef8b4..93842e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["packages/demuxing"] -resolver = "2" +members = ["packages/demuxing", "packages/codecs"] +resolver = "3" diff --git a/apps/mock/.gitignore b/apps/mock/.gitignore index 1eed71a..bc0cfa4 100644 --- a/apps/mock/.gitignore +++ b/apps/mock/.gitignore @@ -1 +1,2 @@ -public/video-sample/huge/* \ No newline at end of file +public/video/huge/* +!public/video/huge/.gitkeep \ No newline at end of file diff --git a/apps/mock/public/video-sample/audiosample.webm b/apps/mock/public/video/audiosample.webm similarity index 100% rename from apps/mock/public/video-sample/audiosample.webm rename to apps/mock/public/video/audiosample.webm diff --git a/apps/mock/public/video/huge/.gitkeep b/apps/mock/public/video/huge/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/mock/public/video-sample/test.webm b/apps/mock/public/video/test.webm similarity index 100% rename from apps/mock/public/video-sample/test.webm rename to apps/mock/public/video/test.webm diff --git a/apps/mock/public/video-sample/video-webm-codecs-avc1-42E01E.webm b/apps/mock/public/video/video-webm-codecs-avc1-42E01E.webm similarity index 100% rename from apps/mock/public/video-sample/video-webm-codecs-avc1-42E01E.webm rename to apps/mock/public/video/video-webm-codecs-avc1-42E01E.webm diff --git a/apps/mock/public/video-sample/video-webm-codecs-vp8.webm b/apps/mock/public/video/video-webm-codecs-vp8.webm similarity index 100% rename from apps/mock/public/video-sample/video-webm-codecs-vp8.webm rename to apps/mock/public/video/video-webm-codecs-vp8.webm diff --git a/apps/playground/package.json b/apps/playground/package.json index 452f9c1..e8b69bc 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -9,7 +9,7 @@ "preview": "rsbuild preview" }, "dependencies": { - "konoebml": "0.1.1", + "konoebml": "0.1.2-rc.5", "lit": "^3.2.1" }, "devDependencies": { diff --git a/apps/playground/src/fetch.ts b/apps/playground/src/fetch.ts index 4570150..4ff4a9f 100644 --- a/apps/playground/src/fetch.ts +++ b/apps/playground/src/fetch.ts @@ -5,11 +5,17 @@ export interface RangedStream { totalSize?: number; } -export async function createRangedStream( - url: string, +export interface CreateRangedStreamOptions { + url: string; + byteStart?: number; + byteEnd?: number; +} + +export async function createRangedStream({ + url, byteStart = 0, - byteEnd?: number -) { + byteEnd, +}: CreateRangedStreamOptions) { const controller = new AbortController(); const signal = controller.signal; const headers = new Headers(); diff --git a/apps/playground/src/index.html b/apps/playground/src/index.html index 8a54361..a31a7ed 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/mkv/model.ts b/apps/playground/src/media/mkv/model.ts index 42ce80a..b2ea239 100644 --- a/apps/playground/src/media/mkv/model.ts +++ b/apps/playground/src/media/mkv/model.ts @@ -8,13 +8,15 @@ import { type EbmlSegmentTagType, EbmlTagIdEnum, EbmlTagPosition, + EbmlTagsTagType, + 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 { convertEbmlTagToComponent, type InferType } from './util'; +import { isEqual, maxBy } from 'lodash-es'; +import { ArkErrors, type Type } from 'arktype'; import { ClusterSchema, type ClusterType, @@ -25,13 +27,16 @@ import { type InfoType, SeekHeadSchema, type SeekHeadType, + TagSchema, + TagType, TrackEntrySchema, - type TrackEntryType + type TrackEntryType, } from './schema'; 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; @@ -42,7 +47,7 @@ export class SegmentSystem { seek: SeekSystem; info: InfoSystem; track: TrackSystem; - + tag: TagSystem; constructor(startNode: EbmlSegmentTagType) { this.startTag = startNode; @@ -51,23 +56,25 @@ export class SegmentSystem { this.seek = new SeekSystem(this); this.info = new InfoSystem(this); this.track = new TrackSystem(this); + this.tag = new TagSystem(this); } - get dataStartOffset() { + get contentStartOffset() { return this.startTag.startOffset + this.startTag.headerLength; } - get startOffset () { + get startOffset() { return this.startTag.startOffset; } - completeHeads () { + 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); + const tagsTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_TAGS); if (cuesTag?.id === EbmlTagIdEnum.Cues) { - this.cue.prepareCuesWithTag(cuesTag) + this.cue.prepareCuesWithTag(cuesTag); } if (infoTag?.id === EbmlTagIdEnum.Info) { this.info.prepareWithInfoTag(infoTag); @@ -75,11 +82,14 @@ export class SegmentSystem { if (tracksTag?.id === EbmlTagIdEnum.Tracks) { this.track.prepareTracksWithTag(tracksTag); } + if (tagsTag?.id === EbmlTagIdEnum.Tags) { + this.tag.prepareTagsWIthTag(tagsTag); + } return this; } - scanHead (tag: EbmlTagType) { + scanHead(tag: EbmlTagType) { if ( tag.id === EbmlTagIdEnum.SeekHead && tag.position === EbmlTagPosition.End @@ -92,30 +102,56 @@ export class SegmentSystem { } } -export class SegmentComponentSystemTrait> { +export type SegmentComponent = T & { + get segment(): SegmentSystem; +}; + +export function withSegment( + component: T, + segment: SegmentSystem +): SegmentComponent { + const component_ = component as T & { segment: SegmentSystem }; + component_.segment = segment; + return component_; +} + +export class SegmentComponentSystemTrait< + E extends EbmlMasterTagType, + S extends Type, +> { segment: SegmentSystem; get schema(): S { - throw new Error("unimplemented!") + throw new Error('unimplemented!'); } constructor(segment: SegmentSystem) { this.segment = segment; } - componentFromTag(tag: E): InferType { + componentFromTag(tag: E): SegmentComponent> { const extracted = convertEbmlTagToComponent(tag); - const result = this.schema(extracted); + const result = this.schema(extracted) as + | (InferType & { segment: SegmentSystem }) + | ArkErrors; if (result instanceof ArkErrors) { const errors = result; - console.error('Parse component from tag error:', tag.toDebugRecord(), errors.flatProblemsByPath) + console.error( + 'Parse component from tag error:', + tag.toDebugRecord(), + errors.flatProblemsByPath + ); throw errors; } - return result as InferType + result.segment = this.segment; + return result; } } -export class SeekSystem extends SegmentComponentSystemTrait { +export class SeekSystem extends SegmentComponentSystemTrait< + EbmlSeekHeadTagType, + typeof SeekHeadSchema +> { override get schema() { return SeekHeadSchema; } @@ -123,25 +159,21 @@ export class SeekSystem extends SegmentComponentSystemTrait = new Map(); - memoTag (tag: EbmlTagType) { + memoTag(tag: EbmlTagType) { this.offsetToTagMemo.set(tag.startOffset, tag); } - addSeekHeadTag (tag: EbmlSeekHeadTagType) { + addSeekHeadTag(tag: EbmlSeekHeadTagType) { const seekHead = this.componentFromTag(tag); this.seekHeads.push(seekHead); return seekHead; } - offsetFromSeekPosition (position: number): number { - return position + this.segment.startOffset; + offsetFromSeekPosition(position: number): number { + return position + this.segment.contentStartOffset; } - offsetFromSeekDataPosition (position: number) : number { - return position + this.segment.dataStartOffset; - } - - seekTagByStartOffset ( + seekTagByStartOffset( startOffset: number | undefined ): EbmlTagType | undefined { return startOffset! >= 0 @@ -150,60 +182,80 @@ export class SeekSystem extends SegmentComponentSystemTrait isEqual(c.SeekID, seekId)) - ?.SeekPosition; - return seekPosition! >= 0 ? this.offsetFromSeekPosition(seekPosition!) : 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) - ); + return this.seekTagByStartOffset(this.seekOffsetBySeekId(seekId)); } } -export class InfoSystem extends SegmentComponentSystemTrait { +export class InfoSystem extends SegmentComponentSystemTrait< + EbmlInfoTagType, + typeof InfoSchema +> { override get schema() { return InfoSchema; } - info!: InfoType; + info!: SegmentComponent; - prepareWithInfoTag (tag: EbmlInfoTagType) { + prepareWithInfoTag(tag: EbmlInfoTagType) { this.info = this.componentFromTag(tag); return this; } } -export class ClusterSystem extends SegmentComponentSystemTrait { +export class ClusterSystem extends SegmentComponentSystemTrait< + EbmlClusterTagType, + typeof ClusterSchema +> { override get schema() { - return ClusterSchema + return ClusterSchema; } - clustersBuffer: ClusterType[] = []; + clustersBuffer: SegmentComponent[] = []; - addClusterWithTag (tag: EbmlClusterTagType): ClusterType { + addClusterWithTag(tag: EbmlClusterTagType) { const cluster = this.componentFromTag(tag); this.clustersBuffer.push(cluster); return cluster; } } -export class TrackSystem extends SegmentComponentSystemTrait { +export class TrackSystem extends SegmentComponentSystemTrait< + EbmlTrackEntryTagType, + typeof TrackEntrySchema +> { override get schema() { return TrackEntrySchema; } - tracks = new Map(); + tracks: SegmentComponent[] = []; - 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); - } - } + getTrackEntry({ + priority = (track) => + (Number(!!track.FlagForced) << 4) + Number(!!track.FlagDefault), + predicate = (track) => track.FlagEnabled !== 0, + }: { + priority?: (v: SegmentComponent) => number; + predicate?: (v: SegmentComponent) => boolean; + }) { + 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; } } @@ -212,16 +264,15 @@ export class CueSystem extends SegmentComponentSystemTrait< EbmlCuePointTagType, typeof CuePointSchema > { - override get schema () { - return CuePointSchema - }; + override get schema() { + return CuePointSchema; + } - cues: CuePointType[] = []; + cues: SegmentComponent[] = []; - - prepareCuesWithTag (tag: EbmlCuesTagType) { + prepareCuesWithTag(tag: EbmlCuesTagType) { this.cues = tag.children - .filter(c => c.id === EbmlTagIdEnum.CuePoint) + .filter((c) => c.id === EbmlTagIdEnum.CuePoint) .map(this.componentFromTag.bind(this)); return this; } @@ -259,24 +310,54 @@ export class CueSystem extends SegmentComponentSystemTrait< const before = cues[right]; const after = cues[left]; - return Math.abs(before.CueTime - seekTime) < - Math.abs(after.CueTime - seekTime) + return Math.abs((before.CueTime as number) - seekTime) < + Math.abs((after.CueTime as number) - seekTime) ? before : after; } - getCueTrackPositions (cuePoint: CuePointType, track?: number): CueTrackPositionsType { + getCueTrackPositions( + cuePoint: CuePointType, + track?: number + ): CueTrackPositionsType { let cueTrackPositions: CueTrackPositionsType | undefined; if (track! >= 0) { - cueTrackPositions = cuePoint.CueTrackPositions.find(c => c.CueTrack === track); + cueTrackPositions = cuePoint.CueTrackPositions.find( + (c) => c.CueTrack === track + ); } - if (!cueTrackPositions) { - cueTrackPositions = maxBy(cuePoint.CueTrackPositions, c => c.CueClusterPosition)!; - } - return cueTrackPositions; + if (!cueTrackPositions) { + cueTrackPositions = maxBy( + cuePoint.CueTrackPositions, + (c) => c.CueClusterPosition + )!; + } + return cueTrackPositions; } - get prepared (): boolean { + get prepared(): boolean { return this.cues.length > 0; } } + +export class TagSystem extends SegmentComponentSystemTrait< + EbmlTagTagType, + typeof TagSchema +> { + override get schema() { + return TagSchema; + } + + tags: SegmentComponent[] = []; + + prepareWithTagsTag(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; + } +} diff --git a/apps/playground/src/media/mkv/reactive.ts b/apps/playground/src/media/mkv/reactive.ts index 9b630a7..de0d117 100644 --- a/apps/playground/src/media/mkv/reactive.ts +++ b/apps/playground/src/media/mkv/reactive.ts @@ -25,23 +25,32 @@ import { takeUntil, withLatestFrom, } from 'rxjs'; -import { createRangedStream } from '@/fetch'; -import { SegmentSystem, SEEK_ID_KAX_CUES, type CueSystem } from './model'; -import { isTagIdPos } from './util'; -import type { ClusterType } from "./schema"; +import { createRangedStream, type CreateRangedStreamOptions } from '@/fetch'; +import { + SegmentSystem, + SEEK_ID_KAX_CUES, + type CueSystem, + type SegmentComponent, + SEEK_ID_KAX_TAGS, +} from './model'; +import { isTagIdPos, waitTick } from './util'; +import type { ClusterType } from './schema'; -export function createRangedEbmlStream( - url: string, +export interface CreateRangedEbmlStreamOptions + extends CreateRangedStreamOptions {} + +export function createRangedEbmlStream({ + url, byteStart = 0, - byteEnd?: number -): Observable<{ + byteEnd, +}: CreateRangedEbmlStreamOptions): Observable<{ ebml$: Observable; totalSize?: number; response: Response; body: ReadableStream; controller: AbortController; }> { - const stream$ = from(createRangedStream(url, byteStart, byteEnd)); + const stream$ = from(createRangedStream({ url, byteStart, byteEnd })); return stream$.pipe( switchMap(({ controller, body, totalSize, response }) => { @@ -52,13 +61,20 @@ export function createRangedEbmlStream( new EbmlStreamDecoder({ streamStartOffset: byteStart, collectChild: (child) => child.id !== EbmlTagIdEnum.Cluster, + backpressure: { + eventLoop: waitTick, + }, }) ) .pipeTo( new WritableStream({ - write: (tag) => subscriber.next(tag), + write: async (tag) => { + await waitTick(); + subscriber.next(tag); + }, close: () => { if (!requestCompleted) { + requestCompleted = true; subscriber.complete(); } }, @@ -68,6 +84,7 @@ export function createRangedEbmlStream( if (requestCompleted && error?.name === 'AbortError') { return; } + requestCompleted = true; subscriber.error(error); }); @@ -103,8 +120,19 @@ export function createRangedEbmlStream( }) ); } -export function createEbmlController(src: string) { - const request$ = createRangedEbmlStream(src, 0); + +export interface CreateEbmlControllerOptions + extends Omit {} + +export function createEbmlController({ + url, + ...options +}: CreateEbmlControllerOptions) { + const request$ = createRangedEbmlStream({ + ...options, + url, + byteStart: 0, + }); const controller$ = request$.pipe( map(({ totalSize, ebml$, response, controller }) => { @@ -115,7 +143,7 @@ export function createEbmlController(src: string) { ); console.debug( - `stream of video "${src}" created, total size is ${totalSize ?? 'unknown'}` + `stream of video "${url}" created, total size is ${totalSize ?? 'unknown'}` ); const segmentStart$ = ebml$.pipe( @@ -123,18 +151,16 @@ export function createEbmlController(src: string) { filter((tag) => tag.id === EbmlTagIdEnum.Segment) ); + /** + * 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); 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(clusterSystem.addClusterWithTag.bind(clusterSystem)) - ); - const segmentEnd$ = ebml$.pipe( filter(isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End)), filter((tag) => tag.id === EbmlTagIdEnum.Segment), @@ -170,9 +196,14 @@ export function createEbmlController(src: string) { if (cueSystem.prepared) { return EMPTY; } - const remoteCuesTagStartOffset = seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_CUES); + const remoteCuesTagStartOffset = + seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_CUES); if (remoteCuesTagStartOffset! >= 0) { - return createRangedEbmlStream(src, remoteCuesTagStartOffset).pipe( + return createRangedEbmlStream({ + ...options, + url, + byteStart: remoteCuesTagStartOffset, + }).pipe( switchMap((req) => req.ebml$), filter(isTagIdPos(EbmlTagIdEnum.Cues, EbmlTagPosition.End)), withLatestFrom(withMeta$), @@ -189,7 +220,42 @@ export function createEbmlController(src: string) { ); const withLocalCues$ = withMeta$.pipe( - switchMap((s) => s.cue.prepared ? of(s) : EMPTY), + 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.prepareWithTagsTag(tags); + return withMeta; + }) + ); + } + return EMPTY; + }), + take(1), + shareReplay(1) + ); + + const withLocalTags$ = withMeta$.pipe( + switchMap((s) => (s.tag.prepared ? of(s) : EMPTY)), shareReplay(1) ); @@ -202,24 +268,33 @@ export function createEbmlController(src: string) { switchMap((empty) => (empty ? withMeta$ : EMPTY)) ); - const seekWithoutCues = (seekTime: number): Observable => { - const cluster$ = continuousReusedCluster$.pipe( - isEmpty(), - switchMap((empty) => { - return empty - ? clusterStart$.pipe( - switchMap((startTag) => - createRangedEbmlStream(src, startTag.startOffset) - ), - switchMap((req) => req.ebml$), - filter( - isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End) - ), - map((tag) => clusterSystem.addClusterWithTag(tag)) - ) - : continuousReusedCluster$; - }) + const withTags$ = merge(withLocalTags$, withRemoteTags$).pipe( + take(1) + ); + + const withoutTags$ = withTags$.pipe( + isEmpty(), + switchMap((empty) => (empty ? withMeta$ : EMPTY)) + ); + + const seekWithoutCues = ( + seekTime: number + ): Observable> => { + const request$ = clusterStart$.pipe( + switchMap((startTag) => + createRangedEbmlStream({ + ...options, + url, + byteStart: startTag.startOffset, + }) + ) ); + const cluster$ = request$.pipe( + switchMap((req) => req.ebml$), + filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)), + map((tag) => clusterSystem.addClusterWithTag(tag)) + ); + if (seekTime === 0) { return cluster$; } @@ -228,12 +303,12 @@ export function createEbmlController(src: string) { scan( (prev, curr) => [prev?.[1], curr] as [ - ClusterType | undefined, - ClusterType | undefined, + SegmentComponent | undefined, + SegmentComponent | undefined, ], [undefined, undefined] as [ - ClusterType | undefined, - ClusterType | undefined, + SegmentComponent | undefined, + SegmentComponent | undefined, ] ), filter((c) => c[1]?.Timestamp! > seekTime), @@ -242,30 +317,36 @@ export function createEbmlController(src: string) { }; const seekWithCues = ( - cues: CueSystem, + cueSystem: CueSystem, seekTime: number - ): Observable => { + ): Observable> => { if (seekTime === 0) { return seekWithoutCues(seekTime); } - const cuePoint = cues.findClosestCue(seekTime); + const cuePoint = cueSystem.findClosestCue(seekTime); if (!cuePoint) { return seekWithoutCues(seekTime); } - return createRangedEbmlStream( - src, - seekSystem.offsetFromSeekDataPosition(cues.getCueTrackPositions(cuePoint).CueClusterPosition) - ).pipe( + 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 => { + const seek = ( + seekTime: number + ): Observable> => { if (seekTime === 0) { const subscription = merge(withCues$, withoutCues$).subscribe(); @@ -277,11 +358,7 @@ export function createEbmlController(src: string) { ); } return merge( - withCues$.pipe( - switchMap((s) => - seekWithCues(s.cue, seekTime) - ) - ), + withCues$.pipe(switchMap((s) => seekWithCues(s.cue, seekTime))), withoutCues$.pipe(switchMap((_) => seekWithoutCues(seekTime))) ); }; @@ -294,6 +371,8 @@ export function createEbmlController(src: string) { withMeta$, withCues$, withoutCues$, + withTags$, + withoutTags$, seekWithCues, seekWithoutCues, seek, @@ -309,7 +388,8 @@ export function createEbmlController(src: string) { controller, response, }; - }) + }), + shareReplay(1) ); return { diff --git a/apps/playground/src/media/mkv/schema.ts b/apps/playground/src/media/mkv/schema.ts index 1a84ffd..e75d524 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); @@ -7,19 +7,19 @@ export const BlockSchema = type.instanceOf(EbmlBlockTag); export const DocTypeExtensionSchema = type({ DocTypeExtensionName: type.string, - DocTypeExtensionVersion: type.number, + DocTypeExtensionVersion: type.number.or(type.bigint), }); export type DocTypeExtensionType = typeof DocTypeExtensionSchema.infer; export const EBMLSchema = type({ - EBMLVersion: type.number.default(1), - EBMLReadVersion: type.number.default(1), - EBMLMaxIDLength: type.number.default(4), - EBMLMaxSizeLength: type.number.default(8), + EBMLVersion: type.number.or(type.bigint).default(1), + EBMLReadVersion: type.number.or(type.bigint).default(1), + EBMLMaxIDLength: type.number.or(type.bigint).default(4), + EBMLMaxSizeLength: type.number.or(type.bigint).default(8), DocType: type.string, - DocTypeVersion: type.number.default(1), - DocTypeReadVersion: type.number.default(1), + DocTypeVersion: type.number.or(type.bigint).default(1), + DocTypeReadVersion: type.number.or(type.bigint).default(1), DocTypeExtension: DocTypeExtensionSchema.array().optional(), }); @@ -27,7 +27,7 @@ export type EBMLType = typeof EBMLSchema.infer; export const SeekSchema = type({ SeekID: BinarySchema, - SeekPosition: type.number, + SeekPosition: type.number.or(type.bigint), }); export type SeekType = typeof SeekSchema.infer; @@ -40,8 +40,8 @@ export type SeekHeadType = typeof SeekHeadSchema.infer; export const ChapterTranslateSchema = type({ ChapterTranslateID: BinarySchema, - ChapterTranslateCodec: type.number, - ChapterTranslateEditionUID: type.number.array().optional(), + ChapterTranslateCodec: type.number.or(type.bigint), + ChapterTranslateEditionUID: type.number.or(type.bigint).array().optional(), }); export type ChapterTranslateType = typeof ChapterTranslateSchema.infer; @@ -55,7 +55,7 @@ export const InfoSchema = type({ NextFilename: type.string.optional(), SegmentFamily: BinarySchema.array().optional(), ChapterTranslate: ChapterTranslateSchema.array().optional(), - TimestampScale: type.number.default(1000000), + TimestampScale: type.number.or(type.bigint).default(1000000), Duration: type.number.optional(), DateUTC: BinarySchema.optional(), Title: type.string.optional(), @@ -66,14 +66,14 @@ export const InfoSchema = type({ export type InfoType = typeof InfoSchema.infer; export const SilentTracksSchema = type({ - SilentTrackNumber: type.number.array().optional(), + SilentTrackNumber: type.number.or(type.bigint).array().optional(), }); export type SilentTracksType = typeof SilentTracksSchema.infer; export const BlockMoreSchema = type({ BlockAdditional: BinarySchema, - BlockAddID: type.number.default(1), + BlockAddID: type.number.or(type.bigint).default(1), }); export type BlockMoreType = typeof BlockMoreSchema.infer; @@ -85,11 +85,11 @@ export const BlockAdditionsSchema = type({ export type BlockAdditionsType = typeof BlockAdditionsSchema.infer; export const TimeSliceSchema = type({ - LaceNumber: type.number.optional(), - FrameNumber: type.number.default(0), - BlockAdditionID: type.number.default(0), - Delay: type.number.default(0), - SliceDuration: type.number.default(0), + LaceNumber: type.number.or(type.bigint).optional(), + FrameNumber: type.number.or(type.bigint).default(0), + BlockAdditionID: type.number.or(type.bigint).default(0), + Delay: type.number.or(type.bigint).default(0), + SliceDuration: type.number.or(type.bigint).default(0), }); export type TimeSliceType = typeof TimeSliceSchema.infer; @@ -101,8 +101,8 @@ export const SlicesSchema = type({ export type SlicesType = typeof SlicesSchema.infer; export const ReferenceFrameSchema = type({ - ReferenceOffset: type.number, - ReferenceTimestamp: type.number, + ReferenceOffset: type.number.or(type.bigint), + ReferenceTimestamp: type.number.or(type.bigint), }); export type ReferenceFrameType = typeof ReferenceFrameSchema.infer; @@ -111,12 +111,12 @@ export const BlockGroupSchema = type({ Block: BlockSchema, BlockVirtual: BinarySchema.optional(), BlockAdditions: BlockAdditionsSchema.optional(), - BlockDuration: type.number.optional(), - ReferencePriority: type.number.default(0), - ReferenceBlock: type.number.array().optional(), - ReferenceVirtual: type.number.optional(), + BlockDuration: type.number.or(type.bigint).optional(), + ReferencePriority: type.number.or(type.bigint).default(0), + ReferenceBlock: type.number.or(type.bigint).array().optional(), + ReferenceVirtual: type.number.or(type.bigint).optional(), CodecState: BinarySchema.optional(), - DiscardPadding: type.number.optional(), + DiscardPadding: type.number.or(type.bigint).optional(), Slices: SlicesSchema.optional(), ReferenceFrame: ReferenceFrameSchema.optional(), }); @@ -124,10 +124,10 @@ export const BlockGroupSchema = type({ export type BlockGroupType = typeof BlockGroupSchema.infer; export const ClusterSchema = type({ - Timestamp: type.number, + Timestamp: type.number.or(type.bigint), SilentTracks: SilentTracksSchema.optional(), - Position: type.number.optional(), - PrevSize: type.number.optional(), + Position: type.number.or(type.bigint).optional(), + PrevSize: type.number.or(type.bigint).optional(), SimpleBlock: SimpleBlockSchema.array().optional(), BlockGroup: BlockGroupSchema.array().optional(), EncryptedBlock: BinarySchema.array().optional(), @@ -136,9 +136,9 @@ export const ClusterSchema = type({ export type ClusterType = typeof ClusterSchema.infer; export const BlockAdditionMappingSchema = type({ - BlockAddIDValue: type.number.optional(), + BlockAddIDValue: type.number.or(type.bigint).optional(), BlockAddIDName: type.string.optional(), - BlockAddIDType: type.number.default(0), + BlockAddIDType: type.number.or(type.bigint).default(0), BlockAddIDExtraData: BinarySchema.optional(), }); @@ -146,8 +146,8 @@ export type BlockAdditionMappingType = typeof BlockAdditionMappingSchema.infer; export const TrackTranslateSchema = type({ TrackTranslateTrackID: BinarySchema, - TrackTranslateCodec: type.number, - TrackTranslateEditionUID: type.number.array().optional(), + TrackTranslateCodec: type.number.or(type.bigint), + TrackTranslateEditionUID: type.number.or(type.bigint).array().optional(), }); export type TrackTranslateType = typeof TrackTranslateSchema.infer; @@ -198,9 +198,12 @@ 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 @@ -209,9 +212,10 @@ 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 @@ -220,9 +224,10 @@ 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 @@ -233,7 +238,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; @@ -276,9 +281,12 @@ 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 @@ -309,24 +317,26 @@ 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({ MatrixCoefficients: MatrixCoefficientsRestriction.default(2), - BitsPerChannel: type.number.default(0), - ChromaSubsamplingHorz: type.number.optional(), - ChromaSubsamplingVert: type.number.optional(), - CbSubsamplingHorz: type.number.optional(), - CbSubsamplingVert: type.number.optional(), + BitsPerChannel: type.number.or(type.bigint).default(0), + ChromaSubsamplingHorz: type.number.or(type.bigint).optional(), + ChromaSubsamplingVert: type.number.or(type.bigint).optional(), + CbSubsamplingHorz: type.number.or(type.bigint).optional(), + CbSubsamplingVert: type.number.or(type.bigint).optional(), ChromaSitingHorz: ChromaSitingHorzRestriction.default(0), ChromaSitingVert: ChromaSitingVertRestriction.default(0), Range: RangeRestriction.default(0), TransferCharacteristics: TransferCharacteristicsRestriction.default(2), Primaries: PrimariesRestriction.default(2), - MaxCLL: type.number.optional(), - MaxFALL: type.number.optional(), + MaxCLL: type.number.or(type.bigint).optional(), + MaxFALL: type.number.or(type.bigint).optional(), MasteringMetadata: MasteringMetadataSchema.optional(), }); @@ -341,9 +351,10 @@ 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), @@ -362,9 +373,10 @@ 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 @@ -379,7 +391,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; @@ -414,8 +426,10 @@ 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 { @@ -423,7 +437,7 @@ export enum AlphaModeRestrictionEnum { NONE = 0, // present PRESENT = 1, -}; +} export const AlphaModeRestriction = type('0 | 1'); export type AlphaModeRestrictionType = typeof AlphaModeRestriction.infer; @@ -436,9 +450,10 @@ 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 @@ -451,7 +466,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; @@ -462,9 +477,10 @@ 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), @@ -472,14 +488,14 @@ export const VideoSchema = type({ StereoMode: StereoModeRestriction.default(0), AlphaMode: AlphaModeRestriction.default(0), OldStereoMode: OldStereoModeRestriction.optional(), - PixelWidth: type.number, - PixelHeight: type.number, - PixelCropBottom: type.number.default(0), - PixelCropTop: type.number.default(0), - PixelCropLeft: type.number.default(0), - PixelCropRight: type.number.default(0), - DisplayWidth: type.number.optional(), - DisplayHeight: type.number.optional(), + PixelWidth: type.number.or(type.bigint), + PixelHeight: type.number.or(type.bigint), + PixelCropBottom: type.number.or(type.bigint).default(0), + PixelCropTop: type.number.or(type.bigint).default(0), + PixelCropLeft: type.number.or(type.bigint).default(0), + PixelCropRight: type.number.or(type.bigint).default(0), + DisplayWidth: type.number.or(type.bigint).optional(), + DisplayHeight: type.number.or(type.bigint).optional(), DisplayUnit: DisplayUnitRestriction.default(0), AspectRatioType: AspectRatioTypeRestriction.default(0), UncompressedFourCC: BinarySchema.optional(), @@ -518,16 +534,18 @@ 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({ SamplingFrequency: type.number.default(0), OutputSamplingFrequency: type.number.optional(), - Channels: type.number.default(1), + Channels: type.number.or(type.bigint).default(1), ChannelPositions: BinarySchema.optional(), - BitDepth: type.number.optional(), + BitDepth: type.number.or(type.bigint).optional(), Emphasis: EmphasisRestriction.default(0), }); @@ -540,12 +558,13 @@ 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, + TrackPlaneUID: type.number.or(type.bigint), TrackPlaneType: TrackPlaneTypeRestriction, }); @@ -558,7 +577,7 @@ export const TrackCombinePlanesSchema = type({ export type TrackCombinePlanesType = typeof TrackCombinePlanesSchema.infer; export const TrackJoinBlocksSchema = type({ - TrackJoinUID: type.number.array().atLeastLength(1), + TrackJoinUID: type.number.or(type.bigint).array().atLeastLength(1), }); export type TrackJoinBlocksType = typeof TrackJoinBlocksSchema.infer; @@ -579,9 +598,10 @@ 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), @@ -595,15 +615,17 @@ 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 @@ -618,18 +640,20 @@ 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 @@ -638,9 +662,10 @@ 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), @@ -661,21 +686,23 @@ 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), + ContentEncodingOrder: type.number.or(type.bigint).default(0), ContentEncodingScope: ContentEncodingScopeRestriction.default(1), ContentEncodingType: ContentEncodingTypeRestriction.default(0), ContentCompression: ContentCompressionSchema.optional(), @@ -707,53 +734,53 @@ 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; export const TrackEntrySchema = type({ - TrackNumber: type.number, - TrackUID: type.number, + TrackNumber: type.number.or(type.bigint), + TrackUID: type.number.or(type.bigint), TrackType: TrackTypeRestriction, - FlagEnabled: type.number.default(1), - FlagDefault: type.number.default(1), - FlagForced: type.number.default(0), - FlagHearingImpaired: type.number.optional(), - FlagVisualImpaired: type.number.optional(), - FlagTextDescriptions: type.number.optional(), - FlagOriginal: type.number.optional(), - FlagCommentary: type.number.optional(), - FlagLacing: type.number.default(1), - MinCache: type.number.default(0), - MaxCache: type.number.optional(), - DefaultDuration: type.number.optional(), - DefaultDecodedFieldDuration: type.number.optional(), + FlagEnabled: type.number.or(type.bigint).default(1), + FlagDefault: type.number.or(type.bigint).default(1), + FlagForced: type.number.or(type.bigint).default(0), + FlagHearingImpaired: type.number.or(type.bigint).optional(), + FlagVisualImpaired: type.number.or(type.bigint).optional(), + FlagTextDescriptions: type.number.or(type.bigint).optional(), + FlagOriginal: type.number.or(type.bigint).optional(), + FlagCommentary: type.number.or(type.bigint).optional(), + FlagLacing: type.number.or(type.bigint).default(1), + MinCache: type.number.or(type.bigint).default(0), + MaxCache: type.number.or(type.bigint).optional(), + DefaultDuration: type.number.or(type.bigint).optional(), + DefaultDecodedFieldDuration: type.number.or(type.bigint).optional(), TrackTimestampScale: type.number.default(0), - TrackOffset: type.number.default(0), - MaxBlockAdditionID: type.number.default(0), + TrackOffset: type.number.or(type.bigint).default(0), + MaxBlockAdditionID: type.number.or(type.bigint).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(), CodecName: type.string.optional(), - AttachmentLink: type.number.optional(), + AttachmentLink: type.number.or(type.bigint).optional(), CodecSettings: type.string.optional(), CodecInfoURL: type.string.array().optional(), CodecDownloadURL: type.string.array().optional(), - CodecDecodeAll: type.number.default(1), - TrackOverlay: type.number.array().optional(), - CodecDelay: type.number.default(0), - SeekPreRoll: type.number.default(0), + CodecDecodeAll: type.number.or(type.bigint).default(1), + TrackOverlay: type.number.or(type.bigint).array().optional(), + CodecDelay: type.number.or(type.bigint).default(0), + SeekPreRoll: type.number.or(type.bigint).default(0), TrackTranslate: TrackTranslateSchema.array().optional(), Video: VideoSchema.optional(), Audio: AudioSchema.optional(), TrackOperation: TrackOperationSchema.optional(), - TrickTrackUID: type.number.optional(), + TrickTrackUID: type.number.or(type.bigint).optional(), TrickTrackSegmentUID: BinarySchema.optional(), - TrickTrackFlag: type.number.default(0), - TrickMasterTrackUID: type.number.optional(), + TrickTrackFlag: type.number.or(type.bigint).default(0), + TrickMasterTrackUID: type.number.or(type.bigint).optional(), TrickMasterTrackSegmentUID: BinarySchema.optional(), ContentEncodings: ContentEncodingsSchema.optional(), }); @@ -767,28 +794,28 @@ export const TracksSchema = type({ export type TracksType = typeof TracksSchema.infer; export const CueReferenceSchema = type({ - CueRefTime: type.number, - CueRefCluster: type.number, - CueRefNumber: type.number.default(1), - CueRefCodecState: type.number.default(0), + CueRefTime: type.number.or(type.bigint), + CueRefCluster: type.number.or(type.bigint), + CueRefNumber: type.number.or(type.bigint).default(1), + CueRefCodecState: type.number.or(type.bigint).default(0), }); export type CueReferenceType = typeof CueReferenceSchema.infer; export const CueTrackPositionsSchema = type({ - CueTrack: type.number, - CueClusterPosition: type.number, - CueRelativePosition: type.number.optional(), - CueDuration: type.number.optional(), - CueBlockNumber: type.number.optional(), - CueCodecState: type.number.default(0), + CueTrack: type.number.or(type.bigint), + CueClusterPosition: type.number.or(type.bigint), + CueRelativePosition: type.number.or(type.bigint).optional(), + CueDuration: type.number.or(type.bigint).optional(), + CueBlockNumber: type.number.or(type.bigint).optional(), + CueCodecState: type.number.or(type.bigint).default(0), CueReference: CueReferenceSchema.array().optional(), }); export type CueTrackPositionsType = typeof CueTrackPositionsSchema.infer; export const CuePointSchema = type({ - CueTime: type.number, + CueTime: type.number.or(type.bigint), CueTrackPositions: CueTrackPositionsSchema.array().atLeastLength(1), }); @@ -805,10 +832,10 @@ export const AttachedFileSchema = type({ FileName: type.string, FileMediaType: type.string, FileData: BinarySchema, - FileUID: type.number, + FileUID: type.number.or(type.bigint), FileReferral: BinarySchema.optional(), - FileUsedStartTime: type.number.optional(), - FileUsedEndTime: type.number.optional(), + FileUsedStartTime: type.number.or(type.bigint).optional(), + FileUsedEndTime: type.number.or(type.bigint).optional(), }); export type AttachedFileType = typeof AttachedFileSchema.infer; @@ -826,13 +853,110 @@ export const EditionDisplaySchema = type({ export type EditionDisplayType = typeof EditionDisplaySchema.infer; -export const EditionEntrySchema = type({ - EditionUID: type.number.optional(), - EditionFlagHidden: type.number.default(0), - EditionFlagDefault: type.number.default(0), - EditionFlagOrdered: type.number.default(0), - EditionDisplay: EditionDisplaySchema.array().optional(), +export const ChapterTrackSchema = type({ + ChapterTrackUID: type.number.or(type.bigint).array().atLeastLength(1), +}); +export type ChapterTrackType = typeof ChapterTrackSchema.infer; + +export const ChapLanguageSchema = match({ + 'string[]': (v) => (v.length > 0 ? v : ['eng']), + default: () => ['eng'], +}).optional(); + +export const ChapterDisplaySchema = type({ + ChapString: type.string, + ChapLanguage: ChapLanguageSchema, + ChapLanguageBCP47: type.string.array().optional(), + ChapCountry: type.string.array().optional(), +}); + +export type ChapterDisplayType = typeof ChapterDisplaySchema.infer; + +export enum ChapProcessTimeRestrictionEnum { + // during the whole chapter + DURING_THE_WHOLE_CHAPTER = 0, + // before starting playback + BEFORE_STARTING_PLAYBACK = 1, + // after playback of the chapter + AFTER_PLAYBACK_OF_THE_CHAPTER = 2, +} +export const ChapProcessTimeRestriction = type('0 | 1 | 2'); +export type ChapProcessTimeRestrictionType = + typeof ChapProcessTimeRestriction.infer; + +export const ChapProcessCommandSchema = type({ + ChapProcessTime: ChapProcessTimeRestriction, + ChapProcessData: BinarySchema, +}); + +export type ChapProcessCommandType = typeof ChapProcessCommandSchema.infer; + +export enum ChapProcessCodecIDRestrictionEnum { + // Matroska Script + MATROSKA_SCRIPT = 0, + // DVD-menu + DVD_MENU = 1, +} +export const ChapProcessCodecIDRestriction = type('0 | 1'); +export type ChapProcessCodecIDRestrictionType = + typeof ChapProcessCodecIDRestriction.infer; + +export const ChapProcessSchema = type({ + ChapProcessCodecID: ChapProcessCodecIDRestriction.default(0), + ChapProcessPrivate: BinarySchema.optional(), + ChapProcessCommand: ChapProcessCommandSchema.array().optional(), +}); + +export type ChapProcessType = typeof ChapProcessSchema.infer; + +export enum ChapterSkipTypeRestrictionEnum { + // No Skipping + NO_SKIPPING = 0, + // Opening Credits + OPENING_CREDITS = 1, + // End Credits + END_CREDITS = 2, + // Recap + RECAP = 3, + // Next Preview + NEXT_PREVIEW = 4, + // Preview + PREVIEW = 5, + // Advertisement + ADVERTISEMENT = 6, + // Intermission + INTERMISSION = 7, +} +export const ChapterSkipTypeRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7'); +export type ChapterSkipTypeRestrictionType = + typeof ChapterSkipTypeRestriction.infer; + +export const ChapterAtomSchema = type({ + ChapterUID: type.number.or(type.bigint), + ChapterStringUID: type.string.optional(), + ChapterTimeStart: type.number.or(type.bigint), + ChapterTimeEnd: type.number.or(type.bigint).optional(), + ChapterFlagHidden: type.number.or(type.bigint).default(0), + ChapterFlagEnabled: type.number.or(type.bigint).default(1), + ChapterSegmentUUID: BinarySchema.optional(), + ChapterSkipType: ChapterSkipTypeRestriction.optional(), + ChapterSegmentEditionUID: type.number.or(type.bigint).optional(), + ChapterPhysicalEquiv: type.number.or(type.bigint).optional(), + ChapterTrack: ChapterTrackSchema.optional(), + ChapterDisplay: ChapterDisplaySchema.array().optional(), + ChapProcess: ChapProcessSchema.array().optional(), +}); + +export type ChapterAtomType = typeof ChapterAtomSchema.infer; + +export const EditionEntrySchema = type({ + EditionUID: type.number.or(type.bigint).optional(), + EditionFlagHidden: type.number.or(type.bigint).default(0), + EditionFlagDefault: type.number.or(type.bigint).default(0), + EditionFlagOrdered: type.number.or(type.bigint).default(0), + EditionDisplay: EditionDisplaySchema.array().optional(), + ChapterAtom: ChapterAtomSchema.array().atLeastLength(1), }); export type EditionEntryType = typeof EditionEntrySchema.infer; @@ -844,28 +968,24 @@ export const ChaptersSchema = type({ export type ChaptersType = typeof ChaptersSchema.infer; export const TagTrackUIDSchema = match({ - "number[]": v => v.length > 0 ? v : [0], - "undefined": () => [0], - default: "assert" -}); + '(number | bigint)[]': (v) => (v.length > 0 ? v : [0]), + default: () => [0], +}).optional(); export const TagEditionUIDSchema = match({ - "number[]": v => v.length > 0 ? v : [0], - "undefined": () => [0], - default: "assert" -}); + '(number | bigint)[]': (v) => (v.length > 0 ? v : [0]), + default: () => [0], +}).optional(); export const TagChapterUIDSchema = match({ - "number[]": v => v.length > 0 ? v : [0], - "undefined": () => [0], - default: "assert" -}); + '(number | bigint)[]': (v) => (v.length > 0 ? v : [0]), + default: () => [0], +}).optional(); export const TagAttachmentUIDSchema = match({ - "number[]": v => v.length > 0 ? v : [0], - "undefined": () => [0], - default: "assert" -}); + '(number | bigint)[]': (v) => (v.length > 0 ? v : [0]), + default: () => [0], +}).optional(); export enum TargetTypeValueRestrictionEnum { // SHOT @@ -882,55 +1002,60 @@ 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({ @@ -944,9 +1069,21 @@ export const TargetsSchema = type({ export type TargetsType = typeof TargetsSchema.infer; +export const SimpleTagSchema = type({ + TagName: type.string, + TagLanguage: type.string.default('und'), + TagLanguageBCP47: type.string.optional(), + TagDefault: type.number.or(type.bigint).default(1), + TagDefaultBogus: type.number.or(type.bigint).default(1), + TagString: type.string.optional(), + TagBinary: BinarySchema.optional(), +}); + +export type SimpleTagType = typeof SimpleTagSchema.infer; + export const TagSchema = type({ Targets: TargetsSchema, - + SimpleTag: SimpleTagSchema.array().atLeastLength(1), }); export type TagType = typeof TagSchema.infer; @@ -998,14 +1135,23 @@ export const IdMultiSet = new Set([ EbmlTagIdEnum.CuePoint, EbmlTagIdEnum.AttachedFile, EbmlTagIdEnum.EditionLanguageIETF, + EbmlTagIdEnum.ChapterTrackUID, + EbmlTagIdEnum.ChapLanguage, + EbmlTagIdEnum.ChapLanguageBCP47, + EbmlTagIdEnum.ChapCountry, + EbmlTagIdEnum.ChapProcessCommand, + EbmlTagIdEnum.ChapterDisplay, + EbmlTagIdEnum.ChapProcess, EbmlTagIdEnum.EditionDisplay, + EbmlTagIdEnum.ChapterAtom, EbmlTagIdEnum.EditionEntry, EbmlTagIdEnum.TagTrackUID, EbmlTagIdEnum.TagEditionUID, EbmlTagIdEnum.TagChapterUID, EbmlTagIdEnum.TagAttachmentUID, + EbmlTagIdEnum.SimpleTag, EbmlTagIdEnum.Tag, EbmlTagIdEnum.SeekHead, EbmlTagIdEnum.Cluster, - EbmlTagIdEnum.Tags -]) \ No newline at end of file + EbmlTagIdEnum.Tags, +]); diff --git a/apps/playground/src/media/mkv/util.ts b/apps/playground/src/media/mkv/util.ts index 9d445e4..59cbca0 100644 --- a/apps/playground/src/media/mkv/util.ts +++ b/apps/playground/src/media/mkv/util.ts @@ -27,7 +27,7 @@ export function isTagPos< pos === '*' || pos === tag.position; } -export function convertEbmlTagToComponent (tag: EbmlTagType) { +export function convertEbmlTagToComponent(tag: EbmlTagType) { if (tag.type === EbmlElementType.Master) { const obj: Record = {}; const children = tag.children; @@ -51,3 +51,12 @@ export function convertEbmlTagToComponent (tag: EbmlTagType) { } return tag.data; } + +export function waitTick() { + return new Promise((resolve) => { + const timeout = setTimeout(() => { + resolve(); + timeout && clearTimeout(timeout); + }, 0); + }); +} diff --git a/apps/playground/src/video-pipeline-demo.ts b/apps/playground/src/video-pipeline-demo.ts index e6e2bb5..d3b892c 100644 --- a/apps/playground/src/video-pipeline-demo.ts +++ b/apps/playground/src/video-pipeline-demo.ts @@ -1,49 +1,394 @@ import { html, css, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; -import { type Subscription, switchMap, take } from 'rxjs'; +import { + animationFrames, + BehaviorSubject, + combineLatest, + ReplaySubject, + EMPTY, + map, + Observable, + shareReplay, + Subject, + Subscription, + switchMap, + take, + tap, + distinctUntilChanged, + fromEvent, + filter, +} from 'rxjs'; import { createEbmlController } from './media/mkv/reactive'; +import { TrackTypeRestrictionEnum, type ClusterType } from './media/mkv/schema'; +import type { SegmentComponent } from './media/mkv/model'; +import { createRef, ref, type Ref } from 'lit/directives/ref.js'; +import { Queue } from 'mnemonist'; +import { dataViewSliceToBuf } from 'konoebml'; export class VideoPipelineDemo extends LitElement { + static styles = css``; + @property() src!: string; - subscripton?: Subscription; + @property({ type: Number }) + width = 1280; - static styles = css``; + @property({ type: Number }) + height = 720; - async prepareVideoPipeline() { - if (!this.src) { + canvasRef: Ref = createRef(); + audioContext = new AudioContext(); + + seek$ = new ReplaySubject(1); + cluster$ = new Subject>(); + videoFrameBuffer$ = new BehaviorSubject(new Queue()); + audioFrameBuffer$ = new BehaviorSubject(new Queue()); + pipeline$$?: Subscription; + bridge$$?: Subscription; + private startTime = 0; + + paused$ = new BehaviorSubject(false); + ended$ = new BehaviorSubject(false); + + private preparePipeline() { + const src = this.src; + if (!src) { return; } - const { controller$ } = createEbmlController(this.src); - - this.subscripton = controller$ - .pipe( - switchMap(({ segments$ }) => segments$.pipe(take(1))), - switchMap(({ seek }) => seek(0)) - ) - .subscribe((cluster) => console.log(cluster)); - - const videoDecoder = new VideoDecoder({ - output: (frame) => {}, - error: (e) => { - e; - }, + const { controller$ } = createEbmlController({ + url: src, }); + + const segment$ = controller$.pipe( + switchMap(({ segments$ }) => segments$.pipe(take(1))) + ); + + const cluster$ = combineLatest({ + seekTime: this.seek$, + segment: segment$, + }).pipe(switchMap(({ seekTime, segment }) => segment.seek(seekTime))); + + const decode$ = segment$.pipe( + switchMap(({ withMeta$ }) => withMeta$), + map((segment) => { + const trackSystem = segment.track; + const infoSystem = segment.info; + const tracks = { + video: trackSystem.getTrackEntry({ + predicate: (c) => + c.TrackType === TrackTypeRestrictionEnum.VIDEO && + c.FlagEnabled !== 0, + }), + audio: trackSystem.getTrackEntry({ + predicate: (c) => + c.TrackType === TrackTypeRestrictionEnum.AUDIO && + c.FlagEnabled !== 0, + }), + subtitle: trackSystem.getTrackEntry({ + predicate: (c) => + c.TrackType === TrackTypeRestrictionEnum.SUBTITLE && + c.FlagEnabled !== 0, + }), + }; + + const videoDecode$ = tracks.video + ? new Observable((subscriber) => { + let isFinalized = false; + const videoTrack = tracks.video!; + const decoder = new VideoDecoder({ + output: (frame) => { + subscriber.next(frame); + }, + error: (e) => { + if (!isFinalized) { + isFinalized = true; + subscriber.error(e); + } + }, + }); + + decoder.configure({ + codec: 'hev1.2.2.L93.B0', // codec: 'vp8', + hardwareAcceleration: 'prefer-hardware', + description: videoTrack.CodecPrivate, // Uint8Array,包含 VPS/SPS/PPS + }); + + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: + const sub = this.cluster$.subscribe((c) => { + if (!isFinalized) { + for (const b of (c.SimpleBlock || []).filter( + (b) => b.track === videoTrack.TrackNumber + )) { + const chunk = new EncodedVideoChunk({ + type: b.keyframe ? 'key' : 'delta', + timestamp: + ((infoSystem.info.TimestampScale as number) / 1000) * + ((c.Timestamp as number) + b.value), + data: b.payload, + }); + decoder.decode(chunk); + } + } + }); + + return () => { + if (!isFinalized) { + isFinalized = true; + decoder.close(); + } + sub.unsubscribe(); + }; + }) + : EMPTY; + + const audioDecode$ = tracks.audio + ? new Observable((subscriber) => { + let isFinalized = false; + + const decoder = new AudioDecoder({ + output: (audioData) => { + subscriber.next(audioData); + }, + error: (e) => { + if (!isFinalized) { + isFinalized = true; + subscriber.error(e); + } + }, + }); + + const audioTrack = tracks.audio!; + const sampleRate = audioTrack.Audio?.SamplingFrequency || 44100; + const codec = 'mp4a.40.2'; + const numberOfChannels = + (audioTrack.Audio?.Channels as number) || 2; + const duration = + Math.round(Number(audioTrack.DefaultDuration / 1000)) || + Math.round((1024 / sampleRate) * 1000000); + + decoder.configure({ + codec: codec, + description: audioTrack.CodecPrivate, + numberOfChannels, + sampleRate, + }); + + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: + const sub = this.cluster$.subscribe((c) => { + if (!isFinalized) { + for (const b of (c.SimpleBlock || []).filter( + (b) => b.track === audioTrack.TrackNumber + )) { + const blockTime = (c.Timestamp as number) + b.value; + let n = 0; + for (const f of b.frames) { + const offsetTimeUs = (n + 1) * duration; + decoder.decode( + new EncodedAudioChunk({ + type: b.keyframe ? 'key' : 'delta', + timestamp: + ((infoSystem.info.TimestampScale as number) / + 1000) * + blockTime + + offsetTimeUs, + data: f, + }) + ); + n += 1; + } + } + } + }); + + return () => { + if (!isFinalized) { + isFinalized = true; + } + sub.unsubscribe(); + }; + }) + : EMPTY; + + return { + video$: videoDecode$, + audio$: audioDecode$, + }; + }), + shareReplay(1) + ); + + const addToVideoFrameBuffer$ = decode$.pipe( + switchMap((decode) => decode.video$), + tap((frame) => { + const buffer = this.videoFrameBuffer$.getValue(); + buffer.enqueue(frame); + this.videoFrameBuffer$.next(buffer); + }) + ); + + const addToAudioFrameBuffer$ = decode$.pipe( + switchMap((decode) => decode.audio$), + tap((frame) => { + const buffer = this.audioFrameBuffer$.getValue(); + buffer.enqueue(frame); + this.audioFrameBuffer$.next(buffer); + }) + ); + + const audio$ = combineLatest({ + paused: this.paused$, + ended: this.ended$, + buffered: this.audioFrameBuffer$.pipe( + map((q) => q.size >= 1), + distinctUntilChanged() + ), + }).pipe( + map(({ ended, paused, buffered }) => !paused && !ended && !!buffered), + switchMap((enabled) => (enabled ? animationFrames() : EMPTY)), + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: + tap(() => { + const audioFrameBuffer = this.audioFrameBuffer$.getValue(); + const nowTime = performance.now(); + const accTime = nowTime - this.startTime; + let audioChanged = false; + while (audioFrameBuffer.size > 0) { + const firstAudio = audioFrameBuffer.peek(); + if (firstAudio && firstAudio.timestamp <= accTime * 1000) { + const audioFrame = audioFrameBuffer.dequeue()!; + audioChanged = true; + const audioContext = this.audioContext; + + if (audioContext) { + const numberOfChannels = audioFrame.numberOfChannels; + const sampleRate = audioFrame.sampleRate; + const numberOfFrames = audioFrame.numberOfFrames; + const data = new Float32Array(numberOfFrames * numberOfChannels); + audioFrame.copyTo(data, { + planeIndex: 0, + }); + + const audioBuffer = audioContext.createBuffer( + numberOfChannels, + numberOfFrames, + sampleRate + ); + + for (let channel = 0; channel < numberOfChannels; channel++) { + const channelData = audioBuffer.getChannelData(channel); + for (let i = 0; i < numberOfFrames; i++) { + channelData[i] = data[i * numberOfChannels + channel]; + } + } + + const audioTime = audioFrame.timestamp / 1000000; + + audioFrame.close(); + + if (audioContext.state === 'running') { + const audioSource = audioContext.createBufferSource(); + audioSource.buffer = audioBuffer; + audioSource.connect(audioContext.destination); + + audioSource.start( + audioContext.currentTime + + Math.max(0, audioTime - accTime / 1000) + ); + } + } + } else { + break; + } + } + if (audioChanged) { + this.audioFrameBuffer$.next(this.audioFrameBuffer$.getValue()); + } + }) + ); + + const video$ = combineLatest({ + paused: this.paused$, + ended: this.ended$, + buffered: this.videoFrameBuffer$.pipe( + map((q) => q.size >= 1), + distinctUntilChanged() + ), + }).pipe( + map(({ ended, paused, buffered }) => !paused && !ended && !!buffered), + switchMap((enabled) => (enabled ? animationFrames() : EMPTY)), + tap(() => { + const videoFrameBuffer = this.videoFrameBuffer$.getValue(); + let videoChanged = false; + const nowTime = performance.now(); + const accTime = nowTime - this.startTime; + while (videoFrameBuffer.size > 0) { + const firstVideo = videoFrameBuffer.peek(); + if (firstVideo && firstVideo.timestamp <= accTime * 1000) { + const videoFrame = videoFrameBuffer.dequeue()!; + const canvas = this.canvasRef.value; + const canvas2dContext = canvas?.getContext('2d'); + if (canvas2dContext) { + canvas2dContext.drawImage( + videoFrame, + 0, + 0, + this.width, + this.height + ); + videoFrame.close(); + videoChanged = true; + } + } else { + break; + } + } + if (videoChanged) { + this.videoFrameBuffer$.next(videoFrameBuffer); + } + }) + ); + + this.pipeline$$ = new Subscription(); + this.pipeline$$.add(audio$.subscribe()); + this.pipeline$$.add(video$.subscribe()); + this.pipeline$$.add(addToVideoFrameBuffer$.subscribe()); + this.pipeline$$.add(addToAudioFrameBuffer$.subscribe()); + this.pipeline$$.add(cluster$.subscribe(this.cluster$)); + this.pipeline$$.add( + fromEvent(document.body, 'click').subscribe(() => { + this.audioContext.resume(); + this.audioFrameBuffer$.next(this.audioFrameBuffer$.getValue()); + }) + ); } connectedCallback(): void { super.connectedCallback(); - this.prepareVideoPipeline(); + this.preparePipeline(); + this.seek(0); } disconnectedCallback(): void { super.disconnectedCallback(); - this.subscripton?.unsubscribe(); + this.pipeline$$?.unsubscribe(); + } + + seek(seekTime: number) { + this.seek$.next(seekTime); + } + + play() { + this.paused$.next(false); + } + + pause() { + this.paused$.next(true); } render() { - return html`