feat: temp save
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { UnsupportedCodecError } from '@konoplayer/core/errors';
|
||||
import {ParseCodecError, UnsupportedCodecError} from '@konoplayer/core/errors';
|
||||
import { VideoCodec, AudioCodec } from '@konoplayer/core/codecs';
|
||||
import type { TrackEntryType } from '../schema';
|
||||
import {
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from './hevc.ts';
|
||||
import {
|
||||
genCodecStringByVP9DecoderConfigurationRecord,
|
||||
parseVP9DecoderConfigurationRecord,
|
||||
parseVP9DecoderConfigurationRecord, VP9_CODEC_TYPE,
|
||||
} from './vp9.ts';
|
||||
|
||||
export const VideoCodecId = {
|
||||
@@ -122,9 +122,13 @@ export interface VideoDecoderConfigExt extends VideoDecoderConfig {
|
||||
codecType: VideoCodec;
|
||||
}
|
||||
|
||||
export function videoCodecIdRequirePeekingKeyframe(codecId: VideoCodecIdType) {
|
||||
return codecId === VideoCodecId.VP9
|
||||
}
|
||||
|
||||
export function videoCodecIdToWebCodecs(
|
||||
track: TrackEntryType,
|
||||
keyframe: Uint8Array
|
||||
keyframe: Uint8Array | undefined
|
||||
): VideoDecoderConfigExt {
|
||||
const codecId = track.CodecID;
|
||||
const codecPrivate = track.CodecPrivate;
|
||||
@@ -141,6 +145,9 @@ export function videoCodecIdToWebCodecs(
|
||||
),
|
||||
};
|
||||
case VideoCodecId.VP9:
|
||||
if (!keyframe) {
|
||||
throw new ParseCodecError(VP9_CODEC_TYPE, 'keyframe is required to parse VP9 codec')
|
||||
}
|
||||
return {
|
||||
...shareOptions,
|
||||
codecType: VideoCodec.VP9,
|
||||
@@ -195,8 +202,15 @@ export interface AudioDecoderConfigExt extends AudioDecoderConfig {
|
||||
codecType: AudioCodec;
|
||||
}
|
||||
|
||||
export function isAudioCodecIdRequirePeekingKeyframe (
|
||||
_track: TrackEntryType,
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function audioCodecIdToWebCodecs(
|
||||
track: TrackEntryType
|
||||
track: TrackEntryType,
|
||||
_keyframe: Uint8Array | undefined
|
||||
): AudioDecoderConfigExt {
|
||||
const codecId = track.CodecID;
|
||||
const codecPrivate = track.CodecPrivate;
|
||||
|
||||
@@ -1,469 +0,0 @@
|
||||
import {
|
||||
type EbmlClusterTagType,
|
||||
type EbmlCuePointTagType,
|
||||
type EbmlCuesTagType,
|
||||
type EbmlInfoTagType,
|
||||
type EbmlMasterTagType,
|
||||
type EbmlSeekHeadTagType,
|
||||
type EbmlSegmentTagType,
|
||||
EbmlTagIdEnum,
|
||||
EbmlTagPosition,
|
||||
type EbmlTagsTagType,
|
||||
type 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 {
|
||||
ClusterSchema,
|
||||
type ClusterType,
|
||||
CuePointSchema,
|
||||
type CuePointType,
|
||||
type CueTrackPositionsType,
|
||||
InfoSchema,
|
||||
type InfoType,
|
||||
SeekHeadSchema,
|
||||
type SeekHeadType,
|
||||
TagSchema,
|
||||
type TagType,
|
||||
TrackEntrySchema,
|
||||
type TrackEntryType,
|
||||
TrackTypeRestrictionEnum,
|
||||
} from './schema';
|
||||
import { concatBufs } from 'konoebml/lib/tools';
|
||||
import {
|
||||
ParseCodecErrors,
|
||||
UnreachableOrLogicError,
|
||||
UnsupportedCodecError,
|
||||
} from '@konoplayer/core/errors';
|
||||
import { audioCodecIdToWebCodecs, videoCodecIdToWebCodecs } from './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_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;
|
||||
headTags: EbmlTagType[] = [];
|
||||
firstCluster: EbmlClusterTagType | undefined;
|
||||
|
||||
cue: CueSystem;
|
||||
cluster: ClusterSystem;
|
||||
seek: SeekSystem;
|
||||
info: InfoSystem;
|
||||
track: TrackSystem;
|
||||
tag: TagSystem;
|
||||
|
||||
constructor(startNode: EbmlSegmentTagType) {
|
||||
this.startTag = startNode;
|
||||
this.cue = new CueSystem(this);
|
||||
this.cluster = new ClusterSystem(this);
|
||||
this.seek = new SeekSystem(this);
|
||||
this.info = new InfoSystem(this);
|
||||
this.track = new TrackSystem(this);
|
||||
this.tag = new TagSystem(this);
|
||||
}
|
||||
|
||||
get contentStartOffset() {
|
||||
return this.startTag.startOffset + this.startTag.headerLength;
|
||||
}
|
||||
|
||||
private seekLocal() {
|
||||
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);
|
||||
}
|
||||
if (infoTag?.id === EbmlTagIdEnum.Info) {
|
||||
this.info.prepareWithInfoTag(infoTag);
|
||||
}
|
||||
if (tracksTag?.id === EbmlTagIdEnum.Tracks) {
|
||||
this.track.prepareTracksWithTag(tracksTag);
|
||||
}
|
||||
if (tagsTag?.id === EbmlTagIdEnum.Tags) {
|
||||
this.tag.prepareTagsWithTag(tagsTag);
|
||||
}
|
||||
}
|
||||
|
||||
scanMeta(tag: EbmlTagType) {
|
||||
if (
|
||||
tag.id === EbmlTagIdEnum.SeekHead &&
|
||||
tag.position === EbmlTagPosition.End
|
||||
) {
|
||||
this.seek.addSeekHeadTag(tag);
|
||||
}
|
||||
this.headTags.push(tag);
|
||||
this.seek.memoTag(tag);
|
||||
if (tag.id === EbmlTagIdEnum.Cluster && !this.firstCluster) {
|
||||
this.firstCluster = tag;
|
||||
this.seekLocal();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
async completeMeta() {
|
||||
this.seekLocal();
|
||||
|
||||
await this.parseCodes();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async parseCodes() {
|
||||
const candidates = this.track.tracks.filter(
|
||||
(c) =>
|
||||
c.TrackType === TrackTypeRestrictionEnum.AUDIO ||
|
||||
c.TrackType === TrackTypeRestrictionEnum.VIDEO
|
||||
);
|
||||
const parseErrors = new ParseCodecErrors();
|
||||
|
||||
for (const t of candidates) {
|
||||
try {
|
||||
await this.track.initTrack(t, this.);
|
||||
} catch (e) {
|
||||
parseErrors.cause.push(e as Error);
|
||||
}
|
||||
}
|
||||
if (parseErrors.cause.length > 0) {
|
||||
console.error(parseErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SegmentComponent<T> = T & {
|
||||
get segment(): SegmentSystem;
|
||||
};
|
||||
|
||||
export function withSegment<T extends object>(
|
||||
component: T,
|
||||
segment: SegmentSystem
|
||||
): SegmentComponent<T> {
|
||||
const component_ = component as T & { segment: SegmentSystem };
|
||||
component_.segment = segment;
|
||||
return component_;
|
||||
}
|
||||
|
||||
export class SegmentComponentSystemTrait<
|
||||
E extends EbmlMasterTagType,
|
||||
S extends Type<any>,
|
||||
> {
|
||||
segment: SegmentSystem;
|
||||
|
||||
get schema(): S {
|
||||
throw new Error('unimplemented!');
|
||||
}
|
||||
|
||||
constructor(segment: SegmentSystem) {
|
||||
this.segment = segment;
|
||||
}
|
||||
|
||||
componentFromTag(tag: E): SegmentComponent<InferType<S>> {
|
||||
const extracted = convertEbmlTagToComponent(tag);
|
||||
const result = this.schema(extracted) as
|
||||
| (InferType<S> & { segment: SegmentSystem })
|
||||
| ArkErrors;
|
||||
if (result instanceof ArkErrors) {
|
||||
const errors = result;
|
||||
console.error(
|
||||
'Parse component from tag error:',
|
||||
tag.toDebugRecord(),
|
||||
errors.flatProblemsByPath
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
result.segment = this.segment;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class SeekSystem extends SegmentComponentSystemTrait<
|
||||
EbmlSeekHeadTagType,
|
||||
typeof SeekHeadSchema
|
||||
> {
|
||||
override get schema() {
|
||||
return SeekHeadSchema;
|
||||
}
|
||||
|
||||
seekHeads: SeekHeadType[] = [];
|
||||
private offsetToTagMemo: Map<number, EbmlTagType> = new Map();
|
||||
|
||||
memoTag(tag: EbmlTagType) {
|
||||
this.offsetToTagMemo.set(tag.startOffset, tag);
|
||||
}
|
||||
|
||||
addSeekHeadTag(tag: EbmlSeekHeadTagType) {
|
||||
const seekHead = this.componentFromTag(tag);
|
||||
this.seekHeads.push(seekHead);
|
||||
return seekHead;
|
||||
}
|
||||
|
||||
offsetFromSeekPosition(position: number): number {
|
||||
return position + this.segment.contentStartOffset;
|
||||
}
|
||||
|
||||
seekTagByStartOffset(
|
||||
startOffset: number | undefined
|
||||
): EbmlTagType | undefined {
|
||||
return startOffset! >= 0
|
||||
? this.offsetToTagMemo.get(startOffset!)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
seekOffsetBySeekId(seekId: Uint8Array): number | 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));
|
||||
}
|
||||
|
||||
get firstClusterOffset() {
|
||||
if (!this.segment.firstCluster) {
|
||||
throw new UnreachableOrLogicError('first cluster not found');
|
||||
}
|
||||
return this.segment.firstCluster.startOffset;
|
||||
}
|
||||
}
|
||||
|
||||
export class InfoSystem extends SegmentComponentSystemTrait<
|
||||
EbmlInfoTagType,
|
||||
typeof InfoSchema
|
||||
> {
|
||||
override get schema() {
|
||||
return InfoSchema;
|
||||
}
|
||||
|
||||
info!: SegmentComponent<InfoType>;
|
||||
|
||||
prepareWithInfoTag(tag: EbmlInfoTagType) {
|
||||
this.info = this.componentFromTag(tag);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterSystem extends SegmentComponentSystemTrait<
|
||||
EbmlClusterTagType,
|
||||
typeof ClusterSchema
|
||||
> {
|
||||
override get schema() {
|
||||
return ClusterSchema;
|
||||
}
|
||||
|
||||
clustersBuffer: SegmentComponent<ClusterType>[] = [];
|
||||
|
||||
addClusterWithTag(tag: EbmlClusterTagType) {
|
||||
const cluster = this.componentFromTag(tag);
|
||||
this.clustersBuffer.push(cluster);
|
||||
return cluster;
|
||||
}
|
||||
}
|
||||
|
||||
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<
|
||||
EbmlTrackEntryTagType,
|
||||
typeof TrackEntrySchema
|
||||
> {
|
||||
override get schema() {
|
||||
return TrackEntrySchema;
|
||||
}
|
||||
|
||||
tracks: SegmentComponent<TrackEntryType>[] = [];
|
||||
videoTrackState = new WeakMap<
|
||||
TrackEntryType,
|
||||
TrackState<VideoDecoder, VideoDecoderConfig, VideoFrame>
|
||||
>();
|
||||
audioTrackState = new WeakMap<
|
||||
TrackEntryType,
|
||||
TrackState<AudioDecoder, AudioDecoderConfig, AudioData>
|
||||
>();
|
||||
|
||||
getTrackEntry({
|
||||
priority = (track) =>
|
||||
(Number(!!track.FlagForced) << 4) + Number(!!track.FlagDefault),
|
||||
predicate = (track) => track.FlagEnabled !== 0,
|
||||
}: GetTrackEntryOptions) {
|
||||
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;
|
||||
}
|
||||
|
||||
async initTrack(track: TrackEntryType) {
|
||||
if (track.TrackType === TrackTypeRestrictionEnum.AUDIO) {
|
||||
const configuration = audioCodecIdToWebCodecs(track);
|
||||
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, this.keyframe);
|
||||
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<
|
||||
EbmlCuePointTagType,
|
||||
typeof CuePointSchema
|
||||
> {
|
||||
override get schema() {
|
||||
return CuePointSchema;
|
||||
}
|
||||
|
||||
cues: SegmentComponent<CuePointType>[] = [];
|
||||
|
||||
prepareCuesWithTag(tag: EbmlCuesTagType) {
|
||||
this.cues = tag.children
|
||||
.filter((c) => c.id === EbmlTagIdEnum.CuePoint)
|
||||
.map(this.componentFromTag.bind(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
findClosestCue(seekTime: number): CuePointType | undefined {
|
||||
const cues = this.cues;
|
||||
if (!cues || cues.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let left = 0;
|
||||
let right = cues.length - 1;
|
||||
|
||||
if (seekTime <= cues[0].CueTime) {
|
||||
return cues[0];
|
||||
}
|
||||
|
||||
if (seekTime >= cues[right].CueTime) {
|
||||
return cues[right];
|
||||
}
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
|
||||
if (cues[mid].CueTime === seekTime) {
|
||||
return cues[mid];
|
||||
}
|
||||
|
||||
if (cues[mid].CueTime < seekTime) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
const before = cues[right];
|
||||
const after = cues[left];
|
||||
return Math.abs((before.CueTime as number) - seekTime) <
|
||||
Math.abs((after.CueTime as number) - seekTime)
|
||||
? before
|
||||
: after;
|
||||
}
|
||||
|
||||
getCueTrackPositions(
|
||||
cuePoint: CuePointType,
|
||||
track?: number
|
||||
): CueTrackPositionsType {
|
||||
let cueTrackPositions: CueTrackPositionsType | undefined;
|
||||
if (track! >= 0) {
|
||||
cueTrackPositions = cuePoint.CueTrackPositions.find(
|
||||
(c) => c.CueTrack === track
|
||||
);
|
||||
}
|
||||
if (!cueTrackPositions) {
|
||||
cueTrackPositions = maxBy(
|
||||
cuePoint.CueTrackPositions,
|
||||
(c) => c.CueClusterPosition
|
||||
)!;
|
||||
}
|
||||
return cueTrackPositions;
|
||||
}
|
||||
|
||||
get prepared(): boolean {
|
||||
return this.cues.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class TagSystem extends SegmentComponentSystemTrait<
|
||||
EbmlTagTagType,
|
||||
typeof TagSchema
|
||||
> {
|
||||
override get schema() {
|
||||
return TagSchema;
|
||||
}
|
||||
|
||||
tags: SegmentComponent<TagType>[] = [];
|
||||
|
||||
prepareTagsWithTag(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;
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,11 @@ import {
|
||||
filter,
|
||||
finalize,
|
||||
from,
|
||||
isEmpty,
|
||||
isEmpty, last,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
of,
|
||||
reduce,
|
||||
scan,
|
||||
share,
|
||||
shareReplay,
|
||||
@@ -28,51 +27,33 @@ import {
|
||||
createRangedStream,
|
||||
type CreateRangedStreamOptions,
|
||||
} from '@konoplayer/core/data';
|
||||
import {
|
||||
type CueSystem,
|
||||
SEEK_ID_KAX_CUES,
|
||||
SEEK_ID_KAX_TAGS,
|
||||
type SegmentComponent,
|
||||
SegmentSystem,
|
||||
} from './model';
|
||||
import { isTagIdPos, waitTick } from './util';
|
||||
import type { ClusterType } from './schema';
|
||||
import {SEEK_ID_KAX_CUES, SEEK_ID_KAX_TAGS, type CueSystem, type SegmentComponent, SegmentSystem} from "./systems";
|
||||
|
||||
export interface CreateRangedEbmlStreamOptions
|
||||
extends CreateRangedStreamOptions {
|
||||
tee?: boolean;
|
||||
}
|
||||
|
||||
export function createRangedEbmlStream({
|
||||
url,
|
||||
byteStart = 0,
|
||||
byteEnd,
|
||||
tee = false,
|
||||
}: CreateRangedEbmlStreamOptions): Observable<{
|
||||
ebml$: Observable<EbmlTagType>;
|
||||
totalSize?: number;
|
||||
response: Response;
|
||||
body: ReadableStream<Uint8Array>;
|
||||
controller: AbortController;
|
||||
teeBody: ReadableStream<Uint8Array> | undefined;
|
||||
}> {
|
||||
const stream$ = from(createRangedStream({ url, byteStart, byteEnd }));
|
||||
|
||||
return stream$.pipe(
|
||||
switchMap(({ controller, body, totalSize, response }) => {
|
||||
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) => {
|
||||
stream
|
||||
body
|
||||
.pipeThrough(
|
||||
new EbmlStreamDecoder({
|
||||
streamStartOffset: byteStart,
|
||||
@@ -130,8 +111,7 @@ export function createRangedEbmlStream({
|
||||
ebml$,
|
||||
totalSize,
|
||||
response,
|
||||
body: stream,
|
||||
teeBody: teeStream,
|
||||
body,
|
||||
controller,
|
||||
});
|
||||
})
|
||||
@@ -149,11 +129,10 @@ export function createEbmlController({
|
||||
...options,
|
||||
url,
|
||||
byteStart: 0,
|
||||
tee: true,
|
||||
});
|
||||
|
||||
const controller$ = metaRequest$.pipe(
|
||||
map(({ totalSize, ebml$, response, controller, teeBody }) => {
|
||||
map(({ totalSize, ebml$, response, controller }) => {
|
||||
const head$ = ebml$.pipe(
|
||||
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
|
||||
take(1),
|
||||
@@ -174,36 +153,23 @@ export function createEbmlController({
|
||||
*/
|
||||
const segments$ = segmentStart$.pipe(
|
||||
map((startTag) => {
|
||||
const segment = new SegmentSystem(startTag, teeBody!);
|
||||
const segment = new SegmentSystem(startTag);
|
||||
const clusterSystem = segment.cluster;
|
||||
const seekSystem = segment.seek;
|
||||
|
||||
const meta$ = ebml$.pipe(
|
||||
const metaScan$ = ebml$.pipe(
|
||||
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.segment.scanMeta(tag);
|
||||
acc.tag = tag;
|
||||
return acc;
|
||||
},
|
||||
{ hasKeyframe: false, tag: undefined as unknown as EbmlTagType }
|
||||
{
|
||||
segment,
|
||||
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),
|
||||
takeWhile((acc) => acc.segment.canCompleteMeta(), true),
|
||||
share({
|
||||
resetOnComplete: false,
|
||||
resetOnError: false,
|
||||
@@ -211,10 +177,13 @@ export function createEbmlController({
|
||||
})
|
||||
);
|
||||
|
||||
const withMeta$ = meta$.pipe(
|
||||
reduce((segment, meta) => segment.scanMeta(meta), segment),
|
||||
switchMap(() => segment.completeMeta()),
|
||||
take(1),
|
||||
const meta$ = metaScan$.pipe(
|
||||
map(({ tag }) => tag)
|
||||
);
|
||||
|
||||
const withMeta$ = metaScan$.pipe(
|
||||
last(),
|
||||
switchMap(({ segment }) => segment.completeMeta()),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
|
||||
20
packages/matroska/src/systems/cluster.ts
Normal file
20
packages/matroska/src/systems/cluster.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type {EbmlClusterTagType} from "konoebml";
|
||||
import {ClusterSchema, type ClusterType} from "../schema";
|
||||
import {type SegmentComponent, SegmentComponentSystemTrait} from "./segment";
|
||||
|
||||
export class ClusterSystem extends SegmentComponentSystemTrait<
|
||||
EbmlClusterTagType,
|
||||
typeof ClusterSchema
|
||||
> {
|
||||
override get schema() {
|
||||
return ClusterSchema;
|
||||
}
|
||||
|
||||
clustersBuffer: SegmentComponent<ClusterType>[] = [];
|
||||
|
||||
addClusterWithTag(tag: EbmlClusterTagType) {
|
||||
const cluster = this.componentFromTag(tag);
|
||||
this.clustersBuffer.push(cluster);
|
||||
return cluster;
|
||||
}
|
||||
}
|
||||
84
packages/matroska/src/systems/cue.ts
Normal file
84
packages/matroska/src/systems/cue.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {type EbmlCuePointTagType, type EbmlCuesTagType, EbmlTagIdEnum} from "konoebml";
|
||||
import {CuePointSchema, type CuePointType, type CueTrackPositionsType} from "../schema.ts";
|
||||
import {maxBy} from "lodash-es";
|
||||
import {type SegmentComponent, SegmentComponentSystemTrait} from "./segment.ts";
|
||||
|
||||
export class CueSystem extends SegmentComponentSystemTrait<
|
||||
EbmlCuePointTagType,
|
||||
typeof CuePointSchema
|
||||
> {
|
||||
override get schema() {
|
||||
return CuePointSchema;
|
||||
}
|
||||
|
||||
cues: SegmentComponent<CuePointType>[] = [];
|
||||
|
||||
prepareCuesWithTag(tag: EbmlCuesTagType) {
|
||||
this.cues = tag.children
|
||||
.filter((c) => c.id === EbmlTagIdEnum.CuePoint)
|
||||
.map(this.componentFromTag.bind(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
findClosestCue(seekTime: number): CuePointType | undefined {
|
||||
const cues = this.cues;
|
||||
if (!cues || cues.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let left = 0;
|
||||
let right = cues.length - 1;
|
||||
|
||||
if (seekTime <= cues[0].CueTime) {
|
||||
return cues[0];
|
||||
}
|
||||
|
||||
if (seekTime >= cues[right].CueTime) {
|
||||
return cues[right];
|
||||
}
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
|
||||
if (cues[mid].CueTime === seekTime) {
|
||||
return cues[mid];
|
||||
}
|
||||
|
||||
if (cues[mid].CueTime < seekTime) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
const before = cues[right];
|
||||
const after = cues[left];
|
||||
return Math.abs((before.CueTime as number) - seekTime) <
|
||||
Math.abs((after.CueTime as number) - seekTime)
|
||||
? before
|
||||
: after;
|
||||
}
|
||||
|
||||
getCueTrackPositions(
|
||||
cuePoint: CuePointType,
|
||||
track?: number
|
||||
): CueTrackPositionsType {
|
||||
let cueTrackPositions: CueTrackPositionsType | undefined;
|
||||
if (track! >= 0) {
|
||||
cueTrackPositions = cuePoint.CueTrackPositions.find(
|
||||
(c) => c.CueTrack === track
|
||||
);
|
||||
}
|
||||
if (!cueTrackPositions) {
|
||||
cueTrackPositions = maxBy(
|
||||
cuePoint.CueTrackPositions,
|
||||
(c) => c.CueClusterPosition
|
||||
)!;
|
||||
}
|
||||
return cueTrackPositions;
|
||||
}
|
||||
|
||||
get prepared(): boolean {
|
||||
return this.cues.length > 0;
|
||||
}
|
||||
}
|
||||
7
packages/matroska/src/systems/index.ts
Normal file
7
packages/matroska/src/systems/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { TrackContext, AudioTrackContext, VideoTrackContext, DefaultTrackContext, type GetTrackEntryOptions, TrackSystem } from './track';
|
||||
export { CueSystem } from './cue';
|
||||
export { TagSystem } from './tag';
|
||||
export { ClusterSystem } from './cluster';
|
||||
export { InfoSystem } from './info';
|
||||
export { type SegmentComponent, SegmentSystem, SegmentComponentSystemTrait, withSegment } from './segment';
|
||||
export { SeekSystem, SEEK_ID_KAX_CUES, SEEK_ID_KAX_INFO, SEEK_ID_KAX_TAGS, SEEK_ID_KAX_TRACKS } from './seek';
|
||||
19
packages/matroska/src/systems/info.ts
Normal file
19
packages/matroska/src/systems/info.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type {EbmlInfoTagType} from "konoebml";
|
||||
import {InfoSchema, type InfoType} from "../schema.ts";
|
||||
import {type SegmentComponent, SegmentComponentSystemTrait} from "./segment.ts";
|
||||
|
||||
export class InfoSystem extends SegmentComponentSystemTrait<
|
||||
EbmlInfoTagType,
|
||||
typeof InfoSchema
|
||||
> {
|
||||
override get schema() {
|
||||
return InfoSchema;
|
||||
}
|
||||
|
||||
info!: SegmentComponent<InfoType>;
|
||||
|
||||
prepareWithInfoTag(tag: EbmlInfoTagType) {
|
||||
this.info = this.componentFromTag(tag);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
65
packages/matroska/src/systems/seek.ts
Normal file
65
packages/matroska/src/systems/seek.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type {EbmlSeekHeadTagType, EbmlTagType} from "konoebml";
|
||||
import {SeekHeadSchema, type SeekHeadType} from "../schema.ts";
|
||||
import {isEqual} from "lodash-es";
|
||||
import {UnreachableOrLogicError} from "@konoplayer/core/errors.ts";
|
||||
|
||||
import {SegmentComponentSystemTrait} from "./segment.ts";
|
||||
|
||||
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 SeekSystem extends SegmentComponentSystemTrait<
|
||||
EbmlSeekHeadTagType,
|
||||
typeof SeekHeadSchema
|
||||
> {
|
||||
override get schema() {
|
||||
return SeekHeadSchema;
|
||||
}
|
||||
|
||||
seekHeads: SeekHeadType[] = [];
|
||||
private offsetToTagMemo: Map<number, EbmlTagType> = new Map();
|
||||
|
||||
memoOffset(tag: EbmlTagType) {
|
||||
this.offsetToTagMemo.set(tag.startOffset, tag);
|
||||
}
|
||||
|
||||
addSeekHeadTag(tag: EbmlSeekHeadTagType) {
|
||||
const seekHead = this.componentFromTag(tag);
|
||||
this.seekHeads.push(seekHead);
|
||||
return seekHead;
|
||||
}
|
||||
|
||||
offsetFromSeekPosition(position: number): number {
|
||||
return position + this.segment.contentStartOffset;
|
||||
}
|
||||
|
||||
seekTagByStartOffset(
|
||||
startOffset: number | undefined
|
||||
): EbmlTagType | undefined {
|
||||
return startOffset! >= 0
|
||||
? this.offsetToTagMemo.get(startOffset!)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
seekOffsetBySeekId(seekId: Uint8Array): number | 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));
|
||||
}
|
||||
|
||||
get firstClusterOffset() {
|
||||
if (!this.segment.firstCluster) {
|
||||
throw new UnreachableOrLogicError('first cluster not found');
|
||||
}
|
||||
return this.segment.firstCluster.startOffset;
|
||||
}
|
||||
}
|
||||
156
packages/matroska/src/systems/segment.ts
Normal file
156
packages/matroska/src/systems/segment.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
type EbmlClusterTagType,
|
||||
type EbmlMasterTagType,
|
||||
type EbmlSegmentTagType,
|
||||
EbmlTagIdEnum,
|
||||
EbmlTagPosition,
|
||||
type EbmlTagType
|
||||
} from "konoebml";
|
||||
import {ArkErrors, type Type} from "arktype";
|
||||
import {convertEbmlTagToComponent, type InferType} from "../util.ts";
|
||||
import {CueSystem} from "./cue.ts";
|
||||
import {ClusterSystem} from "./cluster.ts";
|
||||
import {SEEK_ID_KAX_CUES, SEEK_ID_KAX_INFO, SEEK_ID_KAX_TAGS, SEEK_ID_KAX_TRACKS, SeekSystem} from "./seek.ts";
|
||||
import {InfoSystem} from "./info.ts";
|
||||
import {TrackSystem} from "./track.ts";
|
||||
import {TagSystem} from "./tag.ts";
|
||||
import type {BlockGroupType} from "../schema.ts";
|
||||
|
||||
export class SegmentSystem {
|
||||
startTag: EbmlSegmentTagType;
|
||||
metaTags: EbmlTagType[] = [];
|
||||
firstCluster: EbmlClusterTagType | undefined;
|
||||
|
||||
cue: CueSystem;
|
||||
cluster: ClusterSystem;
|
||||
seek: SeekSystem;
|
||||
info: InfoSystem;
|
||||
track: TrackSystem;
|
||||
tag: TagSystem;
|
||||
|
||||
constructor(startNode: EbmlSegmentTagType) {
|
||||
this.startTag = startNode;
|
||||
this.cue = new CueSystem(this);
|
||||
this.cluster = new ClusterSystem(this);
|
||||
this.seek = new SeekSystem(this);
|
||||
this.info = new InfoSystem(this);
|
||||
this.track = new TrackSystem(this);
|
||||
this.tag = new TagSystem(this);
|
||||
}
|
||||
|
||||
get contentStartOffset() {
|
||||
return this.startTag.startOffset + this.startTag.headerLength;
|
||||
}
|
||||
|
||||
private seekLocal() {
|
||||
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);
|
||||
}
|
||||
if (infoTag?.id === EbmlTagIdEnum.Info) {
|
||||
this.info.prepareWithInfoTag(infoTag);
|
||||
}
|
||||
if (tracksTag?.id === EbmlTagIdEnum.Tracks) {
|
||||
this.track.prepareTracksWithTag(tracksTag);
|
||||
}
|
||||
if (tagsTag?.id === EbmlTagIdEnum.Tags) {
|
||||
this.tag.prepareTagsWithTag(tagsTag);
|
||||
}
|
||||
}
|
||||
|
||||
scanMeta(tag: EbmlTagType) {
|
||||
if (
|
||||
tag.id === EbmlTagIdEnum.SeekHead &&
|
||||
tag.position === EbmlTagPosition.End
|
||||
) {
|
||||
this.seek.addSeekHeadTag(tag);
|
||||
}
|
||||
this.metaTags.push(tag);
|
||||
this.seek.memoOffset(tag);
|
||||
if (tag.id === EbmlTagIdEnum.Cluster && !this.firstCluster) {
|
||||
this.firstCluster = tag;
|
||||
this.seekLocal();
|
||||
}
|
||||
if (this.firstCluster) {
|
||||
if (tag.id === EbmlTagIdEnum.SimpleBlock && tag.keyframe) {
|
||||
this.track.tryPeekKeyframe(tag);
|
||||
} else if (tag.id === EbmlTagIdEnum.BlockGroup) {
|
||||
const blockGroup = convertEbmlTagToComponent(tag) as BlockGroupType;
|
||||
// keep frame
|
||||
if (blockGroup && !blockGroup.ReferenceBlock && blockGroup.Block) {
|
||||
this.track.tryPeekKeyframe(blockGroup.Block);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
canCompleteMeta() {
|
||||
const lastTag = this.metaTags.at(-1);
|
||||
if (!lastTag) {
|
||||
return false;
|
||||
}
|
||||
if (lastTag.id === EbmlTagIdEnum.Segment && lastTag.position === EbmlTagPosition.End) {
|
||||
return true;
|
||||
}
|
||||
return !!(this.firstCluster && this.track.preparedToConfigureTracks());
|
||||
}
|
||||
|
||||
async completeMeta() {
|
||||
this.seekLocal();
|
||||
|
||||
await this.track.buildTracksConfiguration();
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export type SegmentComponent<T> = T & {
|
||||
get segment(): SegmentSystem;
|
||||
};
|
||||
|
||||
export function withSegment<T extends object>(
|
||||
component: T,
|
||||
segment: SegmentSystem
|
||||
): SegmentComponent<T> {
|
||||
const component_ = component as T & { segment: SegmentSystem };
|
||||
component_.segment = segment;
|
||||
return component_;
|
||||
}
|
||||
|
||||
export class SegmentComponentSystemTrait<
|
||||
E extends EbmlMasterTagType,
|
||||
S extends Type<any>,
|
||||
> {
|
||||
segment: SegmentSystem;
|
||||
|
||||
get schema(): S {
|
||||
throw new Error('unimplemented!');
|
||||
}
|
||||
|
||||
constructor(segment: SegmentSystem) {
|
||||
this.segment = segment;
|
||||
}
|
||||
|
||||
componentFromTag(tag: E): SegmentComponent<InferType<S>> {
|
||||
const extracted = convertEbmlTagToComponent(tag);
|
||||
const result = this.schema(extracted) as
|
||||
| (InferType<S> & { segment: SegmentSystem })
|
||||
| ArkErrors;
|
||||
if (result instanceof ArkErrors) {
|
||||
const errors = result;
|
||||
console.error(
|
||||
'Parse component from tag error:',
|
||||
tag.toDebugRecord(),
|
||||
errors.flatProblemsByPath
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
result.segment = this.segment;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
26
packages/matroska/src/systems/tag.ts
Normal file
26
packages/matroska/src/systems/tag.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {EbmlTagIdEnum, type EbmlTagsTagType, type EbmlTagTagType} from "konoebml";
|
||||
import {TagSchema, type TagType} from "../schema.ts";
|
||||
|
||||
import {type SegmentComponent, SegmentComponentSystemTrait} from "./segment.ts";
|
||||
|
||||
export class TagSystem extends SegmentComponentSystemTrait<
|
||||
EbmlTagTagType,
|
||||
typeof TagSchema
|
||||
> {
|
||||
override get schema() {
|
||||
return TagSchema;
|
||||
}
|
||||
|
||||
tags: SegmentComponent<TagType>[] = [];
|
||||
|
||||
prepareTagsWithTag(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;
|
||||
}
|
||||
}
|
||||
160
packages/matroska/src/systems/track.ts
Normal file
160
packages/matroska/src/systems/track.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import {ParseCodecErrors, UnsupportedCodecError} from "@konoplayer/core/errors.ts";
|
||||
import {
|
||||
EbmlTagIdEnum,
|
||||
type EbmlTrackEntryTagType,
|
||||
type EbmlTracksTagType
|
||||
} from "konoebml";
|
||||
import {
|
||||
audioCodecIdToWebCodecs,
|
||||
videoCodecIdRequirePeekingKeyframe,
|
||||
videoCodecIdToWebCodecs, type AudioDecoderConfigExt, type VideoDecoderConfigExt
|
||||
} from "../codecs";
|
||||
import {TrackEntrySchema, type TrackEntryType, TrackTypeRestrictionEnum} from "../schema";
|
||||
import {type SegmentComponent, SegmentComponentSystemTrait} from "./segment";
|
||||
|
||||
export interface GetTrackEntryOptions {
|
||||
priority?: (v: SegmentComponent<TrackEntryType>) => number;
|
||||
predicate?: (v: SegmentComponent<TrackEntryType>) => boolean;
|
||||
}
|
||||
|
||||
export abstract class TrackContext {
|
||||
peekingKeyframe?: Uint8Array;
|
||||
trackEntry: TrackEntryType
|
||||
|
||||
constructor(trackEntry: TrackEntryType) {
|
||||
this.trackEntry = trackEntry;
|
||||
}
|
||||
|
||||
peekKeyframe (payload: Uint8Array) {
|
||||
this.peekingKeyframe = payload;
|
||||
}
|
||||
|
||||
preparedToConfigure () {
|
||||
if (this.requirePeekKeyframe()) {
|
||||
return !!this.peekingKeyframe;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract requirePeekKeyframe (): boolean;
|
||||
|
||||
abstract buildConfiguration (): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultTrackContext extends TrackContext {
|
||||
override requirePeekKeyframe(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override async buildConfiguration(): Promise<void> {}
|
||||
}
|
||||
|
||||
export class VideoTrackContext extends TrackContext {
|
||||
configuration!: VideoDecoderConfigExt;
|
||||
|
||||
override requirePeekKeyframe (): boolean {
|
||||
return videoCodecIdRequirePeekingKeyframe(this.trackEntry.CodecID);
|
||||
}
|
||||
|
||||
async buildConfiguration () {
|
||||
const configuration = videoCodecIdToWebCodecs(this.trackEntry, this.peekingKeyframe);
|
||||
if (await VideoDecoder.isConfigSupported(configuration)) {
|
||||
throw new UnsupportedCodecError(configuration.codec, 'video decoder');
|
||||
}
|
||||
this.configuration = configuration;
|
||||
}
|
||||
}
|
||||
|
||||
export class AudioTrackContext extends TrackContext {
|
||||
configuration!: AudioDecoderConfigExt;
|
||||
|
||||
override requirePeekKeyframe (): boolean {
|
||||
return videoCodecIdRequirePeekingKeyframe(this.trackEntry.CodecID);
|
||||
}
|
||||
|
||||
async buildConfiguration () {
|
||||
const configuration = audioCodecIdToWebCodecs(this.trackEntry, this.peekingKeyframe);
|
||||
if (await AudioDecoder.isConfigSupported(configuration)) {
|
||||
throw new UnsupportedCodecError(configuration.codec, 'audio decoder');
|
||||
}
|
||||
|
||||
this.configuration = configuration;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class TrackSystem extends SegmentComponentSystemTrait<
|
||||
EbmlTrackEntryTagType,
|
||||
typeof TrackEntrySchema
|
||||
> {
|
||||
override get schema() {
|
||||
return TrackEntrySchema;
|
||||
}
|
||||
|
||||
tracks: SegmentComponent<TrackEntryType>[] = [];
|
||||
trackContexts: Map<number | bigint, TrackContext> = new Map();
|
||||
|
||||
getTrackEntry({
|
||||
priority = (track) =>
|
||||
(Number(!!track.FlagForced) << 4) + Number(!!track.FlagDefault),
|
||||
predicate = (track) => track.FlagEnabled !== 0,
|
||||
}: GetTrackEntryOptions) {
|
||||
return this.tracks
|
||||
.filter(predicate)
|
||||
.toSorted((a, b) => priority(b) - priority(a))
|
||||
.at(0);
|
||||
}
|
||||
|
||||
getTrackContext <T extends TrackContext>(options: GetTrackEntryOptions): T | undefined {
|
||||
const trackEntry = this.getTrackEntry(options);
|
||||
const trackNum = trackEntry?.TrackNumber!;
|
||||
return this.trackContexts.get(trackNum) as T | undefined;
|
||||
}
|
||||
|
||||
prepareTracksWithTag(tag: EbmlTracksTagType) {
|
||||
this.tracks = tag.children
|
||||
.filter((c) => c.id === EbmlTagIdEnum.TrackEntry)
|
||||
.map((c) => this.componentFromTag(c));
|
||||
for (const track of this.tracks) {
|
||||
if (track.TrackType === TrackTypeRestrictionEnum.VIDEO) {
|
||||
this.trackContexts.set(track.TrackNumber, new VideoTrackContext(track))
|
||||
} else if (track.TrackType === TrackTypeRestrictionEnum.AUDIO) {
|
||||
this.trackContexts.set(track.TrackNumber, new AudioTrackContext(track))
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
async buildTracksConfiguration () {
|
||||
const parseErrors = new ParseCodecErrors();
|
||||
|
||||
for (const context of this.trackContexts.values()) {
|
||||
try {
|
||||
await context.buildConfiguration();
|
||||
} catch (e) {
|
||||
parseErrors.cause.push(e as Error);
|
||||
}
|
||||
}
|
||||
if (parseErrors.cause.length > 0) {
|
||||
console.error(parseErrors);
|
||||
}
|
||||
}
|
||||
|
||||
tryPeekKeyframe (tag: { track: number | bigint, frames: Uint8Array[] }) {
|
||||
for (const c of this.trackContexts.values()) {
|
||||
if (c.trackEntry.TrackNumber === tag.track) {
|
||||
c.peekKeyframe(tag.frames?.[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preparedToConfigureTracks (): boolean {
|
||||
for (const c of this.trackContexts.values()) {
|
||||
if (!c.preparedToConfigure()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user