fix: fix audio issues

This commit is contained in:
master 2025-03-26 06:55:37 +08:00
parent 39e17eb6a5
commit 8cc1a2bab1
25 changed files with 371 additions and 306 deletions

View File

@ -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)

View File

@ -21,6 +21,6 @@
"node_modules", "node_modules",
"dist", "dist",
"test", "test",
"**/*spec.ts" "**/*spec"
] ]
} }

View File

@ -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>

View File

@ -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>
`;
} }
} }

View File

@ -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

View File

@ -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}]]}

View File

@ -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: {

View File

@ -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",

View File

@ -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$
}; })));
} }

View File

@ -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$
}; })));
} }

View File

@ -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',

View File

@ -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),

View File

@ -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$
}); });
}) })
); );

View File

@ -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$
}; };
} }

View File

@ -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;

View File

@ -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,

View File

@ -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";

View File

@ -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,

View File

@ -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]);

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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);
} }
} }

View File

@ -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),
], ],

View File

@ -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",