feat: update examplees

This commit is contained in:
master 2025-03-24 23:59:59 +08:00
parent 53f2bc8ca7
commit 42e36e3c68
20 changed files with 398 additions and 138 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -9,7 +9,7 @@
"preview": "rsbuild preview" "preview": "rsbuild preview"
}, },
"dependencies": { "dependencies": {
"konoebml": "0.1.2-rc.5", "konoebml": "0.1.2",
"lit": "^3.2.1" "lit": "^3.2.1"
}, },
"devDependencies": { "devDependencies": {

View File

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

View File

@ -1,5 +1,5 @@
import { AudioCodec } from '../../base/audio_codecs'; import { AudioCodec } from '../../base/audio_codecs';
import { UnsupportCodecError } from '../../base/errors'; import { UnsupportedCodecError } from '../../base/errors';
import { VideoCodec } from '../../base/video_codecs'; import { VideoCodec } from '../../base/video_codecs';
import type { TrackEntryType } from '../schema'; import type { TrackEntryType } from '../schema';
import { import {
@ -10,6 +10,7 @@ import {
genCodecIdByAVCDecoderConfigurationRecord, genCodecIdByAVCDecoderConfigurationRecord,
parseAVCDecoderConfigurationRecord, parseAVCDecoderConfigurationRecord,
} from './avc'; } from './avc';
import type {ProbeInfo} from "@/media/mkv/enhance/probe.ts";
export const VideoCodecId = { export const VideoCodecId = {
VCM: 'V_MS/VFW/FOURCC', VCM: 'V_MS/VFW/FOURCC',
@ -107,123 +108,183 @@ export type SubtitleCodecIdType =
| `${(typeof SubtitleCodecId)[keyof typeof SubtitleCodecId]}` | `${(typeof SubtitleCodecId)[keyof typeof SubtitleCodecId]}`
| string; | string;
export function videoCodecIdToWebCodecsVideoDecoder(
track: TrackEntryType export interface VideoDecoderConfigExt extends VideoDecoderConfig {
): [VideoCodec, string] { codecType: VideoCodec,
}
export function videoCodecIdToWebCodecs(
track: TrackEntryType,
_probeInfo?: ProbeInfo
): VideoDecoderConfigExt {
const codecId = track.CodecID; const codecId = track.CodecID;
const codecPrivate = track.CodecPrivate; const codecPrivate = track.CodecPrivate;
const shareOptions = {
description: codecPrivate
}
switch (codecId) { switch (codecId) {
case VideoCodecId.HEVC: case VideoCodecId.HEVC:
return [VideoCodec.HEVC, 'hevc']; return { ...shareOptions, codecType: VideoCodec.HEVC, codec: 'hevc' };
case VideoCodecId.VP9: case VideoCodecId.VP9:
return [VideoCodec.VP9, 'vp09']; return { ...shareOptions, codecType: VideoCodec.VP9, codec: 'vp09' };
case VideoCodecId.AV1: case VideoCodecId.AV1:
return [VideoCodec.AV1, 'av1']; return { ...shareOptions, codecType: VideoCodec.AV1, codec: 'av1' };
case VideoCodecId.H264: case VideoCodecId.H264:
if (!codecPrivate) { if (!codecPrivate) {
throw new UnsupportCodecError( throw new UnsupportedCodecError(
'h264(without codec_private profile)', 'h264(without codec_private profile)',
'web codecs audio decoder' 'web codecs audio decoder'
); );
} }
return [ return {
VideoCodec.H264, ...shareOptions,
genCodecIdByAVCDecoderConfigurationRecord( codecType: VideoCodec.H264,
codec: genCodecIdByAVCDecoderConfigurationRecord(
parseAVCDecoderConfigurationRecord(codecPrivate) parseAVCDecoderConfigurationRecord(codecPrivate)
), )
]; };
case VideoCodecId.THEORA: case VideoCodecId.THEORA:
return [VideoCodec.Theora, 'theora']; return { ...shareOptions, codecType: VideoCodec.Theora, codec: 'theora' };
case VideoCodecId.VP8: case VideoCodecId.VP8:
return [VideoCodec.VP8, 'vp8']; return { ...shareOptions, codecType: VideoCodec.VP8, codec: 'vp8' };
case VideoCodecId.MPEG4_ISO_SP: case VideoCodecId.MPEG4_ISO_SP:
return [VideoCodec.MPEG4, 'mp4v.01.3']; return { ...shareOptions, codecType: VideoCodec.MPEG4, codec: 'mp4v.01.3' };
case VideoCodecId.MPEG4_ISO_ASP: case VideoCodecId.MPEG4_ISO_ASP:
return [VideoCodec.MPEG4, 'mp4v.20.9']; return { ...shareOptions, codecType: VideoCodec.MPEG4, codec: 'mp4v.20.9' };
case VideoCodecId.MPEG4_ISO_AP: case VideoCodecId.MPEG4_ISO_AP:
return [VideoCodec.MPEG4, 'mp4v.20.9']; return { ...shareOptions, codecType: VideoCodec.MPEG4, codec: 'mp4v.20.9' };
default: default:
throw new UnsupportCodecError(codecId, 'web codecs video decoder'); throw new UnsupportedCodecError(codecId, 'web codecs video decoder');
} }
} }
export function videoCodecIdToWebCodecsAudioDecoder( export interface AudioDecoderConfigExt extends AudioDecoderConfig {
track: TrackEntryType codecType: AudioCodec,
): [AudioCodec, string] { }
export function audioCodecIdToWebCodecs(
track: TrackEntryType,
_probeInfo?: ProbeInfo
): AudioDecoderConfigExt {
const codecId = track.CodecID; const codecId = track.CodecID;
const codecPrivate = track.CodecPrivate; const codecPrivate = track.CodecPrivate;
const bitDepth = track.Audio?.BitDepth; const bitDepth = track.Audio?.BitDepth;
const numberOfChannels = Number(track.Audio?.Channels);
const sampleRate = Number(track.Audio?.SamplingFrequency);
const shareOptions = {
numberOfChannels,
sampleRate,
description: codecPrivate
}
switch (track.CodecID) { switch (track.CodecID) {
case AudioCodecId.AAC_MPEG4_MAIN: case AudioCodecId.AAC_MPEG4_MAIN:
case AudioCodecId.AAC_MPEG2_MAIN: case AudioCodecId.AAC_MPEG2_MAIN:
return [AudioCodec.AAC, 'mp4a.40.1']; return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.1'
};
case AudioCodecId.AAC_MPEG2_LC: case AudioCodecId.AAC_MPEG2_LC:
case AudioCodecId.AAC_MPEG4_LC: case AudioCodecId.AAC_MPEG4_LC:
return [AudioCodec.AAC, 'mp4a.40.2']; return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.2'
};
case AudioCodecId.AAC_MPEG2_SSR: case AudioCodecId.AAC_MPEG2_SSR:
case AudioCodecId.AAC_MPEG4_SSR: case AudioCodecId.AAC_MPEG4_SSR:
return [AudioCodec.AAC, 'mp4a.40.3']; return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.3'
};
case AudioCodecId.AAC_MPEG4_LTP: case AudioCodecId.AAC_MPEG4_LTP:
return [AudioCodec.AAC, 'mp4a.40.4']; return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.4'
};
case AudioCodecId.AAC_MPEG2_LC_SBR: case AudioCodecId.AAC_MPEG2_LC_SBR:
case AudioCodecId.AAC_MPEG4_SBR: case AudioCodecId.AAC_MPEG4_SBR:
return [AudioCodec.AAC, 'mp4a.40.5']; return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.5'
};
case AudioCodecId.AAC: case AudioCodecId.AAC:
return [ return {
AudioCodec.AAC, ...shareOptions,
codecPrivate codecType: AudioCodec.AAC,
codec: codecPrivate
? genCodecIdByAudioSpecificConfig( ? genCodecIdByAudioSpecificConfig(
parseAudioSpecificConfig(codecPrivate) parseAudioSpecificConfig(codecPrivate)
) ) : 'mp4a.40.2',
: 'mp4a.40.2', };
];
case AudioCodecId.AC3: case AudioCodecId.AC3:
case AudioCodecId.AC3_BSID9: case AudioCodecId.AC3_BSID9:
return [AudioCodec.AC3, 'ac-3']; return {
...shareOptions,
codecType: AudioCodec.AC3,
codec: 'ac-3'
};
case AudioCodecId.EAC3: case AudioCodecId.EAC3:
case AudioCodecId.AC3_BSID10: case AudioCodecId.AC3_BSID10:
return [AudioCodec.EAC3, 'ec-3']; return {
...shareOptions,
codecType: AudioCodec.EAC3,
codec: 'ec-3'
};
case AudioCodecId.MPEG_L3: case AudioCodecId.MPEG_L3:
return [AudioCodec.MP3, 'mp3']; return {
...shareOptions,
codecType: AudioCodec.MP3,
codec: 'mp3'
};
case AudioCodecId.VORBIS: case AudioCodecId.VORBIS:
return [AudioCodec.Vorbis, 'vorbis']; return { ...shareOptions, codecType: AudioCodec.Vorbis, codec: 'vorbis' }
;
case AudioCodecId.FLAC: case AudioCodecId.FLAC:
return [AudioCodec.FLAC, 'flac']; return { ...shareOptions, codecType: AudioCodec.FLAC, codec: 'flac' }
;
case AudioCodecId.OPUS: case AudioCodecId.OPUS:
return [AudioCodec.Opus, 'opus']; return { ...shareOptions, codecType: AudioCodec.Opus, codec: 'opus' }
;
case AudioCodecId.ALAC: case AudioCodecId.ALAC:
return [AudioCodec.ALAC, 'alac']; return { ...shareOptions, codecType: AudioCodec.ALAC, codec: 'alac' }
;
case AudioCodecId.PCM_INT_BIG: case AudioCodecId.PCM_INT_BIG:
if (bitDepth === 16) { if (bitDepth === 16) {
return [AudioCodec.PCM_S16BE, 'pcm-s16be']; return { ...shareOptions, codecType: AudioCodec.PCM_S16BE, codec: 'pcm-s16be' };
} }
if (bitDepth === 24) { if (bitDepth === 24) {
return [AudioCodec.PCM_S24BE, 'pcm-s24be']; return { ...shareOptions, codecType: AudioCodec.PCM_S24BE, codec: 'pcm-s24be' };
} }
if (bitDepth === 32) { if (bitDepth === 32) {
return [AudioCodec.PCM_S32BE, 'pcm-s32be']; return { ...shareOptions, codecType: AudioCodec.PCM_S32BE, codec: 'pcm-s32be' };
} }
throw new UnsupportCodecError( throw new UnsupportedCodecError(
`${codecId}(${bitDepth}b)`, `${codecId}(${bitDepth}b)`,
'web codecs audio decoder' 'web codecs audio decoder'
); );
case AudioCodecId.PCM_INT_LIT: case AudioCodecId.PCM_INT_LIT:
if (bitDepth === 16) { if (bitDepth === 16) {
return [AudioCodec.PCM_S16LE, 'pcm-s16le']; return { ...shareOptions, codecType: AudioCodec.PCM_S16LE, codec: 'pcm-s16le' };
} }
if (bitDepth === 24) { if (bitDepth === 24) {
return [AudioCodec.PCM_S24LE, 'pcm-s24le']; return { ...shareOptions, codecType: AudioCodec.PCM_S24LE, codec: 'pcm-s24le' };
} }
if (bitDepth === 32) { if (bitDepth === 32) {
return [AudioCodec.PCM_S32LE, 'pcm-s32le']; return { ...shareOptions, codecType: AudioCodec.PCM_S32LE, codec: 'pcm-s32le' };
} }
throw new UnsupportCodecError( throw new UnsupportedCodecError(
`${codecId}(${bitDepth}b)`, `${codecId}(${bitDepth}b)`,
'web codecs audio decoder' 'web codecs audio decoder'
); );
case AudioCodecId.PCM_FLOAT_IEEE: case AudioCodecId.PCM_FLOAT_IEEE:
return [AudioCodec.PCM_F32LE, 'pcm-f32le']; return { ...shareOptions, codecType: AudioCodec.PCM_F32LE, codec: 'pcm-f32le' };
default: default:
throw new UnsupportCodecError(codecId, 'web codecs audio decoder'); throw new UnsupportedCodecError(codecId, 'web codecs audio decoder');
} }
} }

View File

@ -0,0 +1,21 @@
import { type } from 'arktype';
import type {TrackEntryType} from "@/media/mkv/schema.ts";
export const VP9DecoderProfileSchema = type('0 | 1 | 2 | 3');
export const VP9DecoderConfigurationRecordSchema = type({
profile: VP9DecoderProfileSchema,
level: type.number,
bitDepth: type.number,
});
export type VP9DecoderConfigurationRecordType =
typeof VP9DecoderConfigurationRecordSchema.infer;
export function parseVP9DecoderConfigurationRecord(track: TrackEntryType) {
const pixelWidth = Number(track.Video?.PixelWidth);
const pixelHeight = Number(track.Video?.PixelHeight);
const pixels = pixelWidth * pixelHeight;
const bitDepth = Number(track.Video?.Colour?.BitsPerChannel) || 10;
}

View File

@ -0,0 +1,3 @@
export interface ProbeInfo {
}

View File

@ -30,8 +30,14 @@ import {
TagSchema, TagSchema,
type TagType, type TagType,
TrackEntrySchema, TrackEntrySchema,
type TrackEntryType, type TrackEntryType, TrackTypeRestrictionEnum,
} from './schema'; } from './schema';
import {concatBufs} from "konoebml/lib/tools";
import {ParseCodecErrors, UnreachableOrLogicError, UnsupportedCodecError} from "@/media/base/errors.ts";
import type {ProbeInfo} from "@/media/mkv/enhance/probe.ts";
import {audioCodecIdToWebCodecs, videoCodecIdToWebCodecs} from "@/media/mkv/codecs";
import {Queue} from "mnemonist";
import {BehaviorSubject} from "rxjs";
export const SEEK_ID_KAX_INFO = new Uint8Array([0x15, 0x49, 0xa9, 0x66]); export const SEEK_ID_KAX_INFO = new Uint8Array([0x15, 0x49, 0xa9, 0x66]);
export const SEEK_ID_KAX_TRACKS = new Uint8Array([0x16, 0x54, 0xae, 0x6b]); export const SEEK_ID_KAX_TRACKS = new Uint8Array([0x16, 0x54, 0xae, 0x6b]);
@ -41,6 +47,10 @@ export const SEEK_ID_KAX_TAGS = new Uint8Array([0x12, 0x54, 0xc3, 0x67]);
export class SegmentSystem { export class SegmentSystem {
startTag: EbmlSegmentTagType; startTag: EbmlSegmentTagType;
headTags: EbmlTagType[] = []; headTags: EbmlTagType[] = [];
teeStream: ReadableStream<Uint8Array>
teeBufferTask: Promise<Uint8Array>;
firstCluster: EbmlClusterTagType | undefined;
probInfo?: ProbeInfo;
cue: CueSystem; cue: CueSystem;
cluster: ClusterSystem; cluster: ClusterSystem;
@ -49,7 +59,7 @@ export class SegmentSystem {
track: TrackSystem; track: TrackSystem;
tag: TagSystem; tag: TagSystem;
constructor(startNode: EbmlSegmentTagType) { constructor(startNode: EbmlSegmentTagType, teeStream: ReadableStream<Uint8Array>) {
this.startTag = startNode; this.startTag = startNode;
this.cue = new CueSystem(this); this.cue = new CueSystem(this);
this.cluster = new ClusterSystem(this); this.cluster = new ClusterSystem(this);
@ -57,17 +67,35 @@ export class SegmentSystem {
this.info = new InfoSystem(this); this.info = new InfoSystem(this);
this.track = new TrackSystem(this); this.track = new TrackSystem(this);
this.tag = new TagSystem(this); this.tag = new TagSystem(this);
this.teeStream = teeStream;
this.teeBufferTask = this.teeWaitingProbingData(teeStream);
}
private async teeWaitingProbingData (teeStream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
const reader = teeStream.getReader();
const list: Uint8Array<ArrayBufferLike>[] = [];
while (true) {
try {
const { done, value } = await reader.read();
if (done) {
break;
}
list.push(value);
} catch (e: any) {
if (e?.name === 'AbortError') {
break;
}
throw e;
}
}
return concatBufs(...list)
} }
get contentStartOffset() { get contentStartOffset() {
return this.startTag.startOffset + this.startTag.headerLength; return this.startTag.startOffset + this.startTag.headerLength;
} }
get startOffset() { private seekLocal () {
return this.startTag.startOffset;
}
completeHeads() {
const infoTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_INFO); const infoTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_INFO);
const tracksTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_TRACKS); const tracksTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_TRACKS);
const cuesTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_CUES); const cuesTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_CUES);
@ -83,13 +111,11 @@ export class SegmentSystem {
this.track.prepareTracksWithTag(tracksTag); this.track.prepareTracksWithTag(tracksTag);
} }
if (tagsTag?.id === EbmlTagIdEnum.Tags) { if (tagsTag?.id === EbmlTagIdEnum.Tags) {
this.tag.prepareTagsWIthTag(tagsTag); this.tag.prepareTagsWithTag(tagsTag);
}
} }
return this; scanMeta(tag: EbmlTagType) {
}
scanHead(tag: EbmlTagType) {
if ( if (
tag.id === EbmlTagIdEnum.SeekHead && tag.id === EbmlTagIdEnum.SeekHead &&
tag.position === EbmlTagPosition.End tag.position === EbmlTagPosition.End
@ -98,8 +124,62 @@ export class SegmentSystem {
} }
this.headTags.push(tag); this.headTags.push(tag);
this.seek.memoTag(tag); this.seek.memoTag(tag);
if (tag.id === EbmlTagIdEnum.Cluster && !this.firstCluster) {
this.firstCluster = tag;
this.seekLocal();
}
return this; return this;
} }
async completeMeta () {
this.seekLocal();
await this.parseCodes();
return this;
}
async fetchProbeInfo (_payload: Uint8Array): Promise<ProbeInfo> {
// call local or remote ff-probe
return {}
}
async parseCodes () {
const candidates = this.track.tracks.filter(c => c.TrackType === TrackTypeRestrictionEnum.AUDIO || c.TrackType === TrackTypeRestrictionEnum.VIDEO);
const parseErrors = new ParseCodecErrors();
if (!this.probInfo) {
for (const t of candidates) {
try {
await this.track.initTrack(t, undefined)
} catch (e: unknown) {
parseErrors.cause.push(e as Error)
}
}
if (parseErrors.cause.length > 0) {
try {
const teeBuffer = await this.teeBufferTask;
this.probInfo = await this.fetchProbeInfo(teeBuffer);
} catch (e) {
parseErrors.cause.push(e as Error);
return;
}
} else {
return;
}
}
for (const t of candidates) {
try {
await this.track.initTrack(t, this.probInfo)
} catch (e) {
parseErrors.cause.push(e as Error)
}
}
if (parseErrors.cause.length > 0) {
console.error(parseErrors);
}
}
} }
export type SegmentComponent<T> = T & { export type SegmentComponent<T> = T & {
@ -157,7 +237,7 @@ export class SeekSystem extends SegmentComponentSystemTrait<
} }
seekHeads: SeekHeadType[] = []; seekHeads: SeekHeadType[] = [];
offsetToTagMemo: Map<number, EbmlTagType> = new Map(); private offsetToTagMemo: Map<number, EbmlTagType> = new Map();
memoTag(tag: EbmlTagType) { memoTag(tag: EbmlTagType) {
this.offsetToTagMemo.set(tag.startOffset, tag); this.offsetToTagMemo.set(tag.startOffset, tag);
@ -193,6 +273,13 @@ export class SeekSystem extends SegmentComponentSystemTrait<
seekTagBySeekId(seekId: Uint8Array): EbmlTagType | undefined { seekTagBySeekId(seekId: Uint8Array): EbmlTagType | undefined {
return this.seekTagByStartOffset(this.seekOffsetBySeekId(seekId)); return this.seekTagByStartOffset(this.seekOffsetBySeekId(seekId));
} }
get firstClusterOffset () {
if (!this.segment.firstCluster) {
throw new UnreachableOrLogicError("first cluster not found")
}
return this.segment.firstCluster.startOffset;
}
} }
export class InfoSystem extends SegmentComponentSystemTrait< export class InfoSystem extends SegmentComponentSystemTrait<
@ -228,6 +315,18 @@ export class ClusterSystem extends SegmentComponentSystemTrait<
} }
} }
export interface GetTrackEntryOptions {
priority?: (v: SegmentComponent<TrackEntryType>) => number;
predicate?: (v: SegmentComponent<TrackEntryType>) => boolean;
}
export interface TrackState<Decoder, Config, Frame> {
decoder: Decoder,
configuration?: Config,
frameBuffer$: BehaviorSubject<Queue<Frame>>
}
export class TrackSystem extends SegmentComponentSystemTrait< export class TrackSystem extends SegmentComponentSystemTrait<
EbmlTrackEntryTagType, EbmlTrackEntryTagType,
typeof TrackEntrySchema typeof TrackEntrySchema
@ -237,15 +336,14 @@ export class TrackSystem extends SegmentComponentSystemTrait<
} }
tracks: SegmentComponent<TrackEntryType>[] = []; tracks: SegmentComponent<TrackEntryType>[] = [];
videoTrackState = new WeakMap<TrackEntryType, TrackState< VideoDecoder, VideoDecoderConfig, VideoFrame>>();
audioTrackState = new WeakMap<TrackEntryType, TrackState<AudioDecoder, AudioDecoderConfig, AudioData>>();
getTrackEntry({ getTrackEntry({
priority = (track) => priority = (track) =>
(Number(!!track.FlagForced) << 4) + Number(!!track.FlagDefault), (Number(!!track.FlagForced) << 4) + Number(!!track.FlagDefault),
predicate = (track) => track.FlagEnabled !== 0, predicate = (track) => track.FlagEnabled !== 0,
}: { }: GetTrackEntryOptions) {
priority?: (v: SegmentComponent<TrackEntryType>) => number;
predicate?: (v: SegmentComponent<TrackEntryType>) => boolean;
}) {
return this.tracks return this.tracks
.filter(predicate) .filter(predicate)
.toSorted((a, b) => priority(b) - priority(a)) .toSorted((a, b) => priority(b) - priority(a))
@ -258,6 +356,52 @@ export class TrackSystem extends SegmentComponentSystemTrait<
.map((c) => this.componentFromTag(c)); .map((c) => this.componentFromTag(c));
return this; return this;
} }
async initTrack (track: TrackEntryType, probe?: ProbeInfo) {
if (track.TrackType === TrackTypeRestrictionEnum.AUDIO) {
const configuration = audioCodecIdToWebCodecs(track, probe);
if (await AudioDecoder.isConfigSupported(configuration)) {
throw new UnsupportedCodecError(configuration.codec, 'audio decoder')
}
const queue$ = new BehaviorSubject(new Queue<AudioData>());
this.audioTrackState.set(track, {
configuration,
decoder: new AudioDecoder({
output: (audioData) => {
const queue = queue$.getValue();
queue.enqueue(audioData);
queue$.next(queue);
},
error: (e) => {
queue$.error(e);
},
}),
frameBuffer$: queue$,
})
} else if (track.TrackType === TrackTypeRestrictionEnum.VIDEO) {
const configuration = videoCodecIdToWebCodecs(track, probe);
if (await VideoDecoder.isConfigSupported(configuration)) {
throw new UnsupportedCodecError(configuration.codec, 'audio decoder')
}
const queue$ = new BehaviorSubject(new Queue<VideoFrame>());
this.videoTrackState.set(track, {
configuration,
decoder: new VideoDecoder({
output: (audioData) => {
const queue = queue$.getValue();
queue.enqueue(audioData);
queue$.next(queue);
},
error: (e) => {
queue$.error(e);
},
}),
frameBuffer$: queue$,
})
}
}
} }
export class CueSystem extends SegmentComponentSystemTrait< export class CueSystem extends SegmentComponentSystemTrait<
@ -350,7 +494,7 @@ export class TagSystem extends SegmentComponentSystemTrait<
tags: SegmentComponent<TagType>[] = []; tags: SegmentComponent<TagType>[] = [];
prepareWithTagsTag(tag: EbmlTagsTagType) { prepareTagsWithTag(tag: EbmlTagsTagType) {
this.tags = tag.children this.tags = tag.children
.filter((c) => c.id === EbmlTagIdEnum.Tag) .filter((c) => c.id === EbmlTagIdEnum.Tag)
.map((c) => this.componentFromTag(c)); .map((c) => this.componentFromTag(c));

View File

@ -1,62 +1,64 @@
import {EbmlStreamDecoder, EbmlTagIdEnum, EbmlTagPosition, type EbmlTagType,} from 'konoebml';
import { import {
type EbmlTagType,
EbmlStreamDecoder,
EbmlTagIdEnum,
EbmlTagPosition,
} from 'konoebml';
import {
Observable,
from,
switchMap,
share,
defer, defer,
EMPTY, EMPTY,
of,
filter, filter,
finalize, finalize,
from,
isEmpty, isEmpty,
map, map,
merge, merge,
raceWith, Observable,
of,
reduce, reduce,
scan, scan,
share,
shareReplay, shareReplay,
switchMap,
take, take,
takeUntil, takeWhile,
withLatestFrom, withLatestFrom,
} from 'rxjs'; } from 'rxjs';
import { createRangedStream, type CreateRangedStreamOptions } from '@/fetch'; import {createRangedStream, type CreateRangedStreamOptions} from '@/fetch';
import { import {type CueSystem, SEEK_ID_KAX_CUES, SEEK_ID_KAX_TAGS, type SegmentComponent, SegmentSystem,} from './model';
SegmentSystem, import {isTagIdPos, waitTick} from './util';
SEEK_ID_KAX_CUES, import type {ClusterType} from './schema';
type CueSystem,
type SegmentComponent,
SEEK_ID_KAX_TAGS,
} from './model';
import { isTagIdPos, waitTick } from './util';
import type { ClusterType } from './schema';
export interface CreateRangedEbmlStreamOptions export interface CreateRangedEbmlStreamOptions
extends CreateRangedStreamOptions {} extends CreateRangedStreamOptions {
tee?: boolean;
}
export function createRangedEbmlStream({ export function createRangedEbmlStream({
url, url,
byteStart = 0, byteStart = 0,
byteEnd, byteEnd,
tee = false
}: CreateRangedEbmlStreamOptions): Observable<{ }: CreateRangedEbmlStreamOptions): Observable<{
ebml$: Observable<EbmlTagType>; ebml$: Observable<EbmlTagType>;
totalSize?: number; totalSize?: number;
response: Response; response: Response;
body: ReadableStream<Uint8Array>; body: ReadableStream<Uint8Array>;
controller: AbortController; controller: AbortController;
teeBody: ReadableStream<Uint8Array> | undefined;
}> { }> {
const stream$ = from(createRangedStream({ url, byteStart, byteEnd })); const stream$ = from(createRangedStream({ url, byteStart, byteEnd }));
return stream$.pipe( return stream$.pipe(
switchMap(({ controller, body, totalSize, response }) => { switchMap(({ controller, body, totalSize, response }) => {
let requestCompleted = false; let requestCompleted = false;
let teeStream: ReadableStream<Uint8Array> | undefined;
let stream: ReadableStream<Uint8Array>;
if (tee) {
[stream, teeStream] = body.tee();
} else {
stream = body;
}
const originRequest$ = new Observable<EbmlTagType>((subscriber) => { const originRequest$ = new Observable<EbmlTagType>((subscriber) => {
body stream
.pipeThrough( .pipeThrough(
new EbmlStreamDecoder({ new EbmlStreamDecoder({
streamStartOffset: byteStart, streamStartOffset: byteStart,
@ -114,7 +116,8 @@ export function createRangedEbmlStream({
ebml$, ebml$,
totalSize, totalSize,
response, response,
body, body: stream,
teeBody: teeStream,
controller, controller,
}); });
}) })
@ -128,14 +131,16 @@ export function createEbmlController({
url, url,
...options ...options
}: CreateEbmlControllerOptions) { }: CreateEbmlControllerOptions) {
const request$ = createRangedEbmlStream({ const metaRequest$ = createRangedEbmlStream({
...options, ...options,
url, url,
byteStart: 0, byteStart: 0,
tee: true
}); });
const controller$ = request$.pipe( const controller$ = metaRequest$.pipe(
map(({ totalSize, ebml$, response, controller }) => { map(({ totalSize, ebml$, response, controller, teeBody }) => {
const head$ = ebml$.pipe( const head$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)), filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
take(1), take(1),
@ -147,8 +152,7 @@ export function createEbmlController({
); );
const segmentStart$ = ebml$.pipe( const segmentStart$ = ebml$.pipe(
filter((s) => s.position === EbmlTagPosition.Start), filter(isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.Start))
filter((tag) => tag.id === EbmlTagIdEnum.Segment)
); );
/** /**
@ -157,24 +161,24 @@ export function createEbmlController({
*/ */
const segments$ = segmentStart$.pipe( const segments$ = segmentStart$.pipe(
map((startTag) => { map((startTag) => {
const segment = new SegmentSystem(startTag); const segment = new SegmentSystem(startTag, teeBody!);
const clusterSystem = segment.cluster; const clusterSystem = segment.cluster;
const seekSystem = segment.seek; const seekSystem = segment.seek;
const segmentEnd$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End)),
filter((tag) => tag.id === EbmlTagIdEnum.Segment),
take(1)
);
const clusterStart$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.Start)),
take(1),
shareReplay(1)
);
const meta$ = ebml$.pipe( const meta$ = ebml$.pipe(
takeUntil(clusterStart$.pipe(raceWith(segmentEnd$))), scan((acc, tag) => {
// avoid object recreation
acc.hasKeyframe = acc.hasKeyframe || (tag.id === EbmlTagIdEnum.SimpleBlock && tag.keyframe) || (tag.id === EbmlTagIdEnum.BlockGroup && tag.children.every(c => c.id !== EbmlTagIdEnum.ReferenceBlock));
acc.tag = tag;
return acc;
}, { hasKeyframe: false, tag: undefined as unknown as EbmlTagType }),
takeWhile(
({ tag, hasKeyframe }) => {
return !isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End)(tag) && !(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)(tag) && hasKeyframe);
},
true
),
map(({ tag }) => tag),
share({ share({
resetOnComplete: false, resetOnComplete: false,
resetOnError: false, resetOnError: false,
@ -183,8 +187,8 @@ export function createEbmlController({
); );
const withMeta$ = meta$.pipe( const withMeta$ = meta$.pipe(
reduce((segment, meta) => segment.scanHead(meta), segment), reduce((segment, meta) => segment.scanMeta(meta), segment),
map(segment.completeHeads.bind(segment)), switchMap(() => segment.completeMeta()),
take(1), take(1),
shareReplay(1) shareReplay(1)
); );
@ -231,6 +235,7 @@ export function createEbmlController({
if (tagSystem.prepared) { if (tagSystem.prepared) {
return EMPTY; return EMPTY;
} }
const remoteTagsTagStartOffset = const remoteTagsTagStartOffset =
seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_TAGS); seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_TAGS);
if (remoteTagsTagStartOffset! >= 0) { if (remoteTagsTagStartOffset! >= 0) {
@ -243,7 +248,7 @@ export function createEbmlController({
filter(isTagIdPos(EbmlTagIdEnum.Tags, EbmlTagPosition.End)), filter(isTagIdPos(EbmlTagIdEnum.Tags, EbmlTagPosition.End)),
withLatestFrom(withMeta$), withLatestFrom(withMeta$),
map(([tags, withMeta]) => { map(([tags, withMeta]) => {
withMeta.tag.prepareWithTagsTag(tags); withMeta.tag.prepareTagsWithTag(tags);
return withMeta; return withMeta;
}) })
); );
@ -280,12 +285,12 @@ export function createEbmlController({
const seekWithoutCues = ( const seekWithoutCues = (
seekTime: number seekTime: number
): Observable<SegmentComponent<ClusterType>> => { ): Observable<SegmentComponent<ClusterType>> => {
const request$ = clusterStart$.pipe( const request$ = withMeta$.pipe(
switchMap((startTag) => switchMap(() =>
createRangedEbmlStream({ createRangedEbmlStream({
...options, ...options,
url, url,
byteStart: startTag.startOffset, byteStart: seekSystem.firstClusterOffset,
}) })
) )
); );
@ -301,18 +306,16 @@ export function createEbmlController({
return cluster$.pipe( return cluster$.pipe(
scan( scan(
(prev, curr) => (acc, curr) => {
[prev?.[1], curr] as [ // avoid object recreation
SegmentComponent<ClusterType> | undefined, acc.prev = acc.next;
SegmentComponent<ClusterType> | undefined, acc.next = curr;
], return acc;
[undefined, undefined] as [ },
SegmentComponent<ClusterType> | undefined, ({ prev: undefined as (SegmentComponent<ClusterType> | undefined), next: undefined as SegmentComponent<ClusterType> | undefined })
SegmentComponent<ClusterType> | undefined,
]
), ),
filter((c) => c[1]?.Timestamp! > seekTime), filter((c) => c.next?.Timestamp! > seekTime),
map((c) => c[0] ?? c[1]!) map((c) => c.prev ?? c.next!)
); );
}; };
@ -394,6 +397,6 @@ export function createEbmlController({
return { return {
controller$, controller$,
request$, request$: metaRequest$,
}; };
} }

View File

@ -23,7 +23,6 @@ import { TrackTypeRestrictionEnum, type ClusterType } from './media/mkv/schema';
import type { SegmentComponent } from './media/mkv/model'; import type { SegmentComponent } from './media/mkv/model';
import { createRef, ref, type Ref } from 'lit/directives/ref.js'; import { createRef, ref, type Ref } from 'lit/directives/ref.js';
import { Queue } from 'mnemonist'; import { Queue } from 'mnemonist';
import { dataViewSliceToBuf } from 'konoebml';
export class VideoPipelineDemo extends LitElement { export class VideoPipelineDemo extends LitElement {
static styles = css``; static styles = css``;
@ -45,7 +44,6 @@ export class VideoPipelineDemo extends LitElement {
videoFrameBuffer$ = new BehaviorSubject(new Queue<VideoFrame>()); videoFrameBuffer$ = new BehaviorSubject(new Queue<VideoFrame>());
audioFrameBuffer$ = new BehaviorSubject(new Queue<AudioData>()); audioFrameBuffer$ = new BehaviorSubject(new Queue<AudioData>());
pipeline$$?: Subscription; pipeline$$?: Subscription;
bridge$$?: Subscription;
private startTime = 0; private startTime = 0;
paused$ = new BehaviorSubject<boolean>(false); paused$ = new BehaviorSubject<boolean>(false);

20
pnpm-lock.yaml generated
View File

@ -89,8 +89,8 @@ importers:
apps/playground: apps/playground:
dependencies: dependencies:
konoebml: konoebml:
specifier: 0.1.2-rc.5 specifier: 0.1.2
version: 0.1.2-rc.5(arktype@2.1.10) version: 0.1.2(arktype@2.1.10)
lit: lit:
specifier: ^3.2.1 specifier: ^3.2.1
version: 3.2.1 version: 3.2.1
@ -1885,6 +1885,15 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
konoebml@0.1.2:
resolution: {integrity: sha512-ZPibYe5KLu+fhd3ZgKiz2xyitTY24VhhG+DHw+hCUwJppaQbEJDLTkbUcWQ6A3JkSkL5RiZASNocFvqfudASNA==}
engines: {node: '>= 18.0.0'}
peerDependencies:
arktype: ^2.0.0
peerDependenciesMeta:
arktype:
optional: true
konoebml@0.1.2-rc.5: konoebml@0.1.2-rc.5:
resolution: {integrity: sha512-VsXIlsXby0OzSzLER6ERRZE+9kLkqrYUF7Wr9MKAt8qvmUc3/YStf2SdpC2gMOtCjoyxDi7bXCQPIOHziUu4nw==} resolution: {integrity: sha512-VsXIlsXby0OzSzLER6ERRZE+9kLkqrYUF7Wr9MKAt8qvmUc3/YStf2SdpC2gMOtCjoyxDi7bXCQPIOHziUu4nw==}
engines: {node: '>= 18.0.0'} engines: {node: '>= 18.0.0'}
@ -4518,6 +4527,13 @@ snapshots:
kind-of@6.0.3: {} kind-of@6.0.3: {}
konoebml@0.1.2(arktype@2.1.10):
dependencies:
mnemonist: 0.40.3
type-fest: 4.37.0
optionalDependencies:
arktype: 2.1.10
konoebml@0.1.2-rc.5(arktype@2.1.10): konoebml@0.1.2-rc.5(arktype@2.1.10):
dependencies: dependencies:
mnemonist: 0.40.3 mnemonist: 0.40.3