mfx
Version:
In-browser video editing toolkit, with effects accelerated by WebGL
142 lines (124 loc) • 3.6 kB
text/typescript
import type { MFXEncodedChunk } from "./types";
import { RingBuffer } from "ring-buffer-ts";
import { MFXTransformStream } from "./stream";
import { ExtendedVideoFrame } from "./frame";
/**
* @group Debug
*/
export class ConsoleWritableStream<T = any> {
writable: WritableStream<T>;
constructor(id: string) {
let size = 0;
let length = 0;
this.writable = new WritableStream({
write(chunk) {
console.log("id", chunk);
length++;
size +=
(chunk as ArrayBuffer)?.byteLength ||
(chunk as any).size ||
(chunk as string)?.length;
},
close() {
console.log(
`Stream ${id} closed: Read ${length} chunks at total of ${size} bytes`,
);
},
abort(err) {
console.error("Stream ${id} aborted:", err);
},
});
}
}
// Expensive function, sample frames before piping for digest
/**
* @group Debug
*/
export class Digest extends MFXTransformStream<
ExtendedVideoFrame | MFXEncodedChunk,
ExtendedVideoFrame | MFXEncodedChunk
> {
get identifier() {
return "Digest";
}
globalChecksum = "";
constructor(
cb: (sum: string) => void,
final: (sum: string) => void = () => {
/** noop */
},
) {
const calculateChecksum = async (buffer: BufferSource) => {
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
let value = "";
super({
transform: async (chunk, controller) => {
let buffer: Uint8Array = new Uint8Array();
if (Array.isArray(chunk) && chunk[0] instanceof Uint8Array) {
buffer = chunk[0];
} else if (
chunk instanceof ExtendedVideoFrame ||
chunk instanceof VideoFrame
) {
buffer = new Uint8Array(chunk.allocationSize());
await chunk.copyTo(buffer);
} else if (chunk instanceof Blob) {
buffer = new Uint8Array(await chunk.arrayBuffer());
} else if (chunk.video) {
buffer = new Uint8Array(chunk.video.chunk.byteLength);
await chunk.video.chunk.copyTo(buffer);
} else if (chunk.audio) {
buffer = new Uint8Array(chunk.audio.chunk.byteLength);
await chunk.audio.chunk.copyTo(buffer);
}
const checksum = await calculateChecksum(buffer);
const combinedSum = new TextEncoder().encode(`${value}_${checksum}`);
value = await calculateChecksum(combinedSum);
if (typeof cb === "function") {
cb(value);
}
this.globalChecksum = value;
controller.enqueue(chunk);
},
flush: () => {
final(value);
},
});
}
}
/**
* @group Debug
*/
export class FPSDebugger extends MFXTransformStream<
ExtendedVideoFrame,
ExtendedVideoFrame
> {
get identifier() {
return "FPSDebugger";
}
ringBuffer: RingBuffer<number>;
lookupWindow: number;
lastRecordedTime = performance.now();
constructor(lookupWindow = 30) {
super({
transform: async (frame, controller) => {
this.ringBuffer.add(performance.now() - this.lastRecordedTime);
this.lastRecordedTime = performance.now();
controller.enqueue(frame);
},
});
this.lookupWindow = lookupWindow;
this.ringBuffer = new RingBuffer<number>(lookupWindow);
}
getFPS() {
return (
1000 /
(this.ringBuffer.toArray().reduce((a, b) => a + b, 0) /
this.ringBuffer.getBufferLength())
);
}
}