refactor: rewrite playground
This commit is contained in:
parent
3c317627e7
commit
39e17eb6a5
@ -4,28 +4,30 @@ import {
|
||||
animationFrames,
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
ReplaySubject,
|
||||
EMPTY,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
Subject,
|
||||
Subscription,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
distinctUntilChanged,
|
||||
fromEvent, withLatestFrom, share, delay, delayWhen, from, of,
|
||||
fromEvent,
|
||||
share,
|
||||
takeUntil,
|
||||
firstValueFrom,
|
||||
} from 'rxjs';
|
||||
import { createEbmlController } from '@konoplayer/matroska/reactive';
|
||||
import {
|
||||
TrackTypeRestrictionEnum,
|
||||
type ClusterType,
|
||||
} from '@konoplayer/matroska/schema';
|
||||
import { createMatroska } from '@konoplayer/matroska/model';
|
||||
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
||||
import { Queue } from 'mnemonist';
|
||||
|
||||
import type {SegmentComponent, AudioTrackContext, VideoTrackContext} from "@konoplayer/matroska/systems";
|
||||
import type {
|
||||
AudioTrackContext,
|
||||
VideoTrackContext,
|
||||
} from '@konoplayer/matroska/systems';
|
||||
import {
|
||||
captureCanvasAsVideoSrcObject,
|
||||
createRenderingContext,
|
||||
renderBitmapAtRenderingContext,
|
||||
} from '@konoplayer/core/graphics';
|
||||
|
||||
export class VideoPipelineDemo extends LitElement {
|
||||
static styles = css``;
|
||||
@ -39,227 +41,116 @@ export class VideoPipelineDemo extends LitElement {
|
||||
@property({ type: Number })
|
||||
height = 720;
|
||||
|
||||
canvasRef: Ref<HTMLCanvasElement> = createRef();
|
||||
destroyRef$ = new Subject<void>();
|
||||
|
||||
videoRef: Ref<HTMLVideoElement> = createRef();
|
||||
renderingContext = createRenderingContext();
|
||||
audioContext = new AudioContext();
|
||||
canvasSource = new MediaSource();
|
||||
|
||||
seek$ = new ReplaySubject<number>(1);
|
||||
seeked$ = new Subject<number>();
|
||||
|
||||
cluster$ = new Subject<SegmentComponent<ClusterType>>();
|
||||
videoFrameBuffer$ = new BehaviorSubject(new Queue<VideoFrame>());
|
||||
audioFrameBuffer$ = new BehaviorSubject(new Queue<AudioData>());
|
||||
pipeline$$?: Subscription;
|
||||
private startTime = 0;
|
||||
|
||||
paused$ = new BehaviorSubject<boolean>(false);
|
||||
ended$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
private preparePipeline() {
|
||||
currentTime$ = new BehaviorSubject<number>(0);
|
||||
duration$ = new BehaviorSubject<number>(0);
|
||||
frameRate$ = new BehaviorSubject<number>(30);
|
||||
|
||||
videoTrack$ = new BehaviorSubject<VideoTrackContext | undefined>(undefined);
|
||||
audioTrack$ = new BehaviorSubject<AudioTrackContext | undefined>(undefined);
|
||||
|
||||
private async preparePipeline() {
|
||||
const src = this.src;
|
||||
const destroyRef$ = this.destroyRef$;
|
||||
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { controller$ } = createEbmlController({
|
||||
url: src,
|
||||
});
|
||||
|
||||
const segmentContext$ = controller$.pipe(
|
||||
switchMap(({ segments$ }) => segments$.pipe(take(1)))
|
||||
const {
|
||||
segment: {
|
||||
seek,
|
||||
defaultVideoTrack$,
|
||||
defaultAudioTrack$,
|
||||
videoTrackDecoder,
|
||||
audioTrackDecoder,
|
||||
},
|
||||
} = await firstValueFrom(
|
||||
createMatroska({
|
||||
url: src,
|
||||
})
|
||||
);
|
||||
|
||||
const videoTrack$ = segmentContext$.pipe(
|
||||
|
||||
)
|
||||
|
||||
const currentCluster$ = combineLatest({
|
||||
seekTime: this.seek$,
|
||||
segmentContext: segmentContext$,
|
||||
}).pipe(
|
||||
delayWhen(({ segmentContext: { segment } }) => from(segment.track.flushContexts())),
|
||||
switchMap(({ seekTime, segmentContext }) => combineLatest({
|
||||
segmentContext: of(segmentContext),
|
||||
cluster: segmentContext.seek(seekTime),
|
||||
})),
|
||||
const currentCluster$ = this.seeked$.pipe(
|
||||
switchMap((seekTime) => seek(seekTime)),
|
||||
share()
|
||||
);
|
||||
|
||||
const decodeVideo$ = currentCluster$.pipe(
|
||||
defaultVideoTrack$
|
||||
.pipe(takeUntil(destroyRef$), take(1))
|
||||
.subscribe(this.videoTrack$);
|
||||
|
||||
)
|
||||
defaultAudioTrack$
|
||||
.pipe(takeUntil(destroyRef$), take(1))
|
||||
.subscribe(this.audioTrack$);
|
||||
|
||||
const decode$ = segmentContext$.pipe(
|
||||
switchMap(({ withMeta$ }) => withMeta$),
|
||||
map((segment) => {
|
||||
const trackSystem = segment.track;
|
||||
const infoSystem = segment.info;
|
||||
const videoTrack = trackSystem.getTrackContext<VideoTrackContext>({
|
||||
predicate: (c) =>
|
||||
c.TrackType === TrackTypeRestrictionEnum.VIDEO &&
|
||||
c.FlagEnabled !== 0,
|
||||
});
|
||||
const audioTrack = trackSystem.getTrackContext({
|
||||
predicate: (c) =>
|
||||
c.TrackType === TrackTypeRestrictionEnum.AUDIO &&
|
||||
c.FlagEnabled !== 0,
|
||||
});
|
||||
|
||||
const videoDecode$ = track
|
||||
|
||||
const videoDecode$ = tracks.video
|
||||
? new Observable<VideoFrame>((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
|
||||
});
|
||||
|
||||
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<AudioData>((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: <explanation>
|
||||
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();
|
||||
this.videoTrack$
|
||||
.pipe(
|
||||
takeUntil(this.destroyRef$),
|
||||
map((track) =>
|
||||
track ? videoTrackDecoder(track, currentCluster$) : undefined
|
||||
),
|
||||
switchMap((decoder) => {
|
||||
if (!decoder) {
|
||||
return EMPTY;
|
||||
}
|
||||
return decoder.frame$;
|
||||
})
|
||||
)
|
||||
.subscribe((frame) => {
|
||||
const buffer = this.videoFrameBuffer$.value;
|
||||
buffer.enqueue(frame);
|
||||
this.videoFrameBuffer$.next(buffer);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const addToAudioFrameBuffer$ = decode$.pipe(
|
||||
switchMap((decode) => decode.audio$),
|
||||
tap((frame) => {
|
||||
const buffer = this.audioFrameBuffer$.getValue();
|
||||
this.audioTrack$
|
||||
.pipe(
|
||||
takeUntil(this.destroyRef$),
|
||||
map((track) =>
|
||||
track ? audioTrackDecoder(track, currentCluster$) : undefined
|
||||
),
|
||||
switchMap((decoder) => {
|
||||
if (!decoder) {
|
||||
return EMPTY;
|
||||
}
|
||||
return decoder.frame$;
|
||||
})
|
||||
)
|
||||
.subscribe((frame) => {
|
||||
const buffer = this.audioFrameBuffer$.value;
|
||||
buffer.enqueue(frame);
|
||||
this.audioFrameBuffer$.next(buffer);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const audio$ = combineLatest({
|
||||
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: <explanation>
|
||||
tap(() => {
|
||||
})
|
||||
.pipe(
|
||||
takeUntil(this.destroyRef$),
|
||||
map(({ ended, paused, buffered }) => !paused && !ended && !!buffered),
|
||||
switchMap((enabled) => (enabled ? animationFrames() : EMPTY))
|
||||
)
|
||||
.subscribe(() => {
|
||||
const audioFrameBuffer = this.audioFrameBuffer$.getValue();
|
||||
const nowTime = performance.now();
|
||||
const accTime = nowTime - this.startTime;
|
||||
@ -315,20 +206,22 @@ export class VideoPipelineDemo extends LitElement {
|
||||
if (audioChanged) {
|
||||
this.audioFrameBuffer$.next(this.audioFrameBuffer$.getValue());
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const video$ = combineLatest({
|
||||
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(() => {
|
||||
})
|
||||
.pipe(
|
||||
takeUntil(this.destroyRef$),
|
||||
map(({ ended, paused, buffered }) => !paused && !ended && !!buffered),
|
||||
switchMap((enabled) => (enabled ? animationFrames() : EMPTY))
|
||||
)
|
||||
.subscribe(async () => {
|
||||
const videoFrameBuffer = this.videoFrameBuffer$.getValue();
|
||||
let videoChanged = false;
|
||||
const nowTime = performance.now();
|
||||
@ -337,16 +230,10 @@ export class VideoPipelineDemo extends LitElement {
|
||||
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
|
||||
);
|
||||
const renderingContext = this.renderingContext;
|
||||
if (renderingContext) {
|
||||
const bitmap = await createImageBitmap(videoFrame);
|
||||
renderBitmapAtRenderingContext(renderingContext, bitmap);
|
||||
videoFrame.close();
|
||||
videoChanged = true;
|
||||
}
|
||||
@ -357,49 +244,67 @@ export class VideoPipelineDemo extends LitElement {
|
||||
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(currentCluster$.subscribe(this.cluster$));
|
||||
this.pipeline$$.add(
|
||||
fromEvent(document.body, 'click').subscribe(() => {
|
||||
fromEvent(document.body, 'click')
|
||||
.pipe(takeUntil(this.destroyRef$))
|
||||
.subscribe(() => {
|
||||
this.audioContext.resume();
|
||||
this.audioFrameBuffer$.next(this.audioFrameBuffer$.getValue());
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.preparePipeline();
|
||||
this.seek(0);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.pipeline$$?.unsubscribe();
|
||||
}
|
||||
|
||||
seek(seekTime: number) {
|
||||
this.seek$.next(seekTime);
|
||||
}
|
||||
|
||||
play() {
|
||||
this.paused$.next(false);
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.paused$.next(true);
|
||||
this.destroyRef$.next();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<canvas ref=${ref(this.canvasRef)} width=${this.width} height=${this.height}></canvas>
|
||||
<video ref=${ref(this.videoRef)}></video>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
const video = this.videoRef.value;
|
||||
const context = this.renderingContext;
|
||||
const frameRate$ = this.frameRate$;
|
||||
const destroyRef$ = this.destroyRef$;
|
||||
const currentTime$ = this.currentTime$;
|
||||
const duration$ = this.duration$;
|
||||
const seeked$ = this.seeked$;
|
||||
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
const canvas = context.canvas as HTMLCanvasElement;
|
||||
|
||||
Object.defineProperty(video, 'duration', {
|
||||
get: () => duration$.value,
|
||||
set: (val: number) => {
|
||||
duration$.next(val);
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(video, 'currentTime', {
|
||||
get: () => currentTime$.value,
|
||||
set: (val: number) => {
|
||||
currentTime$.next(val);
|
||||
seeked$.next(val);
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
frameRate$
|
||||
.pipe(takeUntil(destroyRef$), distinctUntilChanged())
|
||||
.subscribe((frameRate) =>
|
||||
captureCanvasAsVideoSrcObject(video, canvas, frameRate)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/node": "^22.13.11",
|
||||
"@webgpu/types": "^0.1.59",
|
||||
"change-case": "^5.4.4",
|
||||
"happy-dom": "^17.4.4",
|
||||
"tsx": "^4.19.3",
|
||||
|
35
packages/core/src/audition/index.ts
Normal file
35
packages/core/src/audition/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
|
||||
export function createAudioDecodeStream(configuration: AudioDecoderConfig): {
|
||||
decoder: AudioDecoder;
|
||||
frame$: Observable<AudioData>;
|
||||
} {
|
||||
let decoder!: VideoDecoder;
|
||||
const frame$ = new Observable<AudioData>((subscriber) => {
|
||||
let isFinalized = false;
|
||||
decoder = new AudioDecoder({
|
||||
output: (frame) => subscriber.next(frame),
|
||||
error: (e) => {
|
||||
if (!isFinalized) {
|
||||
isFinalized = true;
|
||||
subscriber.error(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
decoder.configure(configuration);
|
||||
|
||||
return () => {
|
||||
if (!isFinalized) {
|
||||
isFinalized = true;
|
||||
decoder.close();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
decoder,
|
||||
frame$,
|
||||
};
|
||||
}
|
@ -1,356 +0,0 @@
|
||||
import {
|
||||
BehaviorSubject,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
interval,
|
||||
map,
|
||||
merge, Observable,
|
||||
Subject,
|
||||
type Subscription, switchMap, takeUntil, tap,
|
||||
} from 'rxjs';
|
||||
import {NetworkState, ReadyState} from "./state.ts";
|
||||
|
||||
export interface Metadata {
|
||||
duration: number
|
||||
}
|
||||
|
||||
export abstract class VideoElementTrait {
|
||||
private playbackTimer: Subscription | undefined;
|
||||
|
||||
_src$ = new BehaviorSubject<string>('');
|
||||
_currentTime$ = new BehaviorSubject<number>(0);
|
||||
_duration$ = new BehaviorSubject<number>(Number.NaN);
|
||||
_paused$ = new BehaviorSubject<boolean>(true);
|
||||
_ended$ = new BehaviorSubject<boolean>(false);
|
||||
_volume$ = new BehaviorSubject<number>(1.0);
|
||||
_muted$ = new BehaviorSubject<boolean>(false);
|
||||
_playbackRate$ = new BehaviorSubject<number>(1.0);
|
||||
_readyState$ = new BehaviorSubject<number>(0); // HAVE_NOTHING
|
||||
_networkState$ = new BehaviorSubject<number>(0); // NETWORK_EMPTY
|
||||
_width$ = new BehaviorSubject<number>(0);
|
||||
_height$ = new BehaviorSubject<number>(0);
|
||||
_videoWidth$ = new BehaviorSubject<number>(0); // 只读,视频内在宽度
|
||||
_videoHeight$ = new BehaviorSubject<number>(0); // 只读,视频内在高度
|
||||
_poster$ = new BehaviorSubject<string>('');
|
||||
|
||||
_destroyRef$ = new Subject<void>();
|
||||
|
||||
|
||||
_progress$ = new Subject<Event>();
|
||||
_error$ = new Subject<Event>();
|
||||
_abort$ = new Subject<Event>();
|
||||
_emptied$ = new Subject<Event>();
|
||||
_stalled$ = new Subject<Event>();
|
||||
_loadeddata$ = new Subject<Event>();
|
||||
_playing$ = new Subject<Event>();
|
||||
_waiting$ = new Subject<Event>();
|
||||
_seeked$ = new Subject<Event>();
|
||||
_timeupdate$ = new Subject<Event>();
|
||||
_play$ = new Subject<Event>();
|
||||
_resize$ = new Subject<Event>();
|
||||
|
||||
_setCurrentTime$ = new Subject<number>();
|
||||
_setSrc$ = new Subject<string>();
|
||||
_callLoadMetadataStart$ = new Subject<void>();
|
||||
_callLoadMetadataEnd$ = new Subject<Metadata>();
|
||||
_callLoadDataStart$ = new Subject<Metadata>();
|
||||
_callLoadDataEnd$ = new Subject<void>();
|
||||
|
||||
protected constructor() {
|
||||
this._setCurrentTime$.pipe(
|
||||
takeUntil(this._destroyRef$))
|
||||
.subscribe(this._currentTime$)
|
||||
|
||||
this.seeking$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
switchMap(() => this._seek()),
|
||||
map(() => new Event("seeked"))
|
||||
).subscribe(this._seeked$)
|
||||
|
||||
this._setSrc$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
).subscribe(this._src$)
|
||||
this._setSrc$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
switchMap(() => this._load())
|
||||
).subscribe();
|
||||
|
||||
this._readyState$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
filter((r) => r === ReadyState.HAVE_NOTHING),
|
||||
map(() => 0)
|
||||
).subscribe(this._currentTime$);
|
||||
this._readyState$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
filter((r) => r === ReadyState.HAVE_NOTHING),
|
||||
map(() => true),
|
||||
).subscribe(this._paused$);
|
||||
this._readyState$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
filter((r) => r === ReadyState.HAVE_NOTHING),
|
||||
map(() => false)
|
||||
).subscribe(this._ended$)
|
||||
|
||||
this._callLoadMetadataStart$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
map(() => NetworkState.NETWORK_LOADING)
|
||||
).subscribe(
|
||||
this._networkState$
|
||||
);
|
||||
|
||||
this._callLoadDataEnd$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
map(() => NetworkState.NETWORK_IDLE)
|
||||
).subscribe(this._networkState$);
|
||||
this._callLoadMetadataEnd$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
map(() => ReadyState.HAVE_METADATA)
|
||||
).subscribe(this._readyState$)
|
||||
|
||||
this._callLoadMetadataEnd$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
map(meta => meta.duration)
|
||||
).subscribe(this._duration$);
|
||||
|
||||
this._callLoadDataEnd$.pipe(
|
||||
takeUntil(this._destroyRef$),
|
||||
map(() => ReadyState.HAVE_CURRENT_DATA)
|
||||
).subscribe(this._readyState$);
|
||||
}
|
||||
|
||||
get canplay$ () {
|
||||
return this._readyState$.pipe(
|
||||
filter((s) => {
|
||||
return s >= ReadyState.HAVE_CURRENT_DATA
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
map(() => new Event('canplay')),
|
||||
)
|
||||
}
|
||||
|
||||
get canplaythrough$ () {
|
||||
return this._readyState$.pipe(
|
||||
filter((s) => s >= ReadyState.HAVE_ENOUGH_DATA),
|
||||
distinctUntilChanged(),
|
||||
map(() => new Event('canplaythrough')),
|
||||
)
|
||||
}
|
||||
|
||||
get seeked$ () {
|
||||
return this._seeked$.asObservable();
|
||||
}
|
||||
|
||||
get loadstart$() {
|
||||
return this._readyState$.pipe(
|
||||
filter((s) => s === ReadyState.HAVE_ENOUGH_DATA),
|
||||
distinctUntilChanged(),
|
||||
map(() => new Event('loadstart'))
|
||||
)
|
||||
}
|
||||
|
||||
get loadedmetadata$() {
|
||||
return this._readyState$.pipe(
|
||||
filter((r) => r >= ReadyState.HAVE_METADATA),
|
||||
distinctUntilChanged(),
|
||||
map(() => new Event('loadedmetadata'))
|
||||
);
|
||||
}
|
||||
|
||||
get pause$() {
|
||||
return this._paused$.pipe(
|
||||
distinctUntilChanged(),
|
||||
filter(s => s),
|
||||
map(() => new Event('pause'))
|
||||
)
|
||||
}
|
||||
|
||||
get volumechange$() {
|
||||
return merge(
|
||||
this._volume$,
|
||||
this._muted$,
|
||||
).pipe(
|
||||
map(() => new Event('volumechange'))
|
||||
)
|
||||
}
|
||||
|
||||
get ratechange$() {
|
||||
return this._playbackRate$.pipe(
|
||||
map(() => new Event('ratechange'))
|
||||
)
|
||||
}
|
||||
|
||||
get durationchange$() {
|
||||
return this._duration$.pipe(
|
||||
map(() => new Event('durationchange'))
|
||||
)
|
||||
}
|
||||
|
||||
get ended$() {
|
||||
return this._ended$.pipe(
|
||||
distinctUntilChanged(),
|
||||
filter(s => s),
|
||||
map(() => new Event('ended'))
|
||||
)
|
||||
}
|
||||
|
||||
get seeking$() {
|
||||
return this._setCurrentTime$.pipe(
|
||||
map(() => new Event('seeking'))
|
||||
)
|
||||
}
|
||||
|
||||
// 属性 getter/setter
|
||||
get src(): string {
|
||||
return this._src$.value;
|
||||
}
|
||||
|
||||
set src(value: string) {
|
||||
this._setSrc$.next(value);
|
||||
}
|
||||
|
||||
get currentTime(): number {
|
||||
return this._currentTime$.value;
|
||||
}
|
||||
|
||||
set currentTime(value: number) {
|
||||
if (value < 0 || value > this.duration) {
|
||||
return
|
||||
}
|
||||
this._setCurrentTime$.next(
|
||||
value
|
||||
)
|
||||
this._seeked$.next(new Event('seeked'));
|
||||
this._timeupdate$.next(new Event('timeupdate'));
|
||||
}
|
||||
|
||||
get duration(): number {
|
||||
return this._duration$.value;
|
||||
}
|
||||
|
||||
get paused(): boolean {
|
||||
return this._paused$.value;
|
||||
}
|
||||
|
||||
get ended(): boolean {
|
||||
return this._ended$.value;
|
||||
}
|
||||
|
||||
get volume(): number {
|
||||
return this._volume$.value;
|
||||
}
|
||||
|
||||
set volume(value: number) {
|
||||
if (value < 0 || value > 1) {
|
||||
return
|
||||
}
|
||||
this._volume$.next(value);
|
||||
}
|
||||
|
||||
get muted(): boolean {
|
||||
return this._muted$.value;
|
||||
}
|
||||
|
||||
set muted(value: boolean) {
|
||||
this._muted$.next(value);
|
||||
}
|
||||
|
||||
get playbackRate(): number {
|
||||
return this._playbackRate$.value;
|
||||
}
|
||||
|
||||
set playbackRate(value: number) {
|
||||
if (value <= 0) {
|
||||
return;
|
||||
}
|
||||
this._playbackRate$.next(value);
|
||||
}
|
||||
|
||||
get readyState(): number {
|
||||
return this._readyState$.value;
|
||||
}
|
||||
|
||||
get networkState(): number {
|
||||
return this._networkState$.value;
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this._load()
|
||||
}
|
||||
|
||||
// 方法
|
||||
_load(): Observable<void> {
|
||||
this._callLoadMetadataStart$.next(undefined);
|
||||
return this._loadMetadata()
|
||||
.pipe(
|
||||
tap((metadata) => this._callLoadMetadataEnd$.next(metadata)),
|
||||
tap((metadata) => this._callLoadDataStart$.next(metadata)),
|
||||
switchMap((metadata) => this._loadData(metadata)),
|
||||
tap(() => this._callLoadDataEnd$)
|
||||
)
|
||||
}
|
||||
|
||||
play(): Promise<void> {
|
||||
if (!this._paused$.value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (this._readyState$.value < ReadyState.HAVE_FUTURE_DATA) {
|
||||
this._waiting$.next(new Event('waiting'));
|
||||
return Promise.reject(new Error('Not enough data'));
|
||||
}
|
||||
|
||||
this._paused$.next(false);
|
||||
this._play$.next(new Event('play'));
|
||||
this._playing$.next(new Event('playing'));
|
||||
|
||||
// 模拟播放进度
|
||||
this.playbackTimer = this._playbackRate$.pipe(
|
||||
switchMap(playbackRate => interval(1000 / playbackRate)),
|
||||
takeUntil(
|
||||
merge(
|
||||
this._paused$,
|
||||
this._destroyRef$,
|
||||
this._ended$
|
||||
)
|
||||
)
|
||||
).subscribe(() => {
|
||||
const newTime = this.currentTime + 1;
|
||||
if (newTime >= this.duration) {
|
||||
this._currentTime$.next(this.duration);
|
||||
this._paused$.next(true);
|
||||
this._ended$.next(true);
|
||||
} else {
|
||||
this._currentTime$.next(newTime);
|
||||
this._timeupdate$.next(new Event('timeupdate'));
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
if (this._paused$.value) {
|
||||
return;
|
||||
}
|
||||
this._paused$.next(true);
|
||||
}
|
||||
|
||||
canPlayType(type: string): string {
|
||||
// 简化的实现,实际需要根据 MIME 类型检查支持情况
|
||||
return type.includes('video/mp4') ? 'probably' : '';
|
||||
}
|
||||
|
||||
addTextTrack(kind: string, label: string, language: string): void {
|
||||
// 实现文本轨道逻辑(此处简化为占位符)
|
||||
console.log(`Added text track: ${kind}, ${label}, ${language}`);
|
||||
}
|
||||
|
||||
abstract _seek (): Observable<void>
|
||||
|
||||
abstract _loadMetadata (): Observable<Metadata>
|
||||
|
||||
abstract _loadData(metadata: Metadata): Observable<void>
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this._destroyRef$.next(undefined)
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
export enum NetworkState {
|
||||
NETWORK_EMPTY = 0,
|
||||
NETWORK_IDLE = 1,
|
||||
NETWORK_LOADING = 2,
|
||||
NETWORK_NO_SOURCE = 3,
|
||||
}
|
||||
|
||||
export enum ReadyState {
|
||||
HAVE_NOTHING = 0,
|
||||
HAVE_METADATA = 1,
|
||||
HAVE_CURRENT_DATA = 2,
|
||||
HAVE_FUTURE_DATA = 3,
|
||||
HAVE_ENOUGH_DATA = 4
|
||||
}
|
76
packages/core/src/graphics/index.ts
Normal file
76
packages/core/src/graphics/index.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export type RenderingContext =
|
||||
| ImageBitmapRenderingContext
|
||||
| CanvasRenderingContext2D;
|
||||
|
||||
export function createRenderingContext(): RenderingContext {
|
||||
const canvas = document.createElement('canvas');
|
||||
const context =
|
||||
canvas.getContext('bitmaprenderer') || canvas.getContext('2d');
|
||||
if (!context) {
|
||||
throw new DOMException(
|
||||
'can not get rendering context of canvas',
|
||||
'CanvasException'
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function renderBitmapAtRenderingContext(
|
||||
context: RenderingContext,
|
||||
bitmap: ImageBitmap
|
||||
) {
|
||||
const canvas = context.canvas;
|
||||
if (bitmap.width !== canvas.width || bitmap.height !== canvas.height) {
|
||||
canvas.width = bitmap.width;
|
||||
canvas.height = bitmap.height;
|
||||
}
|
||||
if (context instanceof ImageBitmapRenderingContext) {
|
||||
context.transferFromImageBitmap(bitmap);
|
||||
} else {
|
||||
context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height);
|
||||
bitmap.close();
|
||||
}
|
||||
}
|
||||
|
||||
export function captureCanvasAsVideoSrcObject(
|
||||
video: HTMLVideoElement,
|
||||
canvas: HTMLCanvasElement,
|
||||
frameRate: number
|
||||
) {
|
||||
video.srcObject = canvas.captureStream(frameRate);
|
||||
}
|
||||
|
||||
export function createVideoDecodeStream(configuration: VideoDecoderConfig): {
|
||||
decoder: VideoDecoder;
|
||||
frame$: Observable<VideoFrame>;
|
||||
} {
|
||||
let decoder!: VideoDecoder;
|
||||
const frame$ = new Observable<VideoFrame>((subscriber) => {
|
||||
let isFinalized = false;
|
||||
decoder = new VideoDecoder({
|
||||
output: (frame) => subscriber.next(frame),
|
||||
error: (e) => {
|
||||
if (!isFinalized) {
|
||||
isFinalized = true;
|
||||
subscriber.error(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
decoder.configure(configuration);
|
||||
|
||||
return () => {
|
||||
if (!isFinalized) {
|
||||
isFinalized = true;
|
||||
decoder.close();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
decoder,
|
||||
frame$,
|
||||
};
|
||||
}
|
@ -3,9 +3,11 @@ import { ArkErrors, type } from 'arktype';
|
||||
|
||||
export const AAC_CODEC_TYPE = 'AAC';
|
||||
|
||||
export const AudioObjectTypeSchema = type('1 | 2 | 3 | 4 | 5 | 29 | 67');
|
||||
export const AudioObjectTypeSchema = type('1 | 2 | 3 | 4 | 5 | 29 | 67 | 23');
|
||||
|
||||
export const SamplingFrequencyIndexSchema = type('1|2|3|4|5|6|7|8|9|10|11|12');
|
||||
export const SamplingFrequencyIndexSchema = type(
|
||||
'1 | 2 | 3 | 4 |5|6|7|8|9|10|11|12'
|
||||
);
|
||||
|
||||
export const ChannelConfigurationSchema = type('1 | 2 | 3 | 4 | 5 | 6 | 7');
|
||||
|
||||
@ -108,3 +110,15 @@ export function genCodecIdByAudioSpecificConfig(
|
||||
) {
|
||||
return `mp4a.40.${config.audioObjectType}`;
|
||||
}
|
||||
|
||||
export function samplesPerFrameByAACAudioObjectType(audioObjectType: number) {
|
||||
switch (audioObjectType) {
|
||||
case 5:
|
||||
case 29:
|
||||
return 2048;
|
||||
case 23:
|
||||
return 512;
|
||||
default:
|
||||
return 1024;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
import {ParseCodecError, 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 {
|
||||
genCodecIdByAudioSpecificConfig,
|
||||
parseAudioSpecificConfig,
|
||||
samplesPerFrameByAACAudioObjectType,
|
||||
} from './aac';
|
||||
import {
|
||||
genCodecStringByAVCDecoderConfigurationRecord,
|
||||
@ -19,7 +23,8 @@ import {
|
||||
} from './hevc.ts';
|
||||
import {
|
||||
genCodecStringByVP9DecoderConfigurationRecord,
|
||||
parseVP9DecoderConfigurationRecord, VP9_CODEC_TYPE,
|
||||
parseVP9DecoderConfigurationRecord,
|
||||
VP9_CODEC_TYPE,
|
||||
} from './vp9.ts';
|
||||
|
||||
export const VideoCodecId = {
|
||||
@ -123,7 +128,7 @@ export interface VideoDecoderConfigExt extends VideoDecoderConfig {
|
||||
}
|
||||
|
||||
export function videoCodecIdRequirePeekingKeyframe(codecId: VideoCodecIdType) {
|
||||
return codecId === VideoCodecId.VP9
|
||||
return codecId === VideoCodecId.VP9;
|
||||
}
|
||||
|
||||
export function videoCodecIdToWebCodecs(
|
||||
@ -146,7 +151,10 @@ export function videoCodecIdToWebCodecs(
|
||||
};
|
||||
case VideoCodecId.VP9:
|
||||
if (!keyframe) {
|
||||
throw new ParseCodecError(VP9_CODEC_TYPE, 'keyframe is required to parse VP9 codec')
|
||||
throw new ParseCodecError(
|
||||
VP9_CODEC_TYPE,
|
||||
'keyframe is required to parse VP9 codec'
|
||||
);
|
||||
}
|
||||
return {
|
||||
...shareOptions,
|
||||
@ -200,11 +208,10 @@ export function videoCodecIdToWebCodecs(
|
||||
|
||||
export interface AudioDecoderConfigExt extends AudioDecoderConfig {
|
||||
codecType: AudioCodec;
|
||||
samplesPerFrame?: number;
|
||||
}
|
||||
|
||||
export function isAudioCodecIdRequirePeekingKeyframe (
|
||||
_track: TrackEntryType,
|
||||
) {
|
||||
export function isAudioCodecIdRequirePeekingKeyframe(_track: TrackEntryType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -231,6 +238,7 @@ export function audioCodecIdToWebCodecs(
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.AAC,
|
||||
codec: 'mp4a.40.1',
|
||||
samplesPerFrame: 1024,
|
||||
};
|
||||
case AudioCodecId.AAC_MPEG2_LC:
|
||||
case AudioCodecId.AAC_MPEG4_LC:
|
||||
@ -238,6 +246,7 @@ export function audioCodecIdToWebCodecs(
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.AAC,
|
||||
codec: 'mp4a.40.2',
|
||||
samplesPerFrame: 1024,
|
||||
};
|
||||
case AudioCodecId.AAC_MPEG2_SSR:
|
||||
case AudioCodecId.AAC_MPEG4_SSR:
|
||||
@ -245,12 +254,14 @@ export function audioCodecIdToWebCodecs(
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.AAC,
|
||||
codec: 'mp4a.40.3',
|
||||
samplesPerFrame: 1024,
|
||||
};
|
||||
case AudioCodecId.AAC_MPEG4_LTP:
|
||||
return {
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.AAC,
|
||||
codec: 'mp4a.40.4',
|
||||
samplesPerFrame: 1024,
|
||||
};
|
||||
case AudioCodecId.AAC_MPEG2_LC_SBR:
|
||||
case AudioCodecId.AAC_MPEG4_SBR:
|
||||
@ -258,16 +269,25 @@ export function audioCodecIdToWebCodecs(
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.AAC,
|
||||
codec: 'mp4a.40.5',
|
||||
samplesPerFrame: 2048,
|
||||
};
|
||||
case AudioCodecId.AAC:
|
||||
if (codecPrivate) {
|
||||
const config = parseAudioSpecificConfig(codecPrivate);
|
||||
return {
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.AAC,
|
||||
codec: genCodecIdByAudioSpecificConfig(config),
|
||||
samplesPerFrame: samplesPerFrameByAACAudioObjectType(
|
||||
config.audioObjectType
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.AAC,
|
||||
codec: codecPrivate
|
||||
? genCodecIdByAudioSpecificConfig(
|
||||
parseAudioSpecificConfig(codecPrivate)
|
||||
)
|
||||
: 'mp4a.40.2',
|
||||
codec: 'mp4a.40.2',
|
||||
samplesPerFrame: 1024,
|
||||
};
|
||||
case AudioCodecId.AC3:
|
||||
case AudioCodecId.AC3_BSID9:
|
||||
@ -275,6 +295,7 @@ export function audioCodecIdToWebCodecs(
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.AC3,
|
||||
codec: 'ac-3',
|
||||
samplesPerFrame: 1536,
|
||||
};
|
||||
case AudioCodecId.EAC3:
|
||||
case AudioCodecId.AC3_BSID10:
|
||||
@ -282,21 +303,75 @@ export function audioCodecIdToWebCodecs(
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.EAC3,
|
||||
codec: 'ec-3',
|
||||
// TODO: FIXME
|
||||
// parse frame header
|
||||
// samples per frame = numblkscod * 256
|
||||
// most time numblkscod = 6
|
||||
// samplesPerFrame: 1536,
|
||||
};
|
||||
case AudioCodecId.MPEG_L3:
|
||||
return {
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.MP3,
|
||||
codec: 'mp3',
|
||||
samplesPerFrame: 1152,
|
||||
};
|
||||
case AudioCodecId.VORBIS:
|
||||
return { ...shareOptions, codecType: AudioCodec.Vorbis, codec: 'vorbis' };
|
||||
return {
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.Vorbis,
|
||||
codec: 'vorbis',
|
||||
/**
|
||||
* TODO: FIXME
|
||||
* read code private
|
||||
* prase setup header
|
||||
* ShortBlockSize = 2 ^ blocksize_0
|
||||
* LongBlockSize = 2 ^ blocksize_1
|
||||
*/
|
||||
samplesPerFrame: 2048,
|
||||
};
|
||||
case AudioCodecId.FLAC:
|
||||
return { ...shareOptions, codecType: AudioCodec.FLAC, codec: 'flac' };
|
||||
return {
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.FLAC,
|
||||
codec: 'flac',
|
||||
/**
|
||||
* TODO: FIXME
|
||||
* read code private
|
||||
* get block size
|
||||
*/
|
||||
// samplesPerFrame: 4096,
|
||||
};
|
||||
case AudioCodecId.OPUS:
|
||||
return { ...shareOptions, codecType: AudioCodec.Opus, codec: 'opus' };
|
||||
return {
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.Opus,
|
||||
codec: 'opus',
|
||||
/**
|
||||
* TODO: FIXME
|
||||
* Read TOC header from frame data
|
||||
*/
|
||||
// samplesPerFrame: 960,
|
||||
};
|
||||
case AudioCodecId.ALAC:
|
||||
return { ...shareOptions, codecType: AudioCodec.ALAC, codec: 'alac' };
|
||||
return {
|
||||
...shareOptions,
|
||||
codecType: AudioCodec.ALAC,
|
||||
codec: 'alac',
|
||||
/**
|
||||
* TODO: FIXME
|
||||
* parse private data and get frame length
|
||||
* 00 00 10 00 // Frame Length (4096)
|
||||
00 00 00 00 // Compatible Version (0)
|
||||
00 10 // Bit Depth (16-bit)
|
||||
40 00 // PB (like 40)
|
||||
00 00 // MB (like 0)
|
||||
00 00 // KB (like 0)
|
||||
00 02 // Channels (2)
|
||||
00 00 AC 44 // Sample Rate (44100Hz)
|
||||
*/
|
||||
// samplesPerFrame: 4096,
|
||||
};
|
||||
case AudioCodecId.PCM_INT_BIG:
|
||||
if (bitDepth === 16) {
|
||||
return {
|
||||
|
14
packages/matroska/src/model/cluster.ts
Normal file
14
packages/matroska/src/model/cluster.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { ClusterType } from '../schema';
|
||||
|
||||
export function* clusterBlocks(cluster: ClusterType) {
|
||||
if (cluster.SimpleBlock) {
|
||||
for (const simpleBlock of cluster.SimpleBlock) {
|
||||
yield simpleBlock;
|
||||
}
|
||||
}
|
||||
if (cluster.BlockGroup) {
|
||||
for (const block of cluster.BlockGroup) {
|
||||
yield block;
|
||||
}
|
||||
}
|
||||
}
|
69
packages/matroska/src/model/index.ts
Normal file
69
packages/matroska/src/model/index.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import type { CreateRangedStreamOptions } from '@konoplayer/core/data';
|
||||
import { type EbmlEBMLTagType, EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
|
||||
import {
|
||||
switchMap,
|
||||
filter,
|
||||
take,
|
||||
shareReplay,
|
||||
map,
|
||||
combineLatest,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
import { isTagIdPos } from '../util';
|
||||
import { createRangedEbmlStream } from './resource';
|
||||
import { type MatroskaSegmentModel, createMatroskaSegment } from './segment';
|
||||
|
||||
export type CreateMatroskaOptions = Omit<
|
||||
CreateRangedStreamOptions,
|
||||
'byteStart' | 'byteEnd'
|
||||
>;
|
||||
|
||||
export interface MatroskaModel {
|
||||
totalSize?: number;
|
||||
initResponse: Response;
|
||||
head: EbmlEBMLTagType;
|
||||
segment: MatroskaSegmentModel;
|
||||
}
|
||||
|
||||
export function createMatroska(options: CreateMatroskaOptions) {
|
||||
const metadataRequest$ = createRangedEbmlStream({
|
||||
...options,
|
||||
byteStart: 0,
|
||||
});
|
||||
|
||||
return metadataRequest$.pipe(
|
||||
switchMap(({ totalSize, ebml$, response }) => {
|
||||
const head$ = ebml$.pipe(
|
||||
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
|
||||
take(1),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const segmentStart$ = ebml$.pipe(
|
||||
filter(isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.Start))
|
||||
);
|
||||
|
||||
/**
|
||||
* 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) =>
|
||||
createMatroskaSegment({
|
||||
startTag,
|
||||
matroskaOptions: options,
|
||||
ebml$,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return combineLatest({
|
||||
segment: segments$.pipe(take(1)),
|
||||
head: head$,
|
||||
totalSize: of(totalSize),
|
||||
initResponse: of(response),
|
||||
});
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
90
packages/matroska/src/model/resource.ts
Normal file
90
packages/matroska/src/model/resource.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import {
|
||||
type CreateRangedStreamOptions,
|
||||
createRangedStream,
|
||||
} from '@konoplayer/core/data';
|
||||
import { type EbmlTagType, EbmlStreamDecoder, EbmlTagIdEnum } from 'konoebml';
|
||||
import { Observable, from, switchMap, share, defer, EMPTY, of } from 'rxjs';
|
||||
import { waitTick } from '../util';
|
||||
|
||||
export function createRangedEbmlStream({
|
||||
url,
|
||||
byteStart = 0,
|
||||
byteEnd,
|
||||
}: CreateRangedStreamOptions): Observable<{
|
||||
ebml$: Observable<EbmlTagType>;
|
||||
totalSize?: number;
|
||||
response: Response;
|
||||
body: ReadableStream<Uint8Array>;
|
||||
controller: AbortController;
|
||||
}> {
|
||||
const stream$ = from(createRangedStream({ url, byteStart, byteEnd }));
|
||||
|
||||
return stream$.pipe(
|
||||
switchMap(({ controller, body, totalSize, response }) => {
|
||||
let requestCompleted = false;
|
||||
|
||||
const originRequest$ = new Observable<EbmlTagType>((subscriber) => {
|
||||
body
|
||||
.pipeThrough(
|
||||
new EbmlStreamDecoder({
|
||||
streamStartOffset: byteStart,
|
||||
collectChild: (child) => child.id !== EbmlTagIdEnum.Cluster,
|
||||
backpressure: {
|
||||
eventLoop: waitTick,
|
||||
},
|
||||
})
|
||||
)
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write: async (tag) => {
|
||||
await waitTick();
|
||||
subscriber.next(tag);
|
||||
},
|
||||
close: () => {
|
||||
if (!requestCompleted) {
|
||||
requestCompleted = true;
|
||||
subscriber.complete();
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
if (requestCompleted && error?.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
requestCompleted = true;
|
||||
subscriber.error(error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
requestCompleted = true;
|
||||
controller.abort();
|
||||
};
|
||||
}).pipe(
|
||||
share({
|
||||
resetOnComplete: false,
|
||||
resetOnError: false,
|
||||
resetOnRefCountZero: true,
|
||||
})
|
||||
);
|
||||
|
||||
const ebml$ = defer(() =>
|
||||
requestCompleted ? EMPTY : originRequest$
|
||||
).pipe(
|
||||
share({
|
||||
resetOnError: false,
|
||||
resetOnComplete: true,
|
||||
resetOnRefCountZero: true,
|
||||
})
|
||||
);
|
||||
|
||||
return of({
|
||||
ebml$,
|
||||
totalSize,
|
||||
response,
|
||||
body,
|
||||
controller,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
419
packages/matroska/src/model/segment.ts
Normal file
419
packages/matroska/src/model/segment.ts
Normal file
@ -0,0 +1,419 @@
|
||||
import { createAudioDecodeStream } from '@konoplayer/core/audition';
|
||||
import { createVideoDecodeStream } from '@konoplayer/core/graphics';
|
||||
import {
|
||||
type EbmlSegmentTagType,
|
||||
type EbmlTagType,
|
||||
EbmlTagIdEnum,
|
||||
EbmlTagPosition,
|
||||
} from 'konoebml';
|
||||
import {
|
||||
type Observable,
|
||||
scan,
|
||||
takeWhile,
|
||||
share,
|
||||
map,
|
||||
last,
|
||||
switchMap,
|
||||
shareReplay,
|
||||
EMPTY,
|
||||
filter,
|
||||
withLatestFrom,
|
||||
take,
|
||||
of,
|
||||
merge,
|
||||
isEmpty,
|
||||
finalize,
|
||||
} from 'rxjs';
|
||||
import type { CreateMatroskaOptions } from '.';
|
||||
import { type ClusterType, TrackTypeRestrictionEnum } from '../schema';
|
||||
import {
|
||||
SegmentSystem,
|
||||
type SegmentComponent,
|
||||
type VideoTrackContext,
|
||||
type AudioTrackContext,
|
||||
SEEK_ID_KAX_CUES,
|
||||
SEEK_ID_KAX_TAGS,
|
||||
type CueSystem,
|
||||
} from '../systems';
|
||||
import {
|
||||
standardTrackPredicate,
|
||||
standardTrackPriority,
|
||||
} from '../systems/track';
|
||||
import { isTagIdPos } from '../util';
|
||||
import { createRangedEbmlStream } from './resource';
|
||||
|
||||
export interface CreateMatroskaSegmentOptions {
|
||||
matroskaOptions: CreateMatroskaOptions;
|
||||
startTag: EbmlSegmentTagType;
|
||||
ebml$: Observable<EbmlTagType>;
|
||||
}
|
||||
|
||||
export interface MatroskaSegmentModel {
|
||||
startTag: EbmlSegmentTagType;
|
||||
segment: SegmentSystem;
|
||||
metadataTags$: Observable<EbmlTagType>;
|
||||
loadedMetadata$: Observable<SegmentSystem>;
|
||||
loadedTags$: Observable<SegmentSystem>;
|
||||
loadedCues$: Observable<SegmentSystem>;
|
||||
seek: (seekTime: number) => Observable<SegmentComponent<ClusterType>>;
|
||||
videoTrackDecoder: (
|
||||
track: VideoTrackContext,
|
||||
cluster$: Observable<ClusterType>
|
||||
) => {
|
||||
track: VideoTrackContext;
|
||||
decoder: VideoDecoder;
|
||||
frame$: Observable<VideoFrame>;
|
||||
};
|
||||
audioTrackDecoder: (
|
||||
track: AudioTrackContext,
|
||||
cluster$: Observable<ClusterType>
|
||||
) => {
|
||||
track: AudioTrackContext;
|
||||
decoder: AudioDecoder;
|
||||
frame$: Observable<AudioData>;
|
||||
};
|
||||
defaultVideoTrack$: Observable<VideoTrackContext | undefined>;
|
||||
defaultAudioTrack$: Observable<AudioTrackContext | undefined>;
|
||||
}
|
||||
|
||||
export function createMatroskaSegment({
|
||||
matroskaOptions,
|
||||
startTag,
|
||||
ebml$,
|
||||
}: CreateMatroskaSegmentOptions): MatroskaSegmentModel {
|
||||
const segment = new SegmentSystem(startTag);
|
||||
const clusterSystem = segment.cluster;
|
||||
const seekSystem = segment.seek;
|
||||
|
||||
const metaScan$ = ebml$.pipe(
|
||||
scan(
|
||||
(acc, tag) => {
|
||||
acc.segment.scanMeta(tag);
|
||||
acc.tag = tag;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
segment,
|
||||
tag: undefined as unknown as EbmlTagType,
|
||||
}
|
||||
),
|
||||
takeWhile((acc) => acc.segment.canCompleteMeta(), true),
|
||||
share({
|
||||
resetOnComplete: false,
|
||||
resetOnError: false,
|
||||
resetOnRefCountZero: true,
|
||||
})
|
||||
);
|
||||
|
||||
const metadataTags$ = metaScan$.pipe(map(({ tag }) => tag));
|
||||
|
||||
const loadedMetadata$ = metaScan$.pipe(
|
||||
last(),
|
||||
switchMap(({ segment }) => segment.completeMeta()),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const loadedRemoteCues$ = loadedMetadata$.pipe(
|
||||
switchMap((s) => {
|
||||
const cueSystem = s.cue;
|
||||
const seekSystem = s.seek;
|
||||
if (cueSystem.prepared) {
|
||||
return EMPTY;
|
||||
}
|
||||
const remoteCuesTagStartOffset =
|
||||
seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_CUES);
|
||||
if (remoteCuesTagStartOffset! >= 0) {
|
||||
return createRangedEbmlStream({
|
||||
...matroskaOptions,
|
||||
byteStart: remoteCuesTagStartOffset,
|
||||
}).pipe(
|
||||
switchMap((req) => req.ebml$),
|
||||
filter(isTagIdPos(EbmlTagIdEnum.Cues, EbmlTagPosition.End)),
|
||||
withLatestFrom(loadedMetadata$),
|
||||
map(([cues, withMeta]) => {
|
||||
withMeta.cue.prepareCuesWithTag(cues);
|
||||
return withMeta;
|
||||
})
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
take(1),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const loadedLocalCues$ = loadedMetadata$.pipe(
|
||||
switchMap((s) => (s.cue.prepared ? of(s) : EMPTY)),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const loadedEmptyCues$ = merge(loadedLocalCues$, loadedRemoteCues$).pipe(
|
||||
isEmpty(),
|
||||
switchMap((empty) => (empty ? loadedMetadata$ : EMPTY))
|
||||
);
|
||||
|
||||
const loadedCues$ = merge(
|
||||
loadedLocalCues$,
|
||||
loadedRemoteCues$,
|
||||
loadedEmptyCues$
|
||||
).pipe(take(1));
|
||||
|
||||
const loadedRemoteTags$ = loadedMetadata$.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({
|
||||
...matroskaOptions,
|
||||
byteStart: remoteTagsTagStartOffset,
|
||||
}).pipe(
|
||||
switchMap((req) => req.ebml$),
|
||||
filter(isTagIdPos(EbmlTagIdEnum.Tags, EbmlTagPosition.End)),
|
||||
withLatestFrom(loadedMetadata$),
|
||||
map(([tags, withMeta]) => {
|
||||
withMeta.tag.prepareTagsWithTag(tags);
|
||||
return withMeta;
|
||||
})
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
take(1),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const loadedLocalTags$ = loadedMetadata$.pipe(
|
||||
switchMap((s) => (s.tag.prepared ? of(s) : EMPTY)),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const loadedEmptyTags$ = merge(loadedRemoteTags$, loadedLocalTags$).pipe(
|
||||
isEmpty(),
|
||||
switchMap((empty) => (empty ? loadedMetadata$ : EMPTY))
|
||||
);
|
||||
|
||||
const loadedTags$ = merge(
|
||||
loadedLocalTags$,
|
||||
loadedRemoteTags$,
|
||||
loadedEmptyTags$
|
||||
).pipe(take(1));
|
||||
|
||||
const seekWithoutCues = (
|
||||
seekTime: number
|
||||
): Observable<SegmentComponent<ClusterType>> => {
|
||||
const request$ = loadedMetadata$.pipe(
|
||||
switchMap(() =>
|
||||
createRangedEbmlStream({
|
||||
...matroskaOptions,
|
||||
byteStart: seekSystem.firstClusterOffset,
|
||||
})
|
||||
)
|
||||
);
|
||||
const cluster$ = request$.pipe(
|
||||
switchMap((req) => req.ebml$),
|
||||
filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)),
|
||||
map((tag) => clusterSystem.addClusterWithTag(tag))
|
||||
);
|
||||
|
||||
if (seekTime === 0) {
|
||||
return cluster$;
|
||||
}
|
||||
|
||||
return cluster$.pipe(
|
||||
scan(
|
||||
(acc, curr) => {
|
||||
// avoid object recreation
|
||||
acc.prev = acc.next;
|
||||
acc.next = curr;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
prev: undefined as SegmentComponent<ClusterType> | undefined,
|
||||
next: undefined as SegmentComponent<ClusterType> | undefined,
|
||||
}
|
||||
),
|
||||
filter((c) => c.next?.Timestamp! > seekTime),
|
||||
map((c) => c.prev ?? c.next!)
|
||||
);
|
||||
};
|
||||
|
||||
const seekWithCues = (
|
||||
cueSystem: CueSystem,
|
||||
seekTime: number
|
||||
): Observable<SegmentComponent<ClusterType>> => {
|
||||
if (seekTime === 0) {
|
||||
return seekWithoutCues(seekTime);
|
||||
}
|
||||
|
||||
const cuePoint = cueSystem.findClosestCue(seekTime);
|
||||
|
||||
if (!cuePoint) {
|
||||
return seekWithoutCues(seekTime);
|
||||
}
|
||||
|
||||
return createRangedEbmlStream({
|
||||
...matroskaOptions,
|
||||
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<SegmentComponent<ClusterType>> => {
|
||||
if (seekTime === 0) {
|
||||
const subscription = loadedCues$.subscribe();
|
||||
|
||||
// if seekTime equals to 0 at start, reuse the initialize stream
|
||||
return seekWithoutCues(seekTime).pipe(
|
||||
finalize(() => {
|
||||
subscription.unsubscribe();
|
||||
})
|
||||
);
|
||||
}
|
||||
return loadedCues$.pipe(
|
||||
switchMap((segment) => {
|
||||
const cueSystem = segment.cue;
|
||||
if (cueSystem.prepared) {
|
||||
return seekWithCues(cueSystem, seekTime);
|
||||
}
|
||||
return seekWithoutCues(seekTime);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const videoTrackDecoder = (
|
||||
track: VideoTrackContext,
|
||||
cluster$: Observable<ClusterType>
|
||||
) => {
|
||||
const { decoder, frame$ } = createVideoDecodeStream(track.configuration);
|
||||
|
||||
const clusterSystem = segment.cluster;
|
||||
|
||||
const decodeSubscription = cluster$.subscribe((cluster) => {
|
||||
for (const block of clusterSystem.enumerateBlocks(
|
||||
cluster,
|
||||
track.trackEntry
|
||||
)) {
|
||||
const blockTime = Number(cluster.Timestamp) + block.relTime;
|
||||
const blockDuration =
|
||||
frames.length > 1 ? track.predictBlockDuration(blockTime) : 0;
|
||||
const perFrameDuration =
|
||||
frames.length > 1 && blockDuration
|
||||
? blockDuration / block.frames.length
|
||||
: 0;
|
||||
|
||||
for (const frame of block.frames) {
|
||||
const chunk = new EncodedVideoChunk({
|
||||
type: block.keyframe ? 'key' : 'delta',
|
||||
data: frame,
|
||||
timestamp: blockTime + perFrameDuration,
|
||||
});
|
||||
|
||||
decoder.decode(chunk);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
track,
|
||||
decoder,
|
||||
frame$: frame$
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
decodeSubscription.unsubscribe();
|
||||
})
|
||||
)
|
||||
.pipe(share()),
|
||||
};
|
||||
};
|
||||
|
||||
const audioTrackDecoder = (
|
||||
track: AudioTrackContext,
|
||||
cluster$: Observable<ClusterType>
|
||||
) => {
|
||||
const { decoder, frame$ } = createAudioDecodeStream(track.configuration);
|
||||
|
||||
const clusterSystem = segment.cluster;
|
||||
|
||||
const decodeSubscription = cluster$.subscribe((cluster) => {
|
||||
for (const block of clusterSystem.enumerateBlocks(
|
||||
cluster,
|
||||
track.trackEntry
|
||||
)) {
|
||||
const blockTime = Number(cluster.Timestamp) + block.relTime;
|
||||
const blockDuration =
|
||||
frames.length > 1 ? track.predictBlockDuration(blockTime) : 0;
|
||||
const perFrameDuration =
|
||||
frames.length > 1 && blockDuration
|
||||
? blockDuration / block.frames.length
|
||||
: 0;
|
||||
|
||||
let i = 0;
|
||||
for (const frame of block.frames) {
|
||||
const chunk = new EncodedAudioChunk({
|
||||
type: block.keyframe ? 'key' : 'delta',
|
||||
data: frame,
|
||||
timestamp: blockTime + perFrameDuration * i,
|
||||
});
|
||||
i++;
|
||||
|
||||
decoder.decode(chunk);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
track,
|
||||
decoder,
|
||||
frame$: frame$.pipe(finalize(() => decodeSubscription.unsubscribe())),
|
||||
};
|
||||
};
|
||||
|
||||
const defaultVideoTrack$ = loadedMetadata$.pipe(
|
||||
map((segment) =>
|
||||
segment.track.getTrackContext<VideoTrackContext>({
|
||||
predicate: (track) =>
|
||||
track.TrackType === TrackTypeRestrictionEnum.VIDEO &&
|
||||
standardTrackPredicate(track),
|
||||
priority: standardTrackPriority,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const defaultAudioTrack$ = loadedMetadata$.pipe(
|
||||
map((segment) =>
|
||||
segment.track.getTrackContext<AudioTrackContext>({
|
||||
predicate: (track) =>
|
||||
track.TrackType === TrackTypeRestrictionEnum.AUDIO &&
|
||||
standardTrackPredicate(track),
|
||||
priority: standardTrackPriority,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
startTag,
|
||||
segment,
|
||||
metadataTags$,
|
||||
loadedMetadata$,
|
||||
loadedTags$,
|
||||
loadedCues$,
|
||||
seek,
|
||||
videoTrackDecoder,
|
||||
audioTrackDecoder,
|
||||
defaultVideoTrack$,
|
||||
defaultAudioTrack$,
|
||||
};
|
||||
}
|
@ -1,399 +0,0 @@
|
||||
import {
|
||||
EbmlStreamDecoder,
|
||||
EbmlTagIdEnum,
|
||||
EbmlTagPosition,
|
||||
type EbmlTagType,
|
||||
} from 'konoebml';
|
||||
import {
|
||||
defer,
|
||||
EMPTY,
|
||||
filter,
|
||||
finalize,
|
||||
from,
|
||||
isEmpty, last,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
of,
|
||||
scan,
|
||||
share,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
takeWhile,
|
||||
withLatestFrom,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
createRangedStream,
|
||||
type CreateRangedStreamOptions,
|
||||
} from '@konoplayer/core/data';
|
||||
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 {
|
||||
}
|
||||
|
||||
export function createRangedEbmlStream({
|
||||
url,
|
||||
byteStart = 0,
|
||||
byteEnd,
|
||||
}: CreateRangedEbmlStreamOptions): Observable<{
|
||||
ebml$: Observable<EbmlTagType>;
|
||||
totalSize?: number;
|
||||
response: Response;
|
||||
body: ReadableStream<Uint8Array>;
|
||||
controller: AbortController;
|
||||
}> {
|
||||
const stream$ = from(createRangedStream({ url, byteStart, byteEnd }));
|
||||
|
||||
return stream$.pipe(
|
||||
switchMap(({ controller, body, totalSize, response }) => {
|
||||
let requestCompleted = false;
|
||||
|
||||
const originRequest$ = new Observable<EbmlTagType>((subscriber) => {
|
||||
body
|
||||
.pipeThrough(
|
||||
new EbmlStreamDecoder({
|
||||
streamStartOffset: byteStart,
|
||||
collectChild: (child) => child.id !== EbmlTagIdEnum.Cluster,
|
||||
backpressure: {
|
||||
eventLoop: waitTick,
|
||||
},
|
||||
})
|
||||
)
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write: async (tag) => {
|
||||
await waitTick();
|
||||
subscriber.next(tag);
|
||||
},
|
||||
close: () => {
|
||||
if (!requestCompleted) {
|
||||
requestCompleted = true;
|
||||
subscriber.complete();
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
if (requestCompleted && error?.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
requestCompleted = true;
|
||||
subscriber.error(error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
requestCompleted = true;
|
||||
controller.abort();
|
||||
};
|
||||
}).pipe(
|
||||
share({
|
||||
resetOnComplete: false,
|
||||
resetOnError: false,
|
||||
resetOnRefCountZero: true,
|
||||
})
|
||||
);
|
||||
|
||||
const ebml$ = defer(() =>
|
||||
requestCompleted ? EMPTY : originRequest$
|
||||
).pipe(
|
||||
share({
|
||||
resetOnError: false,
|
||||
resetOnComplete: true,
|
||||
resetOnRefCountZero: true,
|
||||
})
|
||||
);
|
||||
|
||||
return of({
|
||||
ebml$,
|
||||
totalSize,
|
||||
response,
|
||||
body,
|
||||
controller,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export interface CreateEbmlControllerOptions
|
||||
extends Omit<CreateRangedEbmlStreamOptions, 'byteStart' | 'byteEnd'> {}
|
||||
|
||||
export function createEbmlController({
|
||||
url,
|
||||
...options
|
||||
}: CreateEbmlControllerOptions) {
|
||||
const metaRequest$ = createRangedEbmlStream({
|
||||
...options,
|
||||
url,
|
||||
byteStart: 0,
|
||||
});
|
||||
|
||||
const controller$ = metaRequest$.pipe(
|
||||
map(({ totalSize, ebml$, response, controller }) => {
|
||||
const head$ = ebml$.pipe(
|
||||
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
|
||||
take(1),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
console.debug(
|
||||
`stream of video "${url}" created, total size is ${totalSize ?? 'unknown'}`
|
||||
);
|
||||
|
||||
const segmentStart$ = ebml$.pipe(
|
||||
filter(isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.Start))
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 metaScan$ = ebml$.pipe(
|
||||
scan(
|
||||
(acc, tag) => {
|
||||
acc.segment.scanMeta(tag);
|
||||
acc.tag = tag;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
segment,
|
||||
tag: undefined as unknown as EbmlTagType,
|
||||
}
|
||||
),
|
||||
takeWhile((acc) => acc.segment.canCompleteMeta(), true),
|
||||
share({
|
||||
resetOnComplete: false,
|
||||
resetOnError: false,
|
||||
resetOnRefCountZero: true,
|
||||
})
|
||||
);
|
||||
|
||||
const meta$ = metaScan$.pipe(
|
||||
map(({ tag }) => tag)
|
||||
);
|
||||
|
||||
const withMeta$ = metaScan$.pipe(
|
||||
last(),
|
||||
switchMap(({ segment }) => segment.completeMeta()),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const withRemoteCues$ = withMeta$.pipe(
|
||||
switchMap((s) => {
|
||||
const cueSystem = s.cue;
|
||||
const seekSystem = s.seek;
|
||||
if (cueSystem.prepared) {
|
||||
return EMPTY;
|
||||
}
|
||||
const remoteCuesTagStartOffset =
|
||||
seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_CUES);
|
||||
if (remoteCuesTagStartOffset! >= 0) {
|
||||
return createRangedEbmlStream({
|
||||
...options,
|
||||
url,
|
||||
byteStart: remoteCuesTagStartOffset,
|
||||
}).pipe(
|
||||
switchMap((req) => req.ebml$),
|
||||
filter(isTagIdPos(EbmlTagIdEnum.Cues, EbmlTagPosition.End)),
|
||||
withLatestFrom(withMeta$),
|
||||
map(([cues, withMeta]) => {
|
||||
withMeta.cue.prepareCuesWithTag(cues);
|
||||
return withMeta;
|
||||
})
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
take(1),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const withLocalCues$ = withMeta$.pipe(
|
||||
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.prepareTagsWithTag(tags);
|
||||
return withMeta;
|
||||
})
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
take(1),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const withLocalTags$ = withMeta$.pipe(
|
||||
switchMap((s) => (s.tag.prepared ? of(s) : EMPTY)),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const withCues$ = merge(withLocalCues$, withRemoteCues$).pipe(
|
||||
take(1)
|
||||
);
|
||||
|
||||
const withoutCues$ = withCues$.pipe(
|
||||
isEmpty(),
|
||||
switchMap((empty) => (empty ? withMeta$ : EMPTY))
|
||||
);
|
||||
|
||||
const withTags$ = merge(withLocalTags$, withRemoteTags$).pipe(
|
||||
take(1)
|
||||
);
|
||||
|
||||
const withoutTags$ = withTags$.pipe(
|
||||
isEmpty(),
|
||||
switchMap((empty) => (empty ? withMeta$ : EMPTY))
|
||||
);
|
||||
|
||||
const seekWithoutCues = (
|
||||
seekTime: number
|
||||
): Observable<SegmentComponent<ClusterType>> => {
|
||||
const request$ = withMeta$.pipe(
|
||||
switchMap(() =>
|
||||
createRangedEbmlStream({
|
||||
...options,
|
||||
url,
|
||||
byteStart: seekSystem.firstClusterOffset,
|
||||
})
|
||||
)
|
||||
);
|
||||
const cluster$ = request$.pipe(
|
||||
switchMap((req) => req.ebml$),
|
||||
filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)),
|
||||
map((tag) => clusterSystem.addClusterWithTag(tag))
|
||||
);
|
||||
|
||||
if (seekTime === 0) {
|
||||
return cluster$;
|
||||
}
|
||||
|
||||
return cluster$.pipe(
|
||||
scan(
|
||||
(acc, curr) => {
|
||||
// avoid object recreation
|
||||
acc.prev = acc.next;
|
||||
acc.next = curr;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
prev: undefined as SegmentComponent<ClusterType> | undefined,
|
||||
next: undefined as SegmentComponent<ClusterType> | undefined,
|
||||
}
|
||||
),
|
||||
filter((c) => c.next?.Timestamp! > seekTime),
|
||||
map((c) => c.prev ?? c.next!)
|
||||
);
|
||||
};
|
||||
|
||||
const seekWithCues = (
|
||||
cueSystem: CueSystem,
|
||||
seekTime: number
|
||||
): Observable<SegmentComponent<ClusterType>> => {
|
||||
if (seekTime === 0) {
|
||||
return seekWithoutCues(seekTime);
|
||||
}
|
||||
|
||||
const cuePoint = cueSystem.findClosestCue(seekTime);
|
||||
|
||||
if (!cuePoint) {
|
||||
return seekWithoutCues(seekTime);
|
||||
}
|
||||
|
||||
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<SegmentComponent<ClusterType>> => {
|
||||
if (seekTime === 0) {
|
||||
const subscription = merge(withCues$, withoutCues$).subscribe();
|
||||
|
||||
// if seekTime equals to 0 at start, reuse the initialize stream
|
||||
return seekWithoutCues(seekTime).pipe(
|
||||
finalize(() => {
|
||||
subscription.unsubscribe();
|
||||
})
|
||||
);
|
||||
}
|
||||
return merge(
|
||||
withCues$.pipe(switchMap((s) => seekWithCues(s.cue, seekTime))),
|
||||
withoutCues$.pipe(switchMap((_) => seekWithoutCues(seekTime)))
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
startTag,
|
||||
head$,
|
||||
segment,
|
||||
meta$,
|
||||
withMeta$,
|
||||
withCues$,
|
||||
withoutCues$,
|
||||
withTags$,
|
||||
withoutTags$,
|
||||
seekWithCues,
|
||||
seekWithoutCues,
|
||||
seek,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
segments$,
|
||||
head$,
|
||||
totalSize,
|
||||
ebml$,
|
||||
controller,
|
||||
response,
|
||||
};
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
return {
|
||||
controller$,
|
||||
request$: metaRequest$,
|
||||
};
|
||||
}
|
@ -2,8 +2,11 @@ import { type, match } from 'arktype';
|
||||
import { EbmlTagIdEnum, EbmlSimpleBlockTag, EbmlBlockTag } from 'konoebml';
|
||||
|
||||
export const BinarySchema = type.instanceOf(Uint8Array);
|
||||
export type BinaryType = typeof BinarySchema.infer;
|
||||
export const SimpleBlockSchema = type.instanceOf(EbmlSimpleBlockTag);
|
||||
export const BlockSchema = type.instanceOf(EbmlBlockTag);
|
||||
export type SimpleBlockType = typeof SimpleBlockSchema.infer;
|
||||
export type BlockType = typeof BlockSchema.infer;
|
||||
|
||||
export const DocTypeExtensionSchema = type({
|
||||
DocTypeExtensionName: type.string,
|
||||
|
@ -1,6 +1,65 @@
|
||||
import type {EbmlClusterTagType} from "konoebml";
|
||||
import {ClusterSchema, type ClusterType} from "../schema";
|
||||
import {type SegmentComponent, SegmentComponentSystemTrait} from "./segment";
|
||||
import type { EbmlClusterTagType } from 'konoebml';
|
||||
import {
|
||||
ClusterSchema,
|
||||
type SimpleBlockType,
|
||||
type ClusterType,
|
||||
type BlockGroupType,
|
||||
type TrackEntryType,
|
||||
} from '../schema';
|
||||
import { type SegmentComponent, SegmentComponentSystemTrait } from './segment';
|
||||
|
||||
export abstract class BlockViewTrait {
|
||||
abstract get keyframe(): boolean;
|
||||
|
||||
abstract get frames(): Uint8Array[];
|
||||
|
||||
abstract get trackNum(): number | bigint;
|
||||
|
||||
abstract get relTime(): number;
|
||||
}
|
||||
|
||||
export class SimpleBlockView extends BlockViewTrait {
|
||||
constructor(public readonly block: SimpleBlockType) {
|
||||
super();
|
||||
}
|
||||
|
||||
get keyframe() {
|
||||
return !!this.block.keyframe;
|
||||
}
|
||||
|
||||
get frames(): Uint8Array<ArrayBufferLike>[] {
|
||||
return this.block.frames;
|
||||
}
|
||||
|
||||
get trackNum() {
|
||||
return this.block.track;
|
||||
}
|
||||
|
||||
get relTime() {
|
||||
return this.block.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class BlockGroupView extends BlockViewTrait {
|
||||
constructor(public readonly block: BlockGroupType) {
|
||||
super();
|
||||
}
|
||||
|
||||
get keyframe() {
|
||||
return !this.block.ReferenceBlock;
|
||||
}
|
||||
|
||||
get frames(): Uint8Array<ArrayBufferLike>[] {
|
||||
return this.block.Block.frames;
|
||||
}
|
||||
get trackNum() {
|
||||
return this.block.Block.track;
|
||||
}
|
||||
|
||||
get relTime() {
|
||||
return this.block.Block.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterSystem extends SegmentComponentSystemTrait<
|
||||
EbmlClusterTagType,
|
||||
@ -14,7 +73,27 @@ export class ClusterSystem extends SegmentComponentSystemTrait<
|
||||
|
||||
addClusterWithTag(tag: EbmlClusterTagType) {
|
||||
const cluster = this.componentFromTag(tag);
|
||||
this.clustersBuffer.push(cluster);
|
||||
// this.clustersBuffer.push(cluster);
|
||||
return cluster;
|
||||
}
|
||||
}
|
||||
|
||||
*enumerateBlocks(
|
||||
cluster: ClusterType,
|
||||
track: TrackEntryType
|
||||
): Generator<BlockViewTrait> {
|
||||
if (cluster.SimpleBlock) {
|
||||
for (const block of cluster.SimpleBlock) {
|
||||
if (block.track === track.TrackNumber) {
|
||||
yield new SimpleBlockView(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cluster.BlockGroup) {
|
||||
for (const block of cluster.BlockGroup) {
|
||||
if (block.Block.track === track.TrackNumber) {
|
||||
yield new BlockGroupView(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +1,69 @@
|
||||
import {ParseCodecErrors, UnsupportedCodecError} from "@konoplayer/core/errors.ts";
|
||||
import {
|
||||
ParseCodecErrors,
|
||||
UnsupportedCodecError,
|
||||
} from '@konoplayer/core/errors.ts';
|
||||
import {
|
||||
EbmlTagIdEnum,
|
||||
type EbmlTrackEntryTagType,
|
||||
type EbmlTracksTagType
|
||||
} from "konoebml";
|
||||
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";
|
||||
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;
|
||||
predicate: (v: SegmentComponent<TrackEntryType>) => boolean;
|
||||
}
|
||||
|
||||
export abstract class TrackContext {
|
||||
peekingKeyframe?: Uint8Array;
|
||||
trackEntry: TrackEntryType
|
||||
trackEntry: TrackEntryType;
|
||||
timecodeScale: number;
|
||||
lastBlockTimestamp = Number.NaN;
|
||||
averageBlockDuration = Number.NaN;
|
||||
|
||||
constructor(trackEntry: TrackEntryType) {
|
||||
constructor(trackEntry: TrackEntryType, timecodeScale: number) {
|
||||
this.trackEntry = trackEntry;
|
||||
this.timecodeScale = timecodeScale;
|
||||
}
|
||||
|
||||
peekKeyframe (payload: Uint8Array) {
|
||||
peekKeyframe(payload: Uint8Array) {
|
||||
this.peekingKeyframe = payload;
|
||||
}
|
||||
|
||||
preparedToConfigure () {
|
||||
preparedToConfigure() {
|
||||
if (this.requirePeekKeyframe()) {
|
||||
return !!this.peekingKeyframe;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract requirePeekKeyframe (): boolean;
|
||||
abstract requirePeekKeyframe(): boolean;
|
||||
|
||||
abstract buildConfiguration (): Promise<void>;
|
||||
abstract buildConfiguration(): Promise<void>;
|
||||
|
||||
predictBlockDuration(blockTimestamp: number): number {
|
||||
if (this.trackEntry.DefaultDuration) {
|
||||
return Number(this.trackEntry.DefaultDuration);
|
||||
}
|
||||
const delta = blockTimestamp - this.lastBlockTimestamp;
|
||||
this.lastBlockTimestamp = blockTimestamp;
|
||||
this.averageBlockDuration = this.averageBlockDuration
|
||||
? this.averageBlockDuration * 0.5 + delta * 0.5
|
||||
: delta;
|
||||
return this.averageBlockDuration;
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultTrackContext extends TrackContext {
|
||||
@ -46,18 +71,22 @@ export class DefaultTrackContext extends TrackContext {
|
||||
return false;
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: <explanation>
|
||||
override async buildConfiguration(): Promise<void> {}
|
||||
}
|
||||
|
||||
export class VideoTrackContext extends TrackContext {
|
||||
configuration!: VideoDecoderConfigExt;
|
||||
|
||||
override requirePeekKeyframe (): boolean {
|
||||
override requirePeekKeyframe(): boolean {
|
||||
return videoCodecIdRequirePeekingKeyframe(this.trackEntry.CodecID);
|
||||
}
|
||||
|
||||
async buildConfiguration () {
|
||||
const configuration = videoCodecIdToWebCodecs(this.trackEntry, this.peekingKeyframe);
|
||||
async buildConfiguration() {
|
||||
const configuration = videoCodecIdToWebCodecs(
|
||||
this.trackEntry,
|
||||
this.peekingKeyframe
|
||||
);
|
||||
if (await VideoDecoder.isConfigSupported(configuration)) {
|
||||
throw new UnsupportedCodecError(configuration.codec, 'video decoder');
|
||||
}
|
||||
@ -68,21 +97,50 @@ export class VideoTrackContext extends TrackContext {
|
||||
export class AudioTrackContext extends TrackContext {
|
||||
configuration!: AudioDecoderConfigExt;
|
||||
|
||||
override requirePeekKeyframe (): boolean {
|
||||
override requirePeekKeyframe(): boolean {
|
||||
return videoCodecIdRequirePeekingKeyframe(this.trackEntry.CodecID);
|
||||
}
|
||||
|
||||
async buildConfiguration () {
|
||||
const configuration = audioCodecIdToWebCodecs(this.trackEntry, this.peekingKeyframe);
|
||||
async buildConfiguration() {
|
||||
const configuration = audioCodecIdToWebCodecs(
|
||||
this.trackEntry,
|
||||
this.peekingKeyframe
|
||||
);
|
||||
if (await AudioDecoder.isConfigSupported(configuration)) {
|
||||
throw new UnsupportedCodecError(configuration.codec, 'audio decoder');
|
||||
}
|
||||
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
override predictBlockDuration(blockTimestamp: number): number {
|
||||
if (this.trackEntry.DefaultDuration) {
|
||||
return Number(this.trackEntry.DefaultDuration);
|
||||
}
|
||||
if (this.configuration.samplesPerFrame) {
|
||||
return (
|
||||
Number(
|
||||
this.configuration.samplesPerFrame / this.configuration.sampleRate
|
||||
) *
|
||||
(1_000_000_000 / Number(this.timecodeScale))
|
||||
);
|
||||
}
|
||||
const delta = blockTimestamp - this.lastBlockTimestamp;
|
||||
this.lastBlockTimestamp = blockTimestamp;
|
||||
this.averageBlockDuration = this.averageBlockDuration
|
||||
? this.averageBlockDuration * 0.5 + delta * 0.5
|
||||
: delta;
|
||||
return this.averageBlockDuration;
|
||||
}
|
||||
}
|
||||
|
||||
export function standardTrackPredicate(track: TrackEntryType) {
|
||||
return track.FlagEnabled !== 0;
|
||||
}
|
||||
|
||||
export function standardTrackPriority(track: TrackEntryType) {
|
||||
return (Number(!!track.FlagForced) << 8) + (Number(!!track.FlagDefault) << 4);
|
||||
}
|
||||
|
||||
export class TrackSystem extends SegmentComponentSystemTrait<
|
||||
EbmlTrackEntryTagType,
|
||||
@ -96,37 +154,45 @@ export class TrackSystem extends SegmentComponentSystemTrait<
|
||||
trackContexts: Map<number | bigint, TrackContext> = new Map();
|
||||
|
||||
getTrackEntry({
|
||||
priority = (track) =>
|
||||
(Number(!!track.FlagForced) << 4) + Number(!!track.FlagDefault),
|
||||
predicate = (track) => track.FlagEnabled !== 0,
|
||||
}: GetTrackEntryOptions) {
|
||||
priority = standardTrackPriority,
|
||||
predicate,
|
||||
}: GetTrackEntryOptions) {
|
||||
return this.tracks
|
||||
.filter(predicate)
|
||||
.toSorted((a, b) => priority(b) - priority(a))
|
||||
.at(0);
|
||||
}
|
||||
|
||||
getTrackContext <T extends TrackContext>(options: GetTrackEntryOptions): T | undefined {
|
||||
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) {
|
||||
const infoSystem = this.segment.info;
|
||||
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))
|
||||
this.trackContexts.set(
|
||||
track.TrackNumber,
|
||||
new VideoTrackContext(track, Number(infoSystem.info.TimestampScale))
|
||||
);
|
||||
} else if (track.TrackType === TrackTypeRestrictionEnum.AUDIO) {
|
||||
this.trackContexts.set(track.TrackNumber, new AudioTrackContext(track))
|
||||
this.trackContexts.set(
|
||||
track.TrackNumber,
|
||||
new AudioTrackContext(track, Number(infoSystem.info.TimestampScale))
|
||||
);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
async buildTracksConfiguration () {
|
||||
async buildTracksConfiguration() {
|
||||
const parseErrors = new ParseCodecErrors();
|
||||
|
||||
for (const context of this.trackContexts.values()) {
|
||||
@ -141,15 +207,15 @@ export class TrackSystem extends SegmentComponentSystemTrait<
|
||||
}
|
||||
}
|
||||
|
||||
tryPeekKeyframe (tag: { track: number | bigint, frames: Uint8Array[] }) {
|
||||
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])
|
||||
c.peekKeyframe(tag.frames?.[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preparedToConfigureTracks (): boolean {
|
||||
preparedToConfigureTracks(): boolean {
|
||||
for (const c of this.trackContexts.values()) {
|
||||
if (!c.preparedToConfigure()) {
|
||||
return false;
|
||||
@ -157,4 +223,4 @@ export class TrackSystem extends SegmentComponentSystemTrait<
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -36,6 +36,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22.13.11
|
||||
version: 22.13.11
|
||||
'@webgpu/types':
|
||||
specifier: ^0.1.59
|
||||
version: 0.1.59
|
||||
change-case:
|
||||
specifier: ^5.4.4
|
||||
version: 5.4.4
|
||||
@ -1151,6 +1154,9 @@ packages:
|
||||
'@webassemblyjs/wast-printer@1.14.1':
|
||||
resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==}
|
||||
|
||||
'@webgpu/types@0.1.59':
|
||||
resolution: {integrity: sha512-jZJ6ipNli+rn++/GAPqsZXfsgjx951wlCW7vNAg+oGdp0ZYidTOkbVTVeK2frzowuD5ch7MRz7leOEX1PMv43A==}
|
||||
|
||||
'@xhmikosr/archive-type@7.0.0':
|
||||
resolution: {integrity: sha512-sIm84ZneCOJuiy3PpWR5bxkx3HaNt1pqaN+vncUBZIlPZCq8ASZH+hBVdu5H8znR7qYC6sKwx+ie2Q7qztJTxA==}
|
||||
engines: {node: ^14.14.0 || >=16.0.0}
|
||||
@ -4015,6 +4021,8 @@ snapshots:
|
||||
'@webassemblyjs/ast': 1.14.1
|
||||
'@xtuc/long': 4.2.2
|
||||
|
||||
'@webgpu/types@0.1.59': {}
|
||||
|
||||
'@xhmikosr/archive-type@7.0.0':
|
||||
dependencies:
|
||||
file-type: 19.6.0
|
||||
|
@ -342,10 +342,15 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) {
|
||||
const idMulti = new Set<string>();
|
||||
const preDefs = [
|
||||
'export const BinarySchema = type.instanceOf(Uint8Array);',
|
||||
'export type BinaryType = typeof BinarySchema.infer;',
|
||||
...Object.entries(AdHocType).map(
|
||||
([name, meta]) =>
|
||||
`export const ${meta.primitive()} = type.instanceOf(Ebml${name}Tag);`
|
||||
),
|
||||
...Object.entries(AdHocType).map(
|
||||
([name, meta]) =>
|
||||
`export type ${name}Type = typeof ${meta.primitive()}.infer;`
|
||||
),
|
||||
];
|
||||
|
||||
const generateAssociated = (el: EbmlElementType): string | undefined => {
|
||||
|
@ -14,6 +14,10 @@
|
||||
"DOM.AsyncIterable",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"types": [
|
||||
"@webgpu/types",
|
||||
"@types/node"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"moduleResolution": "bundler",
|
||||
|
Loading…
Reference in New Issue
Block a user