Files
mediabunny/src/media-drain.ts
T
2024-12-08 15:19:56 +01:00

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