fix: fix audio issues
This commit is contained in:
parent
39e17eb6a5
commit
8cc1a2bab1
28
README.md
28
README.md
@ -1 +1,29 @@
|
|||||||
# konoplayer
|
# konoplayer
|
||||||
|
|
||||||
|
**A project initially launched solely to watch animations in the widely used but poorly supported MKV format in browsers, just for fun.**
|
||||||
|
|
||||||
|
## State of Prototype
|
||||||
|
- [x] Matroska support
|
||||||
|
- [x] Parse EBML and demux (Done / Typescript)
|
||||||
|
- [x] Data validating fit matroska v4 doc (Done / Typescript)
|
||||||
|
- [x] WebCodecs decode + Canvas rendering (Prototyping / Typescript)
|
||||||
|
- [x] Parsing track CodecId/Private and generate Codec String (Partial / Typescript)
|
||||||
|
- Video:
|
||||||
|
- [x] VP9
|
||||||
|
- [x] VP8
|
||||||
|
- [x] AVC
|
||||||
|
- [x] HEVC
|
||||||
|
- [x] AV1
|
||||||
|
- Audio:
|
||||||
|
- [x] AAC
|
||||||
|
- [x] MP3
|
||||||
|
- [x] AC3
|
||||||
|
- [ ] OPUS (not tested, need more work)
|
||||||
|
- [ ] VORBIS (need fix)
|
||||||
|
- [ ] EAC-3 (need fix)
|
||||||
|
- [ ] PCM (need tested)
|
||||||
|
- [ ] ALAC (need tested)
|
||||||
|
- [ ] FLAC (need tested)
|
||||||
|
- [ ] Wrap video element with customElements (Prototyping / Lit-html + Typescript)
|
||||||
|
- [ ] Add WebCodecs polyfill with ffmpeg or libav (Todo / WASM)
|
||||||
|
- [ ] Danmuku integrated (Todo / Typescript)
|
@ -21,6 +21,6 @@
|
|||||||
"node_modules",
|
"node_modules",
|
||||||
"dist",
|
"dist",
|
||||||
"test",
|
"test",
|
||||||
"**/*spec.ts"
|
"**/*spec"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -3,7 +3,7 @@
|
|||||||
<head></head>
|
<head></head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<my-element />
|
<!-- <my-element />-->
|
||||||
<video-pipeline-demo src="/api/static/video/test.webm"></video-pipeline-demo>
|
<!-- <video-pipeline-demo src="/api/static/video/test.webm"></video-pipeline-demo>-->
|
||||||
<!-- <video-pipeline-demo src="/api/static/video/huge/[LoliHouse] Amagami-san Chi no Enmusubi - 23 [WebRip 1080p HEVC-10bit AAC SRTx2].mkv" width="800" height="450" /> -->
|
<video-pipeline-demo src="/api/static/video/huge/[LoliHouse] Amagami-san Chi no Enmusubi - 23 [WebRip 1080p HEVC-10bit AAC SRTx2].mkv" width="800" height="450" />
|
||||||
</body>
|
</body>
|
@ -13,7 +13,7 @@ import {
|
|||||||
fromEvent,
|
fromEvent,
|
||||||
share,
|
share,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
firstValueFrom,
|
firstValueFrom, tap, throwIfEmpty, ReplaySubject, finalize, of, interval,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { createMatroska } from '@konoplayer/matroska/model';
|
import { createMatroska } from '@konoplayer/matroska/model';
|
||||||
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
||||||
@ -45,14 +45,12 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
|
|
||||||
videoRef: Ref<HTMLVideoElement> = createRef();
|
videoRef: Ref<HTMLVideoElement> = createRef();
|
||||||
renderingContext = createRenderingContext();
|
renderingContext = createRenderingContext();
|
||||||
audioContext = new AudioContext();
|
audioContext = new AudioContext({});
|
||||||
canvasSource = new MediaSource();
|
|
||||||
|
|
||||||
seeked$ = new Subject<number>();
|
seeked$ = new ReplaySubject<number>(1);
|
||||||
|
|
||||||
videoFrameBuffer$ = new BehaviorSubject(new Queue<VideoFrame>());
|
videoFrameBuffer$ = new BehaviorSubject(new Queue<VideoFrame>());
|
||||||
audioFrameBuffer$ = new BehaviorSubject(new Queue<AudioData>());
|
audioFrameBuffer$ = new BehaviorSubject(new Queue<AudioData>());
|
||||||
private startTime = 0;
|
|
||||||
|
|
||||||
paused$ = new BehaviorSubject<boolean>(false);
|
paused$ = new BehaviorSubject<boolean>(false);
|
||||||
ended$ = new BehaviorSubject<boolean>(false);
|
ended$ = new BehaviorSubject<boolean>(false);
|
||||||
@ -80,37 +78,37 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
videoTrackDecoder,
|
videoTrackDecoder,
|
||||||
audioTrackDecoder,
|
audioTrackDecoder,
|
||||||
},
|
},
|
||||||
|
totalSize
|
||||||
} = await firstValueFrom(
|
} = await firstValueFrom(
|
||||||
createMatroska({
|
createMatroska({
|
||||||
url: src,
|
url: src,
|
||||||
})
|
}).pipe(
|
||||||
);
|
throwIfEmpty(() => new Error("failed to extract matroska"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.debug(`[MATROSKA]: loaded metadata, total size ${totalSize} bytes`)
|
||||||
|
|
||||||
const currentCluster$ = this.seeked$.pipe(
|
const currentCluster$ = this.seeked$.pipe(
|
||||||
switchMap((seekTime) => seek(seekTime)),
|
switchMap((seekTime) => seek(seekTime)),
|
||||||
share()
|
share({ resetOnRefCountZero: false, resetOnError: false, resetOnComplete: false }),
|
||||||
);
|
);
|
||||||
|
|
||||||
defaultVideoTrack$
|
defaultVideoTrack$
|
||||||
.pipe(takeUntil(destroyRef$), take(1))
|
.pipe(take(1), takeUntil(destroyRef$), tap((track) => console.debug('[MATROSKA]: video track loaded,', track)))
|
||||||
.subscribe(this.videoTrack$);
|
.subscribe(this.videoTrack$.next.bind(this.videoTrack$));
|
||||||
|
|
||||||
defaultAudioTrack$
|
defaultAudioTrack$
|
||||||
.pipe(takeUntil(destroyRef$), take(1))
|
.pipe(take(1), takeUntil(destroyRef$), tap((track) => console.debug('[MATROSKA]: audio track loaded,', track)))
|
||||||
.subscribe(this.audioTrack$);
|
.subscribe(this.audioTrack$.next.bind(this.audioTrack$));
|
||||||
|
|
||||||
this.videoTrack$
|
this.videoTrack$
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntil(this.destroyRef$),
|
takeUntil(this.destroyRef$),
|
||||||
map((track) =>
|
switchMap((track) =>
|
||||||
track ? videoTrackDecoder(track, currentCluster$) : undefined
|
track?.configuration ? videoTrackDecoder(track, currentCluster$) : EMPTY
|
||||||
),
|
),
|
||||||
switchMap((decoder) => {
|
switchMap(({ frame$ }) => frame$)
|
||||||
if (!decoder) {
|
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
return decoder.frame$;
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
.subscribe((frame) => {
|
.subscribe((frame) => {
|
||||||
const buffer = this.videoFrameBuffer$.value;
|
const buffer = this.videoFrameBuffer$.value;
|
||||||
@ -121,15 +119,10 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
this.audioTrack$
|
this.audioTrack$
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntil(this.destroyRef$),
|
takeUntil(this.destroyRef$),
|
||||||
map((track) =>
|
switchMap((track) =>
|
||||||
track ? audioTrackDecoder(track, currentCluster$) : undefined
|
track?.configuration ? audioTrackDecoder(track, currentCluster$) : EMPTY
|
||||||
),
|
),
|
||||||
switchMap((decoder) => {
|
switchMap(({ frame$ }) => frame$)
|
||||||
if (!decoder) {
|
|
||||||
return EMPTY;
|
|
||||||
}
|
|
||||||
return decoder.frame$;
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
.subscribe((frame) => {
|
.subscribe((frame) => {
|
||||||
const buffer = this.audioFrameBuffer$.value;
|
const buffer = this.audioFrameBuffer$.value;
|
||||||
@ -137,39 +130,52 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
this.audioFrameBuffer$.next(buffer);
|
this.audioFrameBuffer$.next(buffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
combineLatest({
|
let playableStartTime = 0;
|
||||||
|
const playable = combineLatest({
|
||||||
paused: this.paused$,
|
paused: this.paused$,
|
||||||
ended: this.ended$,
|
ended: this.ended$,
|
||||||
buffered: this.audioFrameBuffer$.pipe(
|
audioBuffered: this.audioFrameBuffer$.pipe(
|
||||||
map((q) => q.size >= 1),
|
map((q) => q.size >= 1),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
),
|
),
|
||||||
})
|
videoBuffered: this.videoFrameBuffer$.pipe(
|
||||||
.pipe(
|
map((q) => q.size >= 1),
|
||||||
|
distinctUntilChanged()
|
||||||
|
),
|
||||||
|
}).pipe(
|
||||||
takeUntil(this.destroyRef$),
|
takeUntil(this.destroyRef$),
|
||||||
map(({ ended, paused, buffered }) => !paused && !ended && !!buffered),
|
map(({ ended, paused, videoBuffered, audioBuffered }) => !paused && !ended && !!(videoBuffered || audioBuffered)),
|
||||||
switchMap((enabled) => (enabled ? animationFrames() : EMPTY))
|
tap((enabled) => {
|
||||||
|
if (enabled) {
|
||||||
|
playableStartTime = performance.now()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
share()
|
||||||
|
)
|
||||||
|
|
||||||
|
let nextAudioStartTime = 0;
|
||||||
|
playable
|
||||||
|
.pipe(
|
||||||
|
tap(() => {
|
||||||
|
nextAudioStartTime = 0
|
||||||
|
}),
|
||||||
|
switchMap((enabled) => (enabled ? animationFrames() : EMPTY)),
|
||||||
)
|
)
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
const audioFrameBuffer = this.audioFrameBuffer$.getValue();
|
const audioFrameBuffer = this.audioFrameBuffer$.getValue();
|
||||||
|
const audioContext = this.audioContext;
|
||||||
const nowTime = performance.now();
|
const nowTime = performance.now();
|
||||||
const accTime = nowTime - this.startTime;
|
const accTime = nowTime - playableStartTime;
|
||||||
let audioChanged = false;
|
let audioChanged = false;
|
||||||
while (audioFrameBuffer.size > 0) {
|
while (audioFrameBuffer.size > 0) {
|
||||||
const firstAudio = audioFrameBuffer.peek();
|
const firstAudio = audioFrameBuffer.peek();
|
||||||
if (firstAudio && firstAudio.timestamp <= accTime * 1000) {
|
if (firstAudio && (firstAudio.timestamp / 1000) <= accTime) {
|
||||||
const audioFrame = audioFrameBuffer.dequeue()!;
|
const audioFrame = audioFrameBuffer.dequeue()!;
|
||||||
audioChanged = true;
|
audioChanged = true;
|
||||||
const audioContext = this.audioContext;
|
|
||||||
|
|
||||||
if (audioContext) {
|
if (audioContext) {
|
||||||
const numberOfChannels = audioFrame.numberOfChannels;
|
const numberOfChannels = audioFrame.numberOfChannels;
|
||||||
const sampleRate = audioFrame.sampleRate;
|
const sampleRate = audioFrame.sampleRate;
|
||||||
const numberOfFrames = audioFrame.numberOfFrames;
|
const numberOfFrames = audioFrame.numberOfFrames;
|
||||||
const data = new Float32Array(numberOfFrames * numberOfChannels);
|
|
||||||
audioFrame.copyTo(data, {
|
|
||||||
planeIndex: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const audioBuffer = audioContext.createBuffer(
|
const audioBuffer = audioContext.createBuffer(
|
||||||
numberOfChannels,
|
numberOfChannels,
|
||||||
@ -177,14 +183,22 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
sampleRate
|
sampleRate
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// add fade-in-out
|
||||||
|
const fadeLength = Math.min(50, audioFrame.numberOfFrames);
|
||||||
for (let channel = 0; channel < numberOfChannels; channel++) {
|
for (let channel = 0; channel < numberOfChannels; channel++) {
|
||||||
const channelData = audioBuffer.getChannelData(channel);
|
const channelData = new Float32Array(numberOfFrames);
|
||||||
for (let i = 0; i < numberOfFrames; i++) {
|
audioFrame.copyTo(channelData, { planeIndex: channel, frameCount: numberOfFrames });
|
||||||
channelData[i] = data[i * numberOfChannels + channel];
|
for (let i = 0; i < fadeLength; i++) {
|
||||||
|
channelData[i] *= i / fadeLength; // fade-in
|
||||||
|
channelData[audioFrame.numberOfFrames - 1 - i] *= i / fadeLength; // fade-out
|
||||||
}
|
}
|
||||||
|
audioBuffer.copyToChannel(channelData, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioTime = audioFrame.timestamp / 1000000;
|
/**
|
||||||
|
* @TODO: ADD TIME SYNC
|
||||||
|
*/
|
||||||
|
const audioTime = audioFrame.timestamp / 1_000_000;
|
||||||
|
|
||||||
audioFrame.close();
|
audioFrame.close();
|
||||||
|
|
||||||
@ -192,11 +206,10 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
const audioSource = audioContext.createBufferSource();
|
const audioSource = audioContext.createBufferSource();
|
||||||
audioSource.buffer = audioBuffer;
|
audioSource.buffer = audioBuffer;
|
||||||
audioSource.connect(audioContext.destination);
|
audioSource.connect(audioContext.destination);
|
||||||
|
const currentTime = audioContext.currentTime;
|
||||||
audioSource.start(
|
nextAudioStartTime = Math.max(nextAudioStartTime, currentTime); // 确保不早于当前时间
|
||||||
audioContext.currentTime +
|
audioSource.start(nextAudioStartTime);
|
||||||
Math.max(0, audioTime - accTime / 1000)
|
nextAudioStartTime += audioBuffer.duration;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -208,35 +221,26 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
combineLatest({
|
playable
|
||||||
paused: this.paused$,
|
|
||||||
ended: this.ended$,
|
|
||||||
buffered: this.videoFrameBuffer$.pipe(
|
|
||||||
map((q) => q.size >= 1),
|
|
||||||
distinctUntilChanged()
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntil(this.destroyRef$),
|
switchMap((enabled) => (enabled ? animationFrames() : EMPTY)),
|
||||||
map(({ ended, paused, buffered }) => !paused && !ended && !!buffered),
|
|
||||||
switchMap((enabled) => (enabled ? animationFrames() : EMPTY))
|
|
||||||
)
|
)
|
||||||
.subscribe(async () => {
|
.subscribe(async () => {
|
||||||
|
const renderingContext = this.renderingContext;
|
||||||
const videoFrameBuffer = this.videoFrameBuffer$.getValue();
|
const videoFrameBuffer = this.videoFrameBuffer$.getValue();
|
||||||
let videoChanged = false;
|
let videoChanged = false;
|
||||||
const nowTime = performance.now();
|
const nowTime = performance.now();
|
||||||
const accTime = nowTime - this.startTime;
|
const accTime = nowTime - playableStartTime;
|
||||||
while (videoFrameBuffer.size > 0) {
|
while (videoFrameBuffer.size > 0) {
|
||||||
const firstVideo = videoFrameBuffer.peek();
|
const firstVideo = videoFrameBuffer.peek();
|
||||||
if (firstVideo && firstVideo.timestamp <= accTime * 1000) {
|
if (firstVideo && (firstVideo.timestamp / 1000) <= accTime) {
|
||||||
const videoFrame = videoFrameBuffer.dequeue()!;
|
const videoFrame = videoFrameBuffer.dequeue()!;
|
||||||
const renderingContext = this.renderingContext;
|
videoChanged = true;
|
||||||
if (renderingContext) {
|
if (renderingContext) {
|
||||||
const bitmap = await createImageBitmap(videoFrame);
|
const bitmap = await createImageBitmap(videoFrame);
|
||||||
renderBitmapAtRenderingContext(renderingContext, bitmap);
|
renderBitmapAtRenderingContext(renderingContext, bitmap);
|
||||||
videoFrame.close();
|
|
||||||
videoChanged = true;
|
|
||||||
}
|
}
|
||||||
|
videoFrame.close();
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -252,22 +256,18 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
this.audioContext.resume();
|
this.audioContext.resume();
|
||||||
this.audioFrameBuffer$.next(this.audioFrameBuffer$.getValue());
|
this.audioFrameBuffer$.next(this.audioFrameBuffer$.getValue());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.seeked$.next(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
async connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.preparePipeline();
|
await this.preparePipeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.destroyRef$.next();
|
this.destroyRef$.next(undefined);
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<video ref=${ref(this.videoRef)}></video>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
@ -303,8 +303,16 @@ export class VideoPipelineDemo extends LitElement {
|
|||||||
|
|
||||||
frameRate$
|
frameRate$
|
||||||
.pipe(takeUntil(destroyRef$), distinctUntilChanged())
|
.pipe(takeUntil(destroyRef$), distinctUntilChanged())
|
||||||
.subscribe((frameRate) =>
|
.subscribe((frameRate) => {
|
||||||
captureCanvasAsVideoSrcObject(video, canvas, frameRate)
|
canvas.width = this.width || 1;
|
||||||
);
|
canvas.height = this.height || 1;
|
||||||
|
captureCanvasAsVideoSrcObject(video, canvas, frameRate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<video ref=${ref(this.videoRef)} width=${this.width} height=${this.height} autoplay muted></video>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
^https://konoplayer.com/api/static/*** resSpeed://10240
|
#^https://konoplayer.com/api/static/*** resSpeed://10240+
|
||||||
^https://konoplayer.com/api*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5001/api$1
|
^https://konoplayer.com/api*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5001/api$1
|
||||||
^https://konoplayer.com/*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5000/$1 excludeFilter://^https://konoplayer.com/api
|
^https://konoplayer.com/*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5000/$1 excludeFilter://^https://konoplayer.com/api weinre://test
|
||||||
^wss://konoplayer.com/*** reqHeaders://{x-forwarded.json} ws://127.0.0.1:5000/$1 excludeFilter://^wss://konoplayer.com/api
|
^wss://konoplayer.com/*** reqHeaders://{x-forwarded.json} ws://127.0.0.1:5000/$1 excludeFilter://^wss://konoplayer.com/api
|
@ -1 +1 @@
|
|||||||
{"version":"3.0.9","results":[[":src/matroska/codecs/av1.spec.ts",{"duration":52.71331099999952,"failed":false}]]}
|
{"version":"3.0.9","results":[[":src/matroska/codecs/av1.spec",{"duration":52.71331099999952,"failed":false}]]}
|
@ -5,9 +5,9 @@ import { defineConfig } from 'vitest/config';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
cacheDir: '.vitest',
|
cacheDir: '.vitest',
|
||||||
test: {
|
test: {
|
||||||
setupFiles: ['src/init-test.ts'],
|
setupFiles: ['src/init-test'],
|
||||||
environment: 'happy-dom',
|
environment: 'happy-dom',
|
||||||
include: ['src/**/*.spec.ts'],
|
include: ['src/**/*.spec'],
|
||||||
globals: true,
|
globals: true,
|
||||||
restoreMocks: true,
|
restoreMocks: true,
|
||||||
coverage: {
|
coverage: {
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "A strange player, like the dumtruck, taking you to Isekai.",
|
"description": "A strange player, like the dumtruck, taking you to Isekai.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"codegen-mkv": "tsx --tsconfig=./tsconfig.scripts.json ./scripts/codegen-mkv.ts",
|
"codegen-mkv": "tsx --tsconfig=./tsconfig.scripts.json ./scripts/codegen-mkv",
|
||||||
"download-samples": "tsx --tsconfig=./tsconfig.scripts.json ./scripts/download-samples.ts"
|
"download-samples": "tsx --tsconfig=./tsconfig.scripts.json ./scripts/download-samples"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "lonelyhentxi",
|
"author": "lonelyhentxi",
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import { Observable } from 'rxjs';
|
import {map, Observable, Subject} from 'rxjs';
|
||||||
|
|
||||||
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
|
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
|
||||||
export function createAudioDecodeStream(configuration: AudioDecoderConfig): {
|
export function createAudioDecodeStream(configuration: AudioDecoderConfig): Observable<{
|
||||||
decoder: AudioDecoder;
|
decoder: AudioDecoder;
|
||||||
frame$: Observable<AudioData>;
|
frame$: Observable<AudioData>;
|
||||||
} {
|
}> {
|
||||||
let decoder!: VideoDecoder;
|
const frame$ = new Subject<AudioData>()
|
||||||
const frame$ = new Observable<AudioData>((subscriber) => {
|
const decoder$ = new Observable<AudioDecoder>((subscriber) => {
|
||||||
let isFinalized = false;
|
let isFinalized = false;
|
||||||
decoder = new AudioDecoder({
|
const decoder = new AudioDecoder({
|
||||||
output: (frame) => subscriber.next(frame),
|
output: (frame) => frame$.next(frame),
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
if (!isFinalized) {
|
if (!isFinalized) {
|
||||||
isFinalized = true;
|
isFinalized = true;
|
||||||
|
frame$.error(e);
|
||||||
subscriber.error(e);
|
subscriber.error(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -20,16 +22,19 @@ export function createAudioDecodeStream(configuration: AudioDecoderConfig): {
|
|||||||
|
|
||||||
decoder.configure(configuration);
|
decoder.configure(configuration);
|
||||||
|
|
||||||
|
subscriber.next(decoder);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!isFinalized) {
|
if (!isFinalized) {
|
||||||
isFinalized = true;
|
isFinalized = true;
|
||||||
|
frame$.complete();
|
||||||
decoder.close();
|
decoder.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
})
|
||||||
|
|
||||||
return {
|
return decoder$.pipe(map((decoder) => ({
|
||||||
decoder,
|
decoder,
|
||||||
frame$,
|
frame$
|
||||||
};
|
})));
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Observable } from 'rxjs';
|
import {map, Observable, Subject} from 'rxjs';
|
||||||
|
|
||||||
export type RenderingContext =
|
export type RenderingContext =
|
||||||
| ImageBitmapRenderingContext
|
| ImageBitmapRenderingContext
|
||||||
@ -42,18 +42,19 @@ export function captureCanvasAsVideoSrcObject(
|
|||||||
video.srcObject = canvas.captureStream(frameRate);
|
video.srcObject = canvas.captureStream(frameRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createVideoDecodeStream(configuration: VideoDecoderConfig): {
|
export function createVideoDecodeStream(configuration: VideoDecoderConfig): Observable<{
|
||||||
decoder: VideoDecoder;
|
decoder: VideoDecoder;
|
||||||
frame$: Observable<VideoFrame>;
|
frame$: Observable<VideoFrame>;
|
||||||
} {
|
}> {
|
||||||
let decoder!: VideoDecoder;
|
const frame$ = new Subject<VideoFrame>()
|
||||||
const frame$ = new Observable<VideoFrame>((subscriber) => {
|
const decoder$ = new Observable<VideoDecoder>((subscriber) => {
|
||||||
let isFinalized = false;
|
let isFinalized = false;
|
||||||
decoder = new VideoDecoder({
|
const decoder = new VideoDecoder({
|
||||||
output: (frame) => subscriber.next(frame),
|
output: (frame) => frame$.next(frame),
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
if (!isFinalized) {
|
if (!isFinalized) {
|
||||||
isFinalized = true;
|
isFinalized = true;
|
||||||
|
frame$.error(e);
|
||||||
subscriber.error(e);
|
subscriber.error(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -61,16 +62,19 @@ export function createVideoDecodeStream(configuration: VideoDecoderConfig): {
|
|||||||
|
|
||||||
decoder.configure(configuration);
|
decoder.configure(configuration);
|
||||||
|
|
||||||
|
subscriber.next(decoder);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!isFinalized) {
|
if (!isFinalized) {
|
||||||
isFinalized = true;
|
isFinalized = true;
|
||||||
|
frame$.complete();
|
||||||
decoder.close();
|
decoder.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
})
|
||||||
|
|
||||||
return {
|
return decoder$.pipe(map((decoder) => ({
|
||||||
decoder,
|
decoder,
|
||||||
frame$,
|
frame$
|
||||||
};
|
})));
|
||||||
}
|
}
|
||||||
|
@ -16,16 +16,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
genCodecStringByAV1DecoderConfigurationRecord,
|
genCodecStringByAV1DecoderConfigurationRecord,
|
||||||
parseAV1DecoderConfigurationRecord,
|
parseAV1DecoderConfigurationRecord,
|
||||||
} from './av1.ts';
|
} from './av1';
|
||||||
import {
|
import {
|
||||||
genCodecStringByHEVCDecoderConfigurationRecord,
|
genCodecStringByHEVCDecoderConfigurationRecord,
|
||||||
parseHEVCDecoderConfigurationRecord,
|
parseHEVCDecoderConfigurationRecord,
|
||||||
} from './hevc.ts';
|
} from './hevc';
|
||||||
import {
|
import {
|
||||||
genCodecStringByVP9DecoderConfigurationRecord,
|
genCodecStringByVP9DecoderConfigurationRecord,
|
||||||
parseVP9DecoderConfigurationRecord,
|
parseVP9DecoderConfigurationRecord,
|
||||||
VP9_CODEC_TYPE,
|
VP9_CODEC_TYPE,
|
||||||
} from './vp9.ts';
|
} from './vp9';
|
||||||
|
|
||||||
export const VideoCodecId = {
|
export const VideoCodecId = {
|
||||||
VCM: 'V_MS/VFW/FOURCC',
|
VCM: 'V_MS/VFW/FOURCC',
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { CreateRangedStreamOptions } from '@konoplayer/core/data';
|
|
||||||
import { type EbmlEBMLTagType, EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
|
import { type EbmlEBMLTagType, EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
|
||||||
import {
|
import {
|
||||||
switchMap,
|
switchMap,
|
||||||
@ -7,14 +6,14 @@ import {
|
|||||||
shareReplay,
|
shareReplay,
|
||||||
map,
|
map,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
of,
|
of, type Observable, delayWhen, pipe, finalize, tap, throwIfEmpty,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { isTagIdPos } from '../util';
|
import { isTagIdPos } from '../util';
|
||||||
import { createRangedEbmlStream } from './resource';
|
import {createRangedEbmlStream, type CreateRangedEbmlStreamOptions} from './resource';
|
||||||
import { type MatroskaSegmentModel, createMatroskaSegment } from './segment';
|
import { type MatroskaSegmentModel, createMatroskaSegment } from './segment';
|
||||||
|
|
||||||
export type CreateMatroskaOptions = Omit<
|
export type CreateMatroskaOptions = Omit<
|
||||||
CreateRangedStreamOptions,
|
CreateRangedEbmlStreamOptions,
|
||||||
'byteStart' | 'byteEnd'
|
'byteStart' | 'byteEnd'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ export interface MatroskaModel {
|
|||||||
segment: MatroskaSegmentModel;
|
segment: MatroskaSegmentModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMatroska(options: CreateMatroskaOptions) {
|
export function createMatroska(options: CreateMatroskaOptions): Observable<MatroskaModel> {
|
||||||
const metadataRequest$ = createRangedEbmlStream({
|
const metadataRequest$ = createRangedEbmlStream({
|
||||||
...options,
|
...options,
|
||||||
byteStart: 0,
|
byteStart: 0,
|
||||||
@ -33,32 +32,34 @@ export function createMatroska(options: CreateMatroskaOptions) {
|
|||||||
|
|
||||||
return metadataRequest$.pipe(
|
return metadataRequest$.pipe(
|
||||||
switchMap(({ totalSize, ebml$, response }) => {
|
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
|
* 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
|
* some mkv generated by strange tools will emit several
|
||||||
*/
|
*/
|
||||||
const segments$ = segmentStart$.pipe(
|
const segment$ = ebml$.pipe(
|
||||||
map((startTag) =>
|
filter(isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.Start)),
|
||||||
createMatroskaSegment({
|
map((startTag) => createMatroskaSegment({
|
||||||
startTag,
|
startTag,
|
||||||
matroskaOptions: options,
|
matroskaOptions: options,
|
||||||
ebml$,
|
ebml$,
|
||||||
})
|
})),
|
||||||
)
|
delayWhen(
|
||||||
|
({ loadedMetadata$ }) => loadedMetadata$
|
||||||
|
),
|
||||||
|
take(1),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
const head$ = ebml$.pipe(
|
||||||
|
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
|
||||||
|
take(1),
|
||||||
|
shareReplay(1),
|
||||||
|
throwIfEmpty(() => new Error("failed to find head tag"))
|
||||||
);
|
);
|
||||||
|
|
||||||
return combineLatest({
|
return combineLatest({
|
||||||
segment: segments$.pipe(take(1)),
|
segment: segment$,
|
||||||
head: head$,
|
head: head$,
|
||||||
totalSize: of(totalSize),
|
totalSize: of(totalSize),
|
||||||
initResponse: of(response),
|
initResponse: of(response),
|
||||||
|
@ -3,14 +3,18 @@ import {
|
|||||||
createRangedStream,
|
createRangedStream,
|
||||||
} from '@konoplayer/core/data';
|
} from '@konoplayer/core/data';
|
||||||
import { type EbmlTagType, EbmlStreamDecoder, EbmlTagIdEnum } from 'konoebml';
|
import { type EbmlTagType, EbmlStreamDecoder, EbmlTagIdEnum } from 'konoebml';
|
||||||
import { Observable, from, switchMap, share, defer, EMPTY, of } from 'rxjs';
|
import {Observable, from, switchMap, share, defer, EMPTY, of, tap} from 'rxjs';
|
||||||
import { waitTick } from '../util';
|
import { waitTick } from '../util';
|
||||||
|
|
||||||
|
export interface CreateRangedEbmlStreamOptions extends CreateRangedStreamOptions {
|
||||||
|
refCount?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export function createRangedEbmlStream({
|
export function createRangedEbmlStream({
|
||||||
url,
|
url,
|
||||||
byteStart = 0,
|
byteStart = 0,
|
||||||
byteEnd,
|
byteEnd
|
||||||
}: CreateRangedStreamOptions): Observable<{
|
}: CreateRangedEbmlStreamOptions): Observable<{
|
||||||
ebml$: Observable<EbmlTagType>;
|
ebml$: Observable<EbmlTagType>;
|
||||||
totalSize?: number;
|
totalSize?: number;
|
||||||
response: Response;
|
response: Response;
|
||||||
@ -23,7 +27,10 @@ export function createRangedEbmlStream({
|
|||||||
switchMap(({ controller, body, totalSize, response }) => {
|
switchMap(({ controller, body, totalSize, response }) => {
|
||||||
let requestCompleted = false;
|
let requestCompleted = false;
|
||||||
|
|
||||||
const originRequest$ = new Observable<EbmlTagType>((subscriber) => {
|
const ebml$ = new Observable<EbmlTagType>((subscriber) => {
|
||||||
|
if (requestCompleted) {
|
||||||
|
subscriber.complete();
|
||||||
|
}
|
||||||
body
|
body
|
||||||
.pipeThrough(
|
.pipeThrough(
|
||||||
new EbmlStreamDecoder({
|
new EbmlStreamDecoder({
|
||||||
@ -57,8 +64,10 @@ export function createRangedEbmlStream({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (!requestCompleted) {
|
||||||
requestCompleted = true;
|
requestCompleted = true;
|
||||||
controller.abort();
|
controller.abort();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}).pipe(
|
}).pipe(
|
||||||
share({
|
share({
|
||||||
@ -68,22 +77,12 @@ export function createRangedEbmlStream({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ebml$ = defer(() =>
|
|
||||||
requestCompleted ? EMPTY : originRequest$
|
|
||||||
).pipe(
|
|
||||||
share({
|
|
||||||
resetOnError: false,
|
|
||||||
resetOnComplete: true,
|
|
||||||
resetOnRefCountZero: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return of({
|
return of({
|
||||||
ebml$,
|
|
||||||
totalSize,
|
totalSize,
|
||||||
response,
|
response,
|
||||||
body,
|
body,
|
||||||
controller,
|
controller,
|
||||||
|
ebml$
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -12,7 +12,6 @@ import {
|
|||||||
takeWhile,
|
takeWhile,
|
||||||
share,
|
share,
|
||||||
map,
|
map,
|
||||||
last,
|
|
||||||
switchMap,
|
switchMap,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
EMPTY,
|
EMPTY,
|
||||||
@ -23,6 +22,8 @@ import {
|
|||||||
merge,
|
merge,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
finalize,
|
finalize,
|
||||||
|
delayWhen,
|
||||||
|
from,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import type { CreateMatroskaOptions } from '.';
|
import type { CreateMatroskaOptions } from '.';
|
||||||
import { type ClusterType, TrackTypeRestrictionEnum } from '../schema';
|
import { type ClusterType, TrackTypeRestrictionEnum } from '../schema';
|
||||||
@ -51,7 +52,6 @@ export interface CreateMatroskaSegmentOptions {
|
|||||||
export interface MatroskaSegmentModel {
|
export interface MatroskaSegmentModel {
|
||||||
startTag: EbmlSegmentTagType;
|
startTag: EbmlSegmentTagType;
|
||||||
segment: SegmentSystem;
|
segment: SegmentSystem;
|
||||||
metadataTags$: Observable<EbmlTagType>;
|
|
||||||
loadedMetadata$: Observable<SegmentSystem>;
|
loadedMetadata$: Observable<SegmentSystem>;
|
||||||
loadedTags$: Observable<SegmentSystem>;
|
loadedTags$: Observable<SegmentSystem>;
|
||||||
loadedCues$: Observable<SegmentSystem>;
|
loadedCues$: Observable<SegmentSystem>;
|
||||||
@ -59,19 +59,19 @@ export interface MatroskaSegmentModel {
|
|||||||
videoTrackDecoder: (
|
videoTrackDecoder: (
|
||||||
track: VideoTrackContext,
|
track: VideoTrackContext,
|
||||||
cluster$: Observable<ClusterType>
|
cluster$: Observable<ClusterType>
|
||||||
) => {
|
) => Observable<{
|
||||||
track: VideoTrackContext;
|
track: VideoTrackContext;
|
||||||
decoder: VideoDecoder;
|
decoder: VideoDecoder;
|
||||||
frame$: Observable<VideoFrame>;
|
frame$: Observable<VideoFrame>;
|
||||||
};
|
}>;
|
||||||
audioTrackDecoder: (
|
audioTrackDecoder: (
|
||||||
track: AudioTrackContext,
|
track: AudioTrackContext,
|
||||||
cluster$: Observable<ClusterType>
|
cluster$: Observable<ClusterType>
|
||||||
) => {
|
) => Observable<{
|
||||||
track: AudioTrackContext;
|
track: AudioTrackContext;
|
||||||
decoder: AudioDecoder;
|
decoder: AudioDecoder;
|
||||||
frame$: Observable<AudioData>;
|
frame$: Observable<AudioData>;
|
||||||
};
|
}>;
|
||||||
defaultVideoTrack$: Observable<VideoTrackContext | undefined>;
|
defaultVideoTrack$: Observable<VideoTrackContext | undefined>;
|
||||||
defaultAudioTrack$: Observable<AudioTrackContext | undefined>;
|
defaultAudioTrack$: Observable<AudioTrackContext | undefined>;
|
||||||
}
|
}
|
||||||
@ -88,16 +88,20 @@ export function createMatroskaSegment({
|
|||||||
const metaScan$ = ebml$.pipe(
|
const metaScan$ = ebml$.pipe(
|
||||||
scan(
|
scan(
|
||||||
(acc, tag) => {
|
(acc, tag) => {
|
||||||
acc.segment.scanMeta(tag);
|
const segment = acc.segment;
|
||||||
|
segment.scanMeta(tag);
|
||||||
acc.tag = tag;
|
acc.tag = tag;
|
||||||
|
acc.canComplete = segment.canCompleteMeta();
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
segment,
|
segment,
|
||||||
tag: undefined as unknown as EbmlTagType,
|
tag: undefined as unknown as EbmlTagType,
|
||||||
|
canComplete: false,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
takeWhile((acc) => acc.segment.canCompleteMeta(), true),
|
takeWhile(({ canComplete }) => !canComplete, true),
|
||||||
|
delayWhen(({ segment }) => from(segment.completeMeta())),
|
||||||
share({
|
share({
|
||||||
resetOnComplete: false,
|
resetOnComplete: false,
|
||||||
resetOnError: false,
|
resetOnError: false,
|
||||||
@ -105,12 +109,11 @@ export function createMatroskaSegment({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const metadataTags$ = metaScan$.pipe(map(({ tag }) => tag));
|
|
||||||
|
|
||||||
const loadedMetadata$ = metaScan$.pipe(
|
const loadedMetadata$ = metaScan$.pipe(
|
||||||
last(),
|
filter(({ canComplete }) => canComplete),
|
||||||
switchMap(({ segment }) => segment.completeMeta()),
|
map(({ segment }) => segment),
|
||||||
shareReplay(1)
|
take(1),
|
||||||
|
shareReplay(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadedRemoteCues$ = loadedMetadata$.pipe(
|
const loadedRemoteCues$ = loadedMetadata$.pipe(
|
||||||
@ -297,18 +300,20 @@ export function createMatroskaSegment({
|
|||||||
track: VideoTrackContext,
|
track: VideoTrackContext,
|
||||||
cluster$: Observable<ClusterType>
|
cluster$: Observable<ClusterType>
|
||||||
) => {
|
) => {
|
||||||
const { decoder, frame$ } = createVideoDecodeStream(track.configuration);
|
return createVideoDecodeStream(track.configuration).pipe(
|
||||||
|
map(({ decoder, frame$ }) => {
|
||||||
const clusterSystem = segment.cluster;
|
const clusterSystem = segment.cluster;
|
||||||
|
const infoSystem = segment.info;
|
||||||
|
const timestampScale = Number(infoSystem.info.TimestampScale) / 1000;
|
||||||
|
|
||||||
const decodeSubscription = cluster$.subscribe((cluster) => {
|
const decodeSubscription = cluster$.subscribe((cluster) => {
|
||||||
for (const block of clusterSystem.enumerateBlocks(
|
for (const block of clusterSystem.enumerateBlocks(
|
||||||
cluster,
|
cluster,
|
||||||
track.trackEntry
|
track.trackEntry
|
||||||
)) {
|
)) {
|
||||||
const blockTime = Number(cluster.Timestamp) + block.relTime;
|
const blockTime = (Number(cluster.Timestamp) + block.relTime) * timestampScale;
|
||||||
const blockDuration =
|
const blockDuration =
|
||||||
frames.length > 1 ? track.predictBlockDuration(blockTime) : 0;
|
frames.length > 1 ? track.predictBlockDuration(blockTime) * timestampScale : 0;
|
||||||
const perFrameDuration =
|
const perFrameDuration =
|
||||||
frames.length > 1 && blockDuration
|
frames.length > 1 && blockDuration
|
||||||
? blockDuration / block.frames.length
|
? blockDuration / block.frames.length
|
||||||
@ -335,24 +340,27 @@ export function createMatroskaSegment({
|
|||||||
decodeSubscription.unsubscribe();
|
decodeSubscription.unsubscribe();
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.pipe(share()),
|
}
|
||||||
};
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const audioTrackDecoder = (
|
const audioTrackDecoder = (
|
||||||
track: AudioTrackContext,
|
track: AudioTrackContext,
|
||||||
cluster$: Observable<ClusterType>
|
cluster$: Observable<ClusterType>
|
||||||
) => {
|
) => {
|
||||||
const { decoder, frame$ } = createAudioDecodeStream(track.configuration);
|
return createAudioDecodeStream(track.configuration).pipe(
|
||||||
|
map(({ decoder, frame$ }) => {
|
||||||
const clusterSystem = segment.cluster;
|
const clusterSystem = segment.cluster;
|
||||||
|
const infoSystem = segment.info;
|
||||||
|
const timestampScale = Number(infoSystem.info.TimestampScale) / 1000;
|
||||||
|
|
||||||
const decodeSubscription = cluster$.subscribe((cluster) => {
|
const decodeSubscription = cluster$.subscribe((cluster) => {
|
||||||
for (const block of clusterSystem.enumerateBlocks(
|
for (const block of clusterSystem.enumerateBlocks(
|
||||||
cluster,
|
cluster,
|
||||||
track.trackEntry
|
track.trackEntry
|
||||||
)) {
|
)) {
|
||||||
const blockTime = Number(cluster.Timestamp) + block.relTime;
|
const blockTime = (Number(cluster.Timestamp) + block.relTime) * timestampScale;
|
||||||
const blockDuration =
|
const blockDuration =
|
||||||
frames.length > 1 ? track.predictBlockDuration(blockTime) : 0;
|
frames.length > 1 ? track.predictBlockDuration(blockTime) : 0;
|
||||||
const perFrameDuration =
|
const perFrameDuration =
|
||||||
@ -379,6 +387,7 @@ export function createMatroskaSegment({
|
|||||||
decoder,
|
decoder,
|
||||||
frame$: frame$.pipe(finalize(() => decodeSubscription.unsubscribe())),
|
frame$: frame$.pipe(finalize(() => decodeSubscription.unsubscribe())),
|
||||||
};
|
};
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultVideoTrack$ = loadedMetadata$.pipe(
|
const defaultVideoTrack$ = loadedMetadata$.pipe(
|
||||||
@ -406,7 +415,6 @@ export function createMatroskaSegment({
|
|||||||
return {
|
return {
|
||||||
startTag,
|
startTag,
|
||||||
segment,
|
segment,
|
||||||
metadataTags$,
|
|
||||||
loadedMetadata$,
|
loadedMetadata$,
|
||||||
loadedTags$,
|
loadedTags$,
|
||||||
loadedCues$,
|
loadedCues$,
|
||||||
@ -414,6 +422,6 @@ export function createMatroskaSegment({
|
|||||||
videoTrackDecoder,
|
videoTrackDecoder,
|
||||||
audioTrackDecoder,
|
audioTrackDecoder,
|
||||||
defaultVideoTrack$,
|
defaultVideoTrack$,
|
||||||
defaultAudioTrack$,
|
defaultAudioTrack$
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,8 @@ import {
|
|||||||
type BlockGroupType,
|
type BlockGroupType,
|
||||||
type TrackEntryType,
|
type TrackEntryType,
|
||||||
} from '../schema';
|
} from '../schema';
|
||||||
import { type SegmentComponent, SegmentComponentSystemTrait } from './segment';
|
import { type SegmentComponent } from './segment';
|
||||||
|
import {SegmentComponentSystemTrait} from "./segment-component";
|
||||||
|
|
||||||
export abstract class BlockViewTrait {
|
export abstract class BlockViewTrait {
|
||||||
abstract get keyframe(): boolean;
|
abstract get keyframe(): boolean;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import {type EbmlCuePointTagType, type EbmlCuesTagType, EbmlTagIdEnum} from "konoebml";
|
import {type EbmlCuePointTagType, type EbmlCuesTagType, EbmlTagIdEnum} from "konoebml";
|
||||||
import {CuePointSchema, type CuePointType, type CueTrackPositionsType} from "../schema.ts";
|
import {CuePointSchema, type CuePointType, type CueTrackPositionsType} from "../schema";
|
||||||
import {maxBy} from "lodash-es";
|
import {maxBy} from "lodash-es";
|
||||||
import {type SegmentComponent, SegmentComponentSystemTrait} from "./segment.ts";
|
import type {SegmentComponent} from "./segment";
|
||||||
|
import {SegmentComponentSystemTrait} from "./segment-component";
|
||||||
|
|
||||||
export class CueSystem extends SegmentComponentSystemTrait<
|
export class CueSystem extends SegmentComponentSystemTrait<
|
||||||
EbmlCuePointTagType,
|
EbmlCuePointTagType,
|
||||||
|
@ -3,5 +3,6 @@ export { CueSystem } from './cue';
|
|||||||
export { TagSystem } from './tag';
|
export { TagSystem } from './tag';
|
||||||
export { ClusterSystem } from './cluster';
|
export { ClusterSystem } from './cluster';
|
||||||
export { InfoSystem } from './info';
|
export { InfoSystem } from './info';
|
||||||
export { type SegmentComponent, SegmentSystem, SegmentComponentSystemTrait, withSegment } from './segment';
|
export { type SegmentComponent, SegmentSystem, withSegment } from './segment';
|
||||||
export { SeekSystem, SEEK_ID_KAX_CUES, SEEK_ID_KAX_INFO, SEEK_ID_KAX_TAGS, SEEK_ID_KAX_TRACKS } from './seek';
|
export { SeekSystem, SEEK_ID_KAX_CUES, SEEK_ID_KAX_INFO, SEEK_ID_KAX_TAGS, SEEK_ID_KAX_TRACKS } from './seek';
|
||||||
|
export {SegmentComponentSystemTrait} from "./segment-component";
|
@ -1,6 +1,7 @@
|
|||||||
import type {EbmlInfoTagType} from "konoebml";
|
import type {EbmlInfoTagType} from "konoebml";
|
||||||
import {InfoSchema, type InfoType} from "../schema.ts";
|
import {InfoSchema, type InfoType} from "../schema";
|
||||||
import {type SegmentComponent, SegmentComponentSystemTrait} from "./segment.ts";
|
import type {SegmentComponent} from "./segment";
|
||||||
|
import {SegmentComponentSystemTrait} from "./segment-component";
|
||||||
|
|
||||||
export class InfoSystem extends SegmentComponentSystemTrait<
|
export class InfoSystem extends SegmentComponentSystemTrait<
|
||||||
EbmlInfoTagType,
|
EbmlInfoTagType,
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import type {EbmlSeekHeadTagType, EbmlTagType} from "konoebml";
|
import type {EbmlSeekHeadTagType, EbmlTagType} from "konoebml";
|
||||||
import {SeekHeadSchema, type SeekHeadType} from "../schema.ts";
|
import {SeekHeadSchema, type SeekHeadType} from "../schema";
|
||||||
import {isEqual} from "lodash-es";
|
import {isEqual} from "lodash-es";
|
||||||
import {UnreachableOrLogicError} from "@konoplayer/core/errors.ts";
|
import {UnreachableOrLogicError} from "@konoplayer/core/errors";
|
||||||
|
import {SegmentComponentSystemTrait} from "./segment-component";
|
||||||
import {SegmentComponentSystemTrait} from "./segment.ts";
|
|
||||||
|
|
||||||
export const SEEK_ID_KAX_INFO = new Uint8Array([0x15, 0x49, 0xa9, 0x66]);
|
export const SEEK_ID_KAX_INFO = new Uint8Array([0x15, 0x49, 0xa9, 0x66]);
|
||||||
export const SEEK_ID_KAX_TRACKS = new Uint8Array([0x16, 0x54, 0xae, 0x6b]);
|
export const SEEK_ID_KAX_TRACKS = new Uint8Array([0x16, 0x54, 0xae, 0x6b]);
|
||||||
|
37
packages/matroska/src/systems/segment-component.ts
Normal file
37
packages/matroska/src/systems/segment-component.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type {EbmlMasterTagType} from "konoebml";
|
||||||
|
import {ArkErrors, type Type} from "arktype";
|
||||||
|
import {convertEbmlTagToComponent, type InferType} from "../util";
|
||||||
|
import type {SegmentComponent, SegmentSystem} from "./segment";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
type EbmlClusterTagType,
|
type EbmlClusterTagType,
|
||||||
type EbmlMasterTagType,
|
|
||||||
type EbmlSegmentTagType,
|
type EbmlSegmentTagType,
|
||||||
EbmlTagIdEnum,
|
EbmlTagIdEnum,
|
||||||
EbmlTagPosition,
|
EbmlTagPosition,
|
||||||
type EbmlTagType
|
type EbmlTagType
|
||||||
} from "konoebml";
|
} from "konoebml";
|
||||||
import {ArkErrors, type Type} from "arktype";
|
import {convertEbmlTagToComponent} from "../util";
|
||||||
import {convertEbmlTagToComponent, type InferType} from "../util.ts";
|
import {CueSystem} from "./cue";
|
||||||
import {CueSystem} from "./cue.ts";
|
import {ClusterSystem} from "./cluster";
|
||||||
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";
|
||||||
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";
|
||||||
import {InfoSystem} from "./info.ts";
|
import {TrackSystem} from "./track";
|
||||||
import {TrackSystem} from "./track.ts";
|
import {TagSystem} from "./tag";
|
||||||
import {TagSystem} from "./tag.ts";
|
import type {BlockGroupType} from "../schema";
|
||||||
import type {BlockGroupType} from "../schema.ts";
|
|
||||||
|
|
||||||
export class SegmentSystem {
|
export class SegmentSystem {
|
||||||
startTag: EbmlSegmentTagType;
|
startTag: EbmlSegmentTagType;
|
||||||
@ -70,7 +68,9 @@ export class SegmentSystem {
|
|||||||
this.seek.addSeekHeadTag(tag);
|
this.seek.addSeekHeadTag(tag);
|
||||||
}
|
}
|
||||||
this.metaTags.push(tag);
|
this.metaTags.push(tag);
|
||||||
|
if (tag.position !== EbmlTagPosition.Start) {
|
||||||
this.seek.memoOffset(tag);
|
this.seek.memoOffset(tag);
|
||||||
|
}
|
||||||
if (tag.id === EbmlTagIdEnum.Cluster && !this.firstCluster) {
|
if (tag.id === EbmlTagIdEnum.Cluster && !this.firstCluster) {
|
||||||
this.firstCluster = tag;
|
this.firstCluster = tag;
|
||||||
this.seekLocal();
|
this.seekLocal();
|
||||||
@ -97,7 +97,7 @@ export class SegmentSystem {
|
|||||||
if (lastTag.id === EbmlTagIdEnum.Segment && lastTag.position === EbmlTagPosition.End) {
|
if (lastTag.id === EbmlTagIdEnum.Segment && lastTag.position === EbmlTagPosition.End) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return !!(this.firstCluster && this.track.preparedToConfigureTracks());
|
return (!!this.firstCluster && this.track.preparedToConfigureTracks());
|
||||||
}
|
}
|
||||||
|
|
||||||
async completeMeta() {
|
async completeMeta() {
|
||||||
@ -122,35 +122,3 @@ export function withSegment<T extends object>(
|
|||||||
return component_;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,8 @@
|
|||||||
import {EbmlTagIdEnum, type EbmlTagsTagType, type EbmlTagTagType} from "konoebml";
|
import {EbmlTagIdEnum, type EbmlTagsTagType, type EbmlTagTagType} from "konoebml";
|
||||||
import {TagSchema, type TagType} from "../schema.ts";
|
import {TagSchema, type TagType} from "../schema";
|
||||||
|
|
||||||
import {type SegmentComponent, SegmentComponentSystemTrait} from "./segment.ts";
|
import type {SegmentComponent} from "./segment";
|
||||||
|
import {SegmentComponentSystemTrait} from "./segment-component";
|
||||||
|
|
||||||
export class TagSystem extends SegmentComponentSystemTrait<
|
export class TagSystem extends SegmentComponentSystemTrait<
|
||||||
EbmlTagTagType,
|
EbmlTagTagType,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ParseCodecErrors,
|
ParseCodecErrors,
|
||||||
UnsupportedCodecError,
|
UnsupportedCodecError,
|
||||||
} from '@konoplayer/core/errors.ts';
|
} from '@konoplayer/core/errors';
|
||||||
import {
|
import {
|
||||||
EbmlTagIdEnum,
|
EbmlTagIdEnum,
|
||||||
type EbmlTrackEntryTagType,
|
type EbmlTrackEntryTagType,
|
||||||
@ -19,7 +19,9 @@ import {
|
|||||||
type TrackEntryType,
|
type TrackEntryType,
|
||||||
TrackTypeRestrictionEnum,
|
TrackTypeRestrictionEnum,
|
||||||
} from '../schema';
|
} from '../schema';
|
||||||
import { type SegmentComponent, SegmentComponentSystemTrait } from './segment';
|
import type { SegmentComponent } from './segment';
|
||||||
|
import {SegmentComponentSystemTrait} from "./segment-component";
|
||||||
|
import {pick} from "lodash-es";
|
||||||
|
|
||||||
export interface GetTrackEntryOptions {
|
export interface GetTrackEntryOptions {
|
||||||
priority?: (v: SegmentComponent<TrackEntryType>) => number;
|
priority?: (v: SegmentComponent<TrackEntryType>) => number;
|
||||||
@ -29,13 +31,13 @@ export interface GetTrackEntryOptions {
|
|||||||
export abstract class TrackContext {
|
export abstract class TrackContext {
|
||||||
peekingKeyframe?: Uint8Array;
|
peekingKeyframe?: Uint8Array;
|
||||||
trackEntry: TrackEntryType;
|
trackEntry: TrackEntryType;
|
||||||
timecodeScale: number;
|
timestampScale: number;
|
||||||
lastBlockTimestamp = Number.NaN;
|
lastBlockTimestamp = Number.NaN;
|
||||||
averageBlockDuration = Number.NaN;
|
averageBlockDuration = Number.NaN;
|
||||||
|
|
||||||
constructor(trackEntry: TrackEntryType, timecodeScale: number) {
|
constructor(trackEntry: TrackEntryType, timestampScale: number) {
|
||||||
this.trackEntry = trackEntry;
|
this.trackEntry = trackEntry;
|
||||||
this.timecodeScale = timecodeScale;
|
this.timestampScale = Number(timestampScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
peekKeyframe(payload: Uint8Array) {
|
peekKeyframe(payload: Uint8Array) {
|
||||||
@ -87,7 +89,8 @@ export class VideoTrackContext extends TrackContext {
|
|||||||
this.trackEntry,
|
this.trackEntry,
|
||||||
this.peekingKeyframe
|
this.peekingKeyframe
|
||||||
);
|
);
|
||||||
if (await VideoDecoder.isConfigSupported(configuration)) {
|
const checkResult = await VideoDecoder?.isConfigSupported?.(configuration);
|
||||||
|
if (!checkResult?.supported) {
|
||||||
throw new UnsupportedCodecError(configuration.codec, 'video decoder');
|
throw new UnsupportedCodecError(configuration.codec, 'video decoder');
|
||||||
}
|
}
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
@ -106,7 +109,8 @@ export class AudioTrackContext extends TrackContext {
|
|||||||
this.trackEntry,
|
this.trackEntry,
|
||||||
this.peekingKeyframe
|
this.peekingKeyframe
|
||||||
);
|
);
|
||||||
if (await AudioDecoder.isConfigSupported(configuration)) {
|
const checkResult = await AudioDecoder?.isConfigSupported?.(configuration);
|
||||||
|
if (!checkResult?.supported) {
|
||||||
throw new UnsupportedCodecError(configuration.codec, 'audio decoder');
|
throw new UnsupportedCodecError(configuration.codec, 'audio decoder');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,8 +125,7 @@ export class AudioTrackContext extends TrackContext {
|
|||||||
return (
|
return (
|
||||||
Number(
|
Number(
|
||||||
this.configuration.samplesPerFrame / this.configuration.sampleRate
|
this.configuration.samplesPerFrame / this.configuration.sampleRate
|
||||||
) *
|
) * this.timestampScale
|
||||||
(1_000_000_000 / Number(this.timecodeScale))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const delta = blockTimestamp - this.lastBlockTimestamp;
|
const delta = blockTimestamp - this.lastBlockTimestamp;
|
||||||
@ -203,7 +206,7 @@ export class TrackSystem extends SegmentComponentSystemTrait<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (parseErrors.cause.length > 0) {
|
if (parseErrors.cause.length > 0) {
|
||||||
console.error(parseErrors);
|
console.error(parseErrors, parseErrors.cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,7 +437,7 @@ function main() {
|
|||||||
const elementSchemas = extractElementAll();
|
const elementSchemas = extractElementAll();
|
||||||
|
|
||||||
const files = {
|
const files = {
|
||||||
'schema.ts': [
|
'schema': [
|
||||||
generateMkvSchemaImports(elementSchemas),
|
generateMkvSchemaImports(elementSchemas),
|
||||||
generateMkvSchemaHierarchy(elementSchemas),
|
generateMkvSchemaHierarchy(elementSchemas),
|
||||||
],
|
],
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": false,
|
||||||
"emitDeclarationOnly": true,
|
"emitDeclarationOnly": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"target": "ES2021",
|
"target": "ES2021",
|
||||||
|
Loading…
Reference in New Issue
Block a user