feat: temp save
This commit is contained in:
parent
39a4cf2773
commit
3c317627e7
@ -15,18 +15,18 @@ import {
|
|||||||
take,
|
take,
|
||||||
tap,
|
tap,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
fromEvent,
|
fromEvent, withLatestFrom, share, delay, delayWhen, from, of,
|
||||||
filter,
|
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { createEbmlController } from '@konoplayer/matroska/reactive';
|
import { createEbmlController } from '@konoplayer/matroska/reactive';
|
||||||
import {
|
import {
|
||||||
TrackTypeRestrictionEnum,
|
TrackTypeRestrictionEnum,
|
||||||
type ClusterType,
|
type ClusterType,
|
||||||
} from '@konoplayer/matroska/schema';
|
} from '@konoplayer/matroska/schema';
|
||||||
import type { SegmentComponent } from '@konoplayer/matroska/model';
|
|
||||||
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
||||||
import { Queue } from 'mnemonist';
|
import { Queue } from 'mnemonist';
|
||||||
|
|
||||||
|
import type {SegmentComponent, AudioTrackContext, VideoTrackContext} from "@konoplayer/matroska/systems";
|
||||||
|
|
||||||
export class VideoPipelineDemo extends LitElement {
|
export class VideoPipelineDemo extends LitElement {
|
||||||
static styles = css``;
|
static styles = css``;
|
||||||
|
|
||||||
@ -43,6 +43,7 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
audioContext = new AudioContext();
|
audioContext = new AudioContext();
|
||||||
|
|
||||||
seek$ = new ReplaySubject<number>(1);
|
seek$ = new ReplaySubject<number>(1);
|
||||||
|
|
||||||
cluster$ = new Subject<SegmentComponent<ClusterType>>();
|
cluster$ = new Subject<SegmentComponent<ClusterType>>();
|
||||||
videoFrameBuffer$ = new BehaviorSubject(new Queue<VideoFrame>());
|
videoFrameBuffer$ = new BehaviorSubject(new Queue<VideoFrame>());
|
||||||
audioFrameBuffer$ = new BehaviorSubject(new Queue<AudioData>());
|
audioFrameBuffer$ = new BehaviorSubject(new Queue<AudioData>());
|
||||||
@ -62,37 +63,47 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
url: src,
|
url: src,
|
||||||
});
|
});
|
||||||
|
|
||||||
const segment$ = controller$.pipe(
|
const segmentContext$ = controller$.pipe(
|
||||||
switchMap(({ segments$ }) => segments$.pipe(take(1)))
|
switchMap(({ segments$ }) => segments$.pipe(take(1)))
|
||||||
);
|
);
|
||||||
|
|
||||||
const cluster$ = combineLatest({
|
const videoTrack$ = segmentContext$.pipe(
|
||||||
seekTime: this.seek$,
|
|
||||||
segment: segment$,
|
|
||||||
}).pipe(switchMap(({ seekTime, segment }) => segment.seek(seekTime)));
|
|
||||||
|
|
||||||
const decode$ = segment$.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),
|
||||||
|
})),
|
||||||
|
share()
|
||||||
|
);
|
||||||
|
|
||||||
|
const decodeVideo$ = currentCluster$.pipe(
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
const decode$ = segmentContext$.pipe(
|
||||||
switchMap(({ withMeta$ }) => withMeta$),
|
switchMap(({ withMeta$ }) => withMeta$),
|
||||||
map((segment) => {
|
map((segment) => {
|
||||||
const trackSystem = segment.track;
|
const trackSystem = segment.track;
|
||||||
const infoSystem = segment.info;
|
const infoSystem = segment.info;
|
||||||
const tracks = {
|
const videoTrack = trackSystem.getTrackContext<VideoTrackContext>({
|
||||||
video: trackSystem.getTrackEntry({
|
|
||||||
predicate: (c) =>
|
predicate: (c) =>
|
||||||
c.TrackType === TrackTypeRestrictionEnum.VIDEO &&
|
c.TrackType === TrackTypeRestrictionEnum.VIDEO &&
|
||||||
c.FlagEnabled !== 0,
|
c.FlagEnabled !== 0,
|
||||||
}),
|
});
|
||||||
audio: trackSystem.getTrackEntry({
|
const audioTrack = trackSystem.getTrackContext({
|
||||||
predicate: (c) =>
|
predicate: (c) =>
|
||||||
c.TrackType === TrackTypeRestrictionEnum.AUDIO &&
|
c.TrackType === TrackTypeRestrictionEnum.AUDIO &&
|
||||||
c.FlagEnabled !== 0,
|
c.FlagEnabled !== 0,
|
||||||
}),
|
});
|
||||||
subtitle: trackSystem.getTrackEntry({
|
|
||||||
predicate: (c) =>
|
const videoDecode$ = track
|
||||||
c.TrackType === TrackTypeRestrictionEnum.SUBTITLE &&
|
|
||||||
c.FlagEnabled !== 0,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const videoDecode$ = tracks.video
|
const videoDecode$ = tracks.video
|
||||||
? new Observable<VideoFrame>((subscriber) => {
|
? new Observable<VideoFrame>((subscriber) => {
|
||||||
@ -354,7 +365,7 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
this.pipeline$$.add(video$.subscribe());
|
this.pipeline$$.add(video$.subscribe());
|
||||||
this.pipeline$$.add(addToVideoFrameBuffer$.subscribe());
|
this.pipeline$$.add(addToVideoFrameBuffer$.subscribe());
|
||||||
this.pipeline$$.add(addToAudioFrameBuffer$.subscribe());
|
this.pipeline$$.add(addToAudioFrameBuffer$.subscribe());
|
||||||
this.pipeline$$.add(cluster$.subscribe(this.cluster$));
|
this.pipeline$$.add(currentCluster$.subscribe(this.cluster$));
|
||||||
this.pipeline$$.add(
|
this.pipeline$$.add(
|
||||||
fromEvent(document.body, 'click').subscribe(() => {
|
fromEvent(document.body, 'click').subscribe(() => {
|
||||||
this.audioContext.resume();
|
this.audioContext.resume();
|
||||||
|
356
packages/core/src/elements/media-trait.ts
Normal file
356
packages/core/src/elements/media-trait.ts
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
14
packages/core/src/elements/state.ts
Normal file
14
packages/core/src/elements/state.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -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 { VideoCodec, AudioCodec } from '@konoplayer/core/codecs';
|
||||||
import type { TrackEntryType } from '../schema';
|
import type { TrackEntryType } from '../schema';
|
||||||
import {
|
import {
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
} from './hevc.ts';
|
} from './hevc.ts';
|
||||||
import {
|
import {
|
||||||
genCodecStringByVP9DecoderConfigurationRecord,
|
genCodecStringByVP9DecoderConfigurationRecord,
|
||||||
parseVP9DecoderConfigurationRecord,
|
parseVP9DecoderConfigurationRecord, VP9_CODEC_TYPE,
|
||||||
} from './vp9.ts';
|
} from './vp9.ts';
|
||||||
|
|
||||||
export const VideoCodecId = {
|
export const VideoCodecId = {
|
||||||
@ -122,9 +122,13 @@ export interface VideoDecoderConfigExt extends VideoDecoderConfig {
|
|||||||
codecType: VideoCodec;
|
codecType: VideoCodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function videoCodecIdRequirePeekingKeyframe(codecId: VideoCodecIdType) {
|
||||||
|
return codecId === VideoCodecId.VP9
|
||||||
|
}
|
||||||
|
|
||||||
export function videoCodecIdToWebCodecs(
|
export function videoCodecIdToWebCodecs(
|
||||||
track: TrackEntryType,
|
track: TrackEntryType,
|
||||||
keyframe: Uint8Array
|
keyframe: Uint8Array | undefined
|
||||||
): VideoDecoderConfigExt {
|
): VideoDecoderConfigExt {
|
||||||
const codecId = track.CodecID;
|
const codecId = track.CodecID;
|
||||||
const codecPrivate = track.CodecPrivate;
|
const codecPrivate = track.CodecPrivate;
|
||||||
@ -141,6 +145,9 @@ export function videoCodecIdToWebCodecs(
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
case VideoCodecId.VP9:
|
case VideoCodecId.VP9:
|
||||||
|
if (!keyframe) {
|
||||||
|
throw new ParseCodecError(VP9_CODEC_TYPE, 'keyframe is required to parse VP9 codec')
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...shareOptions,
|
...shareOptions,
|
||||||
codecType: VideoCodec.VP9,
|
codecType: VideoCodec.VP9,
|
||||||
@ -195,8 +202,15 @@ export interface AudioDecoderConfigExt extends AudioDecoderConfig {
|
|||||||
codecType: AudioCodec;
|
codecType: AudioCodec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAudioCodecIdRequirePeekingKeyframe (
|
||||||
|
_track: TrackEntryType,
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function audioCodecIdToWebCodecs(
|
export function audioCodecIdToWebCodecs(
|
||||||
track: TrackEntryType
|
track: TrackEntryType,
|
||||||
|
_keyframe: Uint8Array | undefined
|
||||||
): AudioDecoderConfigExt {
|
): AudioDecoderConfigExt {
|
||||||
const codecId = track.CodecID;
|
const codecId = track.CodecID;
|
||||||
const codecPrivate = track.CodecPrivate;
|
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,
|
filter,
|
||||||
finalize,
|
finalize,
|
||||||
from,
|
from,
|
||||||
isEmpty,
|
isEmpty, last,
|
||||||
map,
|
map,
|
||||||
merge,
|
merge,
|
||||||
Observable,
|
Observable,
|
||||||
of,
|
of,
|
||||||
reduce,
|
|
||||||
scan,
|
scan,
|
||||||
share,
|
share,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
@ -28,51 +27,33 @@ import {
|
|||||||
createRangedStream,
|
createRangedStream,
|
||||||
type CreateRangedStreamOptions,
|
type CreateRangedStreamOptions,
|
||||||
} from '@konoplayer/core/data';
|
} 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 { isTagIdPos, waitTick } from './util';
|
||||||
import type { ClusterType } from './schema';
|
import type { ClusterType } from './schema';
|
||||||
|
import {SEEK_ID_KAX_CUES, SEEK_ID_KAX_TAGS, type CueSystem, type SegmentComponent, SegmentSystem} from "./systems";
|
||||||
|
|
||||||
export interface CreateRangedEbmlStreamOptions
|
export interface CreateRangedEbmlStreamOptions
|
||||||
extends CreateRangedStreamOptions {
|
extends CreateRangedStreamOptions {
|
||||||
tee?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRangedEbmlStream({
|
export function createRangedEbmlStream({
|
||||||
url,
|
url,
|
||||||
byteStart = 0,
|
byteStart = 0,
|
||||||
byteEnd,
|
byteEnd,
|
||||||
tee = false,
|
|
||||||
}: CreateRangedEbmlStreamOptions): Observable<{
|
}: CreateRangedEbmlStreamOptions): Observable<{
|
||||||
ebml$: Observable<EbmlTagType>;
|
ebml$: Observable<EbmlTagType>;
|
||||||
totalSize?: number;
|
totalSize?: number;
|
||||||
response: Response;
|
response: Response;
|
||||||
body: ReadableStream<Uint8Array>;
|
body: ReadableStream<Uint8Array>;
|
||||||
controller: AbortController;
|
controller: AbortController;
|
||||||
teeBody: ReadableStream<Uint8Array> | undefined;
|
|
||||||
}> {
|
}> {
|
||||||
const stream$ = from(createRangedStream({ url, byteStart, byteEnd }));
|
const stream$ = from(createRangedStream({ url, byteStart, byteEnd }));
|
||||||
|
|
||||||
return stream$.pipe(
|
return stream$.pipe(
|
||||||
switchMap(({ controller, body, totalSize, response }) => {
|
switchMap(({ controller, body, totalSize, response }) => {
|
||||||
let requestCompleted = false;
|
let requestCompleted = false;
|
||||||
let teeStream: ReadableStream<Uint8Array> | undefined;
|
|
||||||
|
|
||||||
let stream: ReadableStream<Uint8Array>;
|
|
||||||
|
|
||||||
if (tee) {
|
|
||||||
[stream, teeStream] = body.tee();
|
|
||||||
} else {
|
|
||||||
stream = body;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originRequest$ = new Observable<EbmlTagType>((subscriber) => {
|
const originRequest$ = new Observable<EbmlTagType>((subscriber) => {
|
||||||
stream
|
body
|
||||||
.pipeThrough(
|
.pipeThrough(
|
||||||
new EbmlStreamDecoder({
|
new EbmlStreamDecoder({
|
||||||
streamStartOffset: byteStart,
|
streamStartOffset: byteStart,
|
||||||
@ -130,8 +111,7 @@ export function createRangedEbmlStream({
|
|||||||
ebml$,
|
ebml$,
|
||||||
totalSize,
|
totalSize,
|
||||||
response,
|
response,
|
||||||
body: stream,
|
body,
|
||||||
teeBody: teeStream,
|
|
||||||
controller,
|
controller,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -149,11 +129,10 @@ export function createEbmlController({
|
|||||||
...options,
|
...options,
|
||||||
url,
|
url,
|
||||||
byteStart: 0,
|
byteStart: 0,
|
||||||
tee: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const controller$ = metaRequest$.pipe(
|
const controller$ = metaRequest$.pipe(
|
||||||
map(({ totalSize, ebml$, response, controller, teeBody }) => {
|
map(({ totalSize, ebml$, response, controller }) => {
|
||||||
const head$ = ebml$.pipe(
|
const head$ = ebml$.pipe(
|
||||||
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
|
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
|
||||||
take(1),
|
take(1),
|
||||||
@ -174,36 +153,23 @@ export function createEbmlController({
|
|||||||
*/
|
*/
|
||||||
const segments$ = segmentStart$.pipe(
|
const segments$ = segmentStart$.pipe(
|
||||||
map((startTag) => {
|
map((startTag) => {
|
||||||
const segment = new SegmentSystem(startTag, teeBody!);
|
const segment = new SegmentSystem(startTag);
|
||||||
const clusterSystem = segment.cluster;
|
const clusterSystem = segment.cluster;
|
||||||
const seekSystem = segment.seek;
|
const seekSystem = segment.seek;
|
||||||
|
|
||||||
const meta$ = ebml$.pipe(
|
const metaScan$ = ebml$.pipe(
|
||||||
scan(
|
scan(
|
||||||
(acc, tag) => {
|
(acc, tag) => {
|
||||||
// avoid object recreation
|
acc.segment.scanMeta(tag);
|
||||||
acc.hasKeyframe =
|
|
||||||
acc.hasKeyframe ||
|
|
||||||
(tag.id === EbmlTagIdEnum.SimpleBlock && tag.keyframe) ||
|
|
||||||
(tag.id === EbmlTagIdEnum.BlockGroup &&
|
|
||||||
tag.children.every(
|
|
||||||
(c) => c.id !== EbmlTagIdEnum.ReferenceBlock
|
|
||||||
));
|
|
||||||
acc.tag = tag;
|
acc.tag = tag;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{ hasKeyframe: false, tag: undefined as unknown as EbmlTagType }
|
{
|
||||||
|
segment,
|
||||||
|
tag: undefined as unknown as EbmlTagType,
|
||||||
|
}
|
||||||
),
|
),
|
||||||
takeWhile(({ tag, hasKeyframe }) => {
|
takeWhile((acc) => acc.segment.canCompleteMeta(), true),
|
||||||
return (
|
|
||||||
!isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End)(tag) &&
|
|
||||||
!(
|
|
||||||
isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)(tag) &&
|
|
||||||
hasKeyframe
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, true),
|
|
||||||
map(({ tag }) => tag),
|
|
||||||
share({
|
share({
|
||||||
resetOnComplete: false,
|
resetOnComplete: false,
|
||||||
resetOnError: false,
|
resetOnError: false,
|
||||||
@ -211,10 +177,13 @@ export function createEbmlController({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const withMeta$ = meta$.pipe(
|
const meta$ = metaScan$.pipe(
|
||||||
reduce((segment, meta) => segment.scanMeta(meta), segment),
|
map(({ tag }) => tag)
|
||||||
switchMap(() => segment.completeMeta()),
|
);
|
||||||
take(1),
|
|
||||||
|
const withMeta$ = metaScan$.pipe(
|
||||||
|
last(),
|
||||||
|
switchMap(({ segment }) => segment.completeMeta()),
|
||||||
shareReplay(1)
|
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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user