mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-01 21:03:46 +02:00
193 lines
4.5 KiB
TypeScript
193 lines
4.5 KiB
TypeScript
import { InputVideoTrack } from './input-track';
|
|
import { assert, promiseWithResolvers } from './misc';
|
|
|
|
export class EncodedVideoChunkDrain {
|
|
constructor(public videoTrack: InputVideoTrack) {}
|
|
|
|
getFirstChunk() {
|
|
return this.videoTrack._backing.getFirstChunk();
|
|
}
|
|
|
|
getChunk(timestamp: number) {
|
|
return this.videoTrack._backing.getChunk(timestamp);
|
|
}
|
|
|
|
getNextChunk(chunk: EncodedVideoChunk) {
|
|
return this.videoTrack._backing.getNextChunk(chunk);
|
|
}
|
|
|
|
getKeyChunk(timestamp: number) {
|
|
return this.videoTrack._backing.getKeyChunk(timestamp);
|
|
}
|
|
|
|
getNextKeyChunk(chunk: EncodedVideoChunk) {
|
|
return this.videoTrack._backing.getNextKeyChunk(chunk);
|
|
}
|
|
|
|
async* chunks(startTimestamp = 0) {
|
|
let chunk = await this.getChunk(startTimestamp); // Not necessarily correct if there is no chunk at timestamp 0
|
|
while (chunk) {
|
|
yield chunk;
|
|
chunk = await this.getNextChunk(chunk);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class VideoFrameDrain {
|
|
decoderConfig: VideoDecoderConfig | null = null;
|
|
|
|
constructor(public videoTrack: InputVideoTrack) {}
|
|
|
|
async createDecoder(onFrame: (frame: VideoFrame) => unknown) {
|
|
if (!this.decoderConfig) {
|
|
this.decoderConfig = await this.videoTrack.getDecoderConfig();
|
|
}
|
|
|
|
const decoder = new VideoDecoder({
|
|
output: onFrame,
|
|
error: error => console.error(error),
|
|
});
|
|
decoder.configure(this.decoderConfig);
|
|
|
|
return decoder;
|
|
}
|
|
|
|
async getKeyFrame(timestamp: number) {
|
|
let result: VideoFrame | null = null;
|
|
|
|
const decoder = await this.createDecoder(frame => result = frame);
|
|
const chunk = await this.videoTrack._backing.getKeyChunk(timestamp);
|
|
if (!chunk) {
|
|
return null;
|
|
}
|
|
|
|
decoder.decode(chunk);
|
|
|
|
await decoder.flush();
|
|
decoder.close();
|
|
|
|
return result;
|
|
}
|
|
|
|
async getFrame(timestamp: number) {
|
|
let result: VideoFrame | null = null;
|
|
|
|
const decoder = await this.createDecoder((frame) => {
|
|
if (frame.timestamp / 1e6 <= timestamp) {
|
|
result?.close();
|
|
result = frame;
|
|
} else {
|
|
frame.close();
|
|
}
|
|
});
|
|
const keyChunk = await this.videoTrack._backing.getKeyChunk(timestamp);
|
|
if (!keyChunk) {
|
|
return null;
|
|
}
|
|
|
|
const targetChunk = await this.videoTrack._backing.getChunk(timestamp);
|
|
assert(targetChunk);
|
|
|
|
decoder.decode(keyChunk);
|
|
|
|
let currentChunk = keyChunk;
|
|
while (currentChunk !== targetChunk) {
|
|
const nextChunk = await this.videoTrack._backing.getNextChunk(currentChunk);
|
|
assert(nextChunk);
|
|
|
|
currentChunk = nextChunk;
|
|
decoder.decode(nextChunk);
|
|
|
|
if (decoder.decodeQueueSize >= 10) {
|
|
await new Promise(resolve => decoder.addEventListener('dequeue', resolve, { once: true }));
|
|
}
|
|
}
|
|
|
|
await decoder.flush();
|
|
decoder.close();
|
|
|
|
return result;
|
|
}
|
|
|
|
async* frames(startTimestamp = 0) {
|
|
const frameQueue: VideoFrame[] = [];
|
|
let firstFrameQueued = false;
|
|
let lastFrame: VideoFrame | null = null;
|
|
let { promise: queueNonEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers();
|
|
let ended = false;
|
|
|
|
const decoder = await this.createDecoder((frame) => {
|
|
if (ended) {
|
|
frame.close();
|
|
return;
|
|
}
|
|
|
|
const frameTimestamp = frame.timestamp / 1e6;
|
|
|
|
if (lastFrame) {
|
|
if (frameTimestamp > startTimestamp) {
|
|
frameQueue.push(lastFrame);
|
|
firstFrameQueued = true;
|
|
} else {
|
|
lastFrame.close();
|
|
}
|
|
}
|
|
|
|
if (frameTimestamp >= startTimestamp) {
|
|
frameQueue.push(frame);
|
|
firstFrameQueued = true;
|
|
}
|
|
|
|
lastFrame = firstFrameQueued ? null : frame;
|
|
|
|
if (frameQueue.length > 0) {
|
|
onQueueNotEmpty();
|
|
({ promise: queueNonEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers());
|
|
}
|
|
});
|
|
|
|
const keyChunk = await this.videoTrack._backing.getKeyChunk(startTimestamp);
|
|
if (!keyChunk) {
|
|
return;
|
|
}
|
|
|
|
let decoderIsFlushed = false;
|
|
|
|
// The following is the "pump" process that keeps pumping chunks into the decoder
|
|
void (async () => {
|
|
let currentChunk: EncodedVideoChunk | null = keyChunk;
|
|
|
|
while (currentChunk && !ended) {
|
|
decoder.decode(currentChunk);
|
|
|
|
if (decoder.decodeQueueSize >= 10) {
|
|
await new Promise(resolve => decoder.addEventListener('dequeue', resolve, { once: true }));
|
|
}
|
|
|
|
const nextChunk = await this.videoTrack._backing.getNextChunk(currentChunk);
|
|
currentChunk = nextChunk;
|
|
}
|
|
|
|
await decoder.flush();
|
|
decoder.close();
|
|
|
|
decoderIsFlushed = true;
|
|
onQueueNotEmpty(); // To unstuck the generator
|
|
})();
|
|
|
|
try {
|
|
while (true) {
|
|
if (frameQueue.length > 0) {
|
|
yield frameQueue.shift()!;
|
|
} else if (!decoderIsFlushed) {
|
|
await queueNonEmpty;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
} finally {
|
|
ended = true;
|
|
}
|
|
}
|
|
}
|