mediapipe/mediapipe/web/graph_runner/graph_runner.ts
MediaPipe Team bd73617e5c Internal change
PiperOrigin-RevId: 525854969
2023-04-20 14:47:17 -07:00

1262 lines
53 KiB
TypeScript

// Placeholder for internal dependency on assertTruthy
// Placeholder for internal dependency on jsloader
import {isWebKit} from '../../web/graph_runner/platform_utils';
// Placeholder for internal dependency on trusted resource url
// This file can serve as a common interface for most simple TypeScript
// libraries-- additionally, it can hook automatically into wasm_mediapipe_demo
// to autogenerate simple TS APIs from demos for instantaneous 1P integrations.
/**
* Simple interface for allowing users to set the directory where internal
* wasm-loading and asset-loading code looks (e.g. for .wasm and .data file
* locations).
*/
export declare interface FileLocator {
locateFile: (filename: string) => string;
mainScriptUrlOrBlob?: string;
}
/**
* A listener that receives the contents of a non-empty MediaPipe packet and
* its timestamp.
*/
export type SimpleListener<T> = (data: T, timestamp: number) => void;
/**
* A listener that receives the current MediaPipe packet timestamp. This is
* invoked even for empty packet.
*/
export type EmptyPacketListener = (timestamp: number) => void;
/**
* A listener that receives a single element of vector-returning output packet.
* Receives one element at a time (in order). Once all elements are processed,
* the listener is invoked with `data` set to `unknown` and `done` set to true.
* Intended for internal usage.
*/
export type VectorListener<T> = (data: T, done: boolean, timestamp: number) =>
void;
/**
* A listener that receives the CalculatorGraphConfig in binary encoding.
*/
export type CalculatorGraphConfigListener = (graphConfig: Uint8Array) => void;
/**
* The name of the internal listener that we use to obtain the calculator graph
* config. Intended for internal usage. Exported for testing only.
*/
export const CALCULATOR_GRAPH_CONFIG_LISTENER_NAME = '__graph_config__';
/**
* Declarations for Emscripten's WebAssembly Module behavior, so TS compiler
* doesn't break our JS/C++ bridge.
*/
export declare interface WasmModule {
canvas: HTMLCanvasElement|OffscreenCanvas|null;
HEAPU8: Uint8Array;
HEAPU32: Uint32Array;
HEAPF32: Float32Array;
HEAPF64: Float64Array;
errorListener?: ErrorListener;
_bindTextureToCanvas: () => boolean;
_changeBinaryGraph: (size: number, dataPtr: number) => void;
_changeTextGraph: (size: number, dataPtr: number) => void;
_free: (ptr: number) => void;
_malloc: (size: number) => number;
_processFrame: (width: number, height: number, timestamp: number) => void;
_setAutoRenderToScreen: (enabled: boolean) => void;
_waitUntilIdle: () => void;
// Exposed so that clients of this lib can access this field
dataFileDownloads?: {[url: string]: {loaded: number, total: number}};
// Wasm Module multistream entrypoints. Require
// gl_graph_runner_internal_multi_input as a build dependency.
stringToNewUTF8: (data: string) => number;
_bindTextureToStream: (streamNamePtr: number) => void;
_addBoundTextureToStream:
(streamNamePtr: number, width: number, height: number,
timestamp: number) => void;
_addBoolToInputStream:
(data: boolean, streamNamePtr: number, timestamp: number) => void;
_addDoubleToInputStream:
(data: number, streamNamePtr: number, timestamp: number) => void;
_addFloatToInputStream:
(data: number, streamNamePtr: number, timestamp: number) => void;
_addIntToInputStream:
(data: number, streamNamePtr: number, timestamp: number) => void;
_addStringToInputStream:
(dataPtr: number, streamNamePtr: number, timestamp: number) => void;
_addFlatHashMapToInputStream:
(keysPtr: number, valuesPtr: number, count: number, streamNamePtr: number,
timestamp: number) => void;
_addProtoToInputStream:
(dataPtr: number, dataSize: number, protoNamePtr: number,
streamNamePtr: number, timestamp: number) => void;
_addEmptyPacketToInputStream:
(streamNamePtr: number, timestamp: number) => void;
// Input side packets
_addBoolToInputSidePacket: (data: boolean, streamNamePtr: number) => void;
_addDoubleToInputSidePacket: (data: number, streamNamePtr: number) => void;
_addFloatToInputSidePacket: (data: number, streamNamePtr: number) => void;
_addIntToInputSidePacket: (data: number, streamNamePtr: number) => void;
_addStringToInputSidePacket: (dataPtr: number, streamNamePtr: number) => void;
_addProtoToInputSidePacket:
(dataPtr: number, dataSize: number, protoNamePtr: number,
streamNamePtr: number) => void;
// Map of output streams to packet listeners. Also built as part of
// gl_graph_runner_internal_multi_input.
simpleListeners?:
Record<string, SimpleListener<unknown>|VectorListener<unknown>>;
// Map of output streams to empty packet listeners.
emptyPacketListeners?: Record<string, EmptyPacketListener>;
_attachBoolListener: (streamNamePtr: number) => void;
_attachBoolVectorListener: (streamNamePtr: number) => void;
_attachDoubleListener: (streamNamePtr: number) => void;
_attachDoubleVectorListener: (streamNamePtr: number) => void;
_attachFloatListener: (streamNamePtr: number) => void;
_attachFloatVectorListener: (streamNamePtr: number) => void;
_attachIntListener: (streamNamePtr: number) => void;
_attachIntVectorListener: (streamNamePtr: number) => void;
_attachStringListener: (streamNamePtr: number) => void;
_attachStringVectorListener: (streamNamePtr: number) => void;
_attachProtoListener: (streamNamePtr: number, makeDeepCopy?: boolean) => void;
_attachProtoVectorListener:
(streamNamePtr: number, makeDeepCopy?: boolean) => void;
// Require dependency ":gl_graph_runner_audio_out"
_attachAudioListener: (streamNamePtr: number, makeDeepCopy?: boolean) => void;
// Require dependency ":gl_graph_runner_audio"
_addAudioToInputStream: (dataPtr: number, numChannels: number,
numSamples: number, streamNamePtr: number, timestamp: number) => void;
_configureAudio: (channels: number, samples: number, sampleRate: number,
streamNamePtr: number, headerNamePtr: number) => void;
// Get the graph configuration and invoke the listener configured under
// streamNamePtr
_getGraphConfig: (streamNamePtr: number, makeDeepCopy?: boolean) => void;
// TODO: Refactor to just use a few numbers (perhaps refactor away
// from gl_graph_runner_internal.cc entirely to use something a little more
// streamlined; new version is _processFrame above).
_processGl: (frameDataPtr: number) => number;
}
// Global declarations, for tapping into Window for Wasm blob running
declare global {
interface Window {
// Created by us using wasm-runner script
Module?: WasmModule|FileLocator;
// Created by wasm-runner script
ModuleFactory?: (fileLocator: FileLocator) => Promise<WasmModule>;
}
}
/**
* Fetches each URL in urls, executes them one-by-one in the order they are
* passed, and then returns (or throws if something went amiss).
*/
declare function importScripts(...urls: Array<string|URL>): void;
/**
* Valid types of image sources which we can run our GraphRunner over.
*/
export type ImageSource =
HTMLCanvasElement|HTMLVideoElement|HTMLImageElement|ImageData|ImageBitmap;
/** A listener that will be invoked with an absl::StatusCode and message. */
export type ErrorListener = (code: number, message: string) => void;
/**
* Internal type of constructors used for initializing GraphRunner and
* subclasses.
*/
export type WasmMediaPipeConstructor<LibType> =
(new (
module: WasmModule, canvas?: HTMLCanvasElement|OffscreenCanvas|null) =>
LibType);
/**
* Simple class to run an arbitrary image-in/image-out MediaPipe graph (i.e.
* as created by wasm_mediapipe_demo BUILD macro), and either render results
* into canvas, or else return the output WebGLTexture. Takes a WebAssembly
* Module.
*/
export class GraphRunner {
// TODO: These should be protected/private, but are left exposed for
// now so that we can use proper TS mixins with this class as a base. This
// should be somewhat fixed when we create our .d.ts files.
readonly wasmModule: WasmModule;
readonly hasMultiStreamSupport: boolean;
autoResizeCanvas: boolean = true;
audioPtr: number|null;
audioSize: number;
/**
* Creates a new MediaPipe WASM module. Must be called *after* wasm Module has
* initialized. Note that we take control of the GL canvas from here on out,
* and will resize it to fit input.
*
* @param module The underlying Wasm Module to use.
* @param glCanvas The type of the GL canvas to use, or `null` if no GL
* canvas should be initialzed. Initializes an offscreen canvas if not
* provided.
*/
constructor(
module: WasmModule, glCanvas?: HTMLCanvasElement|OffscreenCanvas|null) {
this.wasmModule = module;
this.audioPtr = null;
this.audioSize = 0;
this.hasMultiStreamSupport =
(typeof this.wasmModule._addIntToInputStream === 'function');
if (glCanvas !== undefined) {
this.wasmModule.canvas = glCanvas;
} else if (typeof OffscreenCanvas !== 'undefined' && !isWebKit()) {
// If no canvas is provided, assume Chrome/Firefox and just make an
// OffscreenCanvas for GPU processing. Note that we exclude Safari
// since it does not (yet) support WebGL for OffscreenCanvas.
this.wasmModule.canvas = new OffscreenCanvas(1, 1);
} else {
console.warn(
'OffscreenCanvas not supported and GraphRunner constructor ' +
'glCanvas parameter is undefined. Creating backup canvas.');
this.wasmModule.canvas = document.createElement('canvas');
}
}
/**
* Convenience helper to load a MediaPipe graph from a file and pass it to
* setGraph.
* @param graphFile The url of the MediaPipe graph file to load.
*/
async initializeGraph(graphFile: string): Promise<void> {
// Fetch and set graph
const response = await fetch(graphFile);
const graphData = await response.arrayBuffer();
const isBinary =
!(graphFile.endsWith('.pbtxt') || graphFile.endsWith('.textproto'));
this.setGraph(new Uint8Array(graphData), isBinary);
}
/**
* Convenience helper for calling setGraph with a string representing a text
* proto config.
* @param graphConfig The text proto graph config, expected to be a string in
* default JavaScript UTF-16 format.
*/
setGraphFromString(graphConfig: string): void {
this.setGraph((new TextEncoder()).encode(graphConfig), false);
}
/**
* Takes the raw data from a MediaPipe graph, and passes it to C++ to be run
* over the video stream. Will replace the previously running MediaPipe graph,
* if there is one.
* @param graphData The raw MediaPipe graph data, either in binary
* protobuffer format (.binarypb), or else in raw text format (.pbtxt or
* .textproto).
* @param isBinary This should be set to true if the graph is in
* binary format, and false if it is in human-readable text format.
*/
setGraph(graphData: Uint8Array, isBinary: boolean): void {
const size = graphData.length;
const dataPtr = this.wasmModule._malloc(size);
this.wasmModule.HEAPU8.set(graphData, dataPtr);
if (isBinary) {
this.wasmModule._changeBinaryGraph(size, dataPtr);
} else {
this.wasmModule._changeTextGraph(size, dataPtr);
}
this.wasmModule._free(dataPtr);
}
/**
* Configures the current graph to handle audio processing in a certain way
* for all its audio input streams. Additionally can configure audio headers
* (both input side packets as well as input stream headers), but these
* configurations only take effect if called before the graph is set/started.
* @param numChannels The number of channels of audio input. Only 1
* is supported for now.
* @param numSamples The number of samples that are taken in each
* audio capture.
* @param sampleRate The rate, in Hz, of the sampling.
* @param streamName The optional name of the input stream to additionally
* configure with audio information. This configuration only occurs before
* the graph is set/started. If unset, a default stream name will be used.
* @param headerName The optional name of the header input side packet to
* additionally configure with audio information. This configuration only
* occurs before the graph is set/started. If unset, a default header name
* will be used.
*/
configureAudio(numChannels: number, numSamples: number, sampleRate: number,
streamName?: string, headerName?: string) {
if (!this.wasmModule._configureAudio) {
console.warn(
'Attempting to use configureAudio without support for input audio. ' +
'Is build dep ":gl_graph_runner_audio" missing?');
}
streamName = streamName || 'input_audio';
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
headerName = headerName || 'audio_header';
this.wrapStringPtr(headerName, (headerNamePtr: number) => {
this.wasmModule._configureAudio(streamNamePtr, headerNamePtr,
numChannels, numSamples, sampleRate);
});
});
}
/**
* Allows disabling automatic canvas resizing, in case clients want to control
* control this.
* @param resize True will re-enable automatic canvas resizing, while false
* will disable the feature.
*/
setAutoResizeCanvas(resize: boolean): void {
this.autoResizeCanvas = resize;
}
/**
* Allows disabling the automatic render-to-screen code, in case clients don't
* need/want this. In particular, this removes the requirement for pipelines
* to have access to GPU resources, as well as the requirement for graphs to
* have "input_frames_gpu" and "output_frames_gpu" streams defined, so pure
* CPU pipelines and non-video pipelines can be created.
* NOTE: This only affects future graph initializations (via setGraph or
* initializeGraph), and does NOT affect the currently running graph, so
* calls to this should be made *before* setGraph/initializeGraph for the
* graph file being targeted.
* @param enabled True will re-enable automatic render-to-screen code and
* cause GPU resources to once again be requested, while false will
* disable the feature.
*/
setAutoRenderToScreen(enabled: boolean): void {
this.wasmModule._setAutoRenderToScreen(enabled);
}
/**
* Bind texture to our internal canvas, and upload image source to GPU.
* Returns tuple [width, height] of texture. Intended for internal usage.
*/
bindTextureToStream(imageSource: ImageSource, streamNamePtr?: number):
[number, number] {
if (!this.wasmModule.canvas) {
throw new Error('No OpenGL canvas configured.');
}
if (!streamNamePtr) {
// TODO: Remove this path once completely refactored away.
console.assert(this.wasmModule._bindTextureToCanvas());
} else {
this.wasmModule._bindTextureToStream(streamNamePtr);
}
const gl =
(this.wasmModule.canvas.getContext('webgl2') ||
this.wasmModule.canvas.getContext('webgl')) as WebGL2RenderingContext |
WebGLRenderingContext | null;
if (!gl) {
throw new Error(
'Failed to obtain WebGL context from the provided canvas. ' +
'`getContext()` should only be invoked with `webgl` or `webgl2`.');
}
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageSource);
let width, height;
if ((imageSource as HTMLVideoElement).videoWidth) {
width = (imageSource as HTMLVideoElement).videoWidth;
height = (imageSource as HTMLVideoElement).videoHeight;
} else if ((imageSource as HTMLImageElement).naturalWidth) {
// TODO: Ensure this works with SVG images
width = (imageSource as HTMLImageElement).naturalWidth;
height = (imageSource as HTMLImageElement).naturalHeight;
} else {
width = imageSource.width;
height = imageSource.height;
}
if (this.autoResizeCanvas &&
(width !== this.wasmModule.canvas.width ||
height !== this.wasmModule.canvas.height)) {
this.wasmModule.canvas.width = width;
this.wasmModule.canvas.height = height;
}
return [width, height];
}
/**
* Takes the raw data from a JS image source, and sends it to C++ to be
* processed, waiting synchronously for the response. Note that we will resize
* our GL canvas to fit the input, so input size should only change
* infrequently.
* @param imageSource An image source to process.
* @param timestamp The timestamp of the current frame, in ms.
* @return texture? The WebGL texture reference, if one was produced.
*/
processGl(imageSource: ImageSource, timestamp: number): WebGLTexture
|undefined {
// Bind to default input stream
const [width, height] = this.bindTextureToStream(imageSource);
// 2 ints and a ll (timestamp)
const frameDataPtr = this.wasmModule._malloc(16);
this.wasmModule.HEAPU32[frameDataPtr / 4] = width;
this.wasmModule.HEAPU32[(frameDataPtr / 4) + 1] = height;
this.wasmModule.HEAPF64[(frameDataPtr / 8) + 1] = timestamp;
// outputPtr points in HEAPF32-space to running mspf calculations, which we
// don't use at the moment.
// tslint:disable-next-line:no-unused-variable
const outputPtr = this.wasmModule._processGl(frameDataPtr) / 4;
this.wasmModule._free(frameDataPtr);
// TODO: Hook up WebGLTexture output, when given.
// TODO: Allow user to toggle whether or not to render output into canvas.
return undefined;
}
/**
* Converts JavaScript string input parameters into C++ c-string pointers.
* See b/204830158 for more details. Intended for internal usage.
*/
wrapStringPtr(stringData: string, stringPtrFunc: (ptr: number) => void):
void {
if (!this.hasMultiStreamSupport) {
console.error(
'No wasm multistream support detected: ensure dependency ' +
'inclusion of :gl_graph_runner_internal_multi_input target');
}
const stringDataPtr = this.wasmModule.stringToNewUTF8(stringData);
stringPtrFunc(stringDataPtr);
this.wasmModule._free(stringDataPtr);
}
/**
* Converts JavaScript string input parameters into C++ c-string pointers.
* See b/204830158 for more details.
*/
wrapStringPtrPtr(stringData: string[], ptrFunc: (ptr: number) => void): void {
if (!this.hasMultiStreamSupport) {
console.error(
'No wasm multistream support detected: ensure dependency ' +
'inclusion of :gl_graph_runner_internal_multi_input target');
}
const uint32Array = new Uint32Array(stringData.length);
for (let i = 0; i < stringData.length; i++) {
uint32Array[i] = this.wasmModule.stringToNewUTF8(stringData[i]);
}
const heapSpace = this.wasmModule._malloc(uint32Array.length * 4);
this.wasmModule.HEAPU32.set(uint32Array, heapSpace >> 2);
ptrFunc(heapSpace);
for (const uint32ptr of uint32Array) {
this.wasmModule._free(uint32ptr);
}
this.wasmModule._free(heapSpace);
}
/**
* Invokes the callback with the current calculator configuration (in binary
* format).
*
* Consumers must deserialize the binary representation themselves as this
* avoids addding a direct dependency on the Protobuf JSPB target in the graph
* library.
*/
getCalculatorGraphConfig(
callback: CalculatorGraphConfigListener, makeDeepCopy?: boolean): void {
const listener = CALCULATOR_GRAPH_CONFIG_LISTENER_NAME;
// Create a short-lived listener to receive the binary encoded proto
this.setListener(listener, (data: Uint8Array) => {
callback(data);
});
this.wrapStringPtr(listener, (outputStreamNamePtr: number) => {
this.wasmModule._getGraphConfig(outputStreamNamePtr, makeDeepCopy);
});
delete this.wasmModule.simpleListeners![listener];
}
/**
* Ensures existence of the simple listeners table and registers the callback.
* Intended for internal usage.
*/
setListener<T>(outputStreamName: string, callbackFcn: SimpleListener<T>) {
this.wasmModule.simpleListeners = this.wasmModule.simpleListeners || {};
this.wasmModule.simpleListeners[outputStreamName] =
callbackFcn as SimpleListener<unknown>;
}
/**
* Ensures existence of the vector listeners table and registers the callback.
* Intended for internal usage.
*/
setVectorListener<T>(
outputStreamName: string, callbackFcn: SimpleListener<T[]>) {
let buffer: T[] = [];
this.wasmModule.simpleListeners = this.wasmModule.simpleListeners || {};
this.wasmModule.simpleListeners[outputStreamName] =
(data: unknown, done: boolean, timestamp: number) => {
if (done) {
callbackFcn(buffer, timestamp);
buffer = [];
} else {
buffer.push(data as T);
}
};
}
/**
* Attaches a listener that will be invoked when the MediaPipe framework
* returns an error.
*/
attachErrorListener(callbackFcn: (code: number, message: string) => void) {
this.wasmModule.errorListener = callbackFcn;
}
/**
* Attaches a listener that will be invoked when the MediaPipe framework
* receives an empty packet on the provided output stream. This can be used
* to receive the latest output timestamp.
*
* Empty packet listeners are only active if there is a corresponding packet
* listener.
*
* @param outputStreamName The name of the graph output stream to receive
* empty packets from.
* @param callbackFcn The callback to receive the timestamp.
*/
attachEmptyPacketListener(
outputStreamName: string, callbackFcn: EmptyPacketListener) {
this.wasmModule.emptyPacketListeners =
this.wasmModule.emptyPacketListeners || {};
this.wasmModule.emptyPacketListeners[outputStreamName] = callbackFcn;
}
/**
* Takes the raw data from a JS audio capture array, and sends it to C++ to be
* processed.
* @param audioData An array of raw audio capture data, like
* from a call to getChannelData on an AudioBuffer.
* @param streamName The name of the MediaPipe graph stream to add the audio
* data to.
* @param timestamp The timestamp of the current frame, in ms.
*/
addAudioToStream(
audioData: Float32Array, streamName: string, timestamp: number) {
// numChannels and numSamples being 0 will cause defaults to be used,
// which will reflect values from last call to configureAudio.
this.addAudioToStreamWithShape(audioData, 0, 0, streamName, timestamp);
}
/**
* Takes the raw data from a JS audio capture array, and sends it to C++ to be
* processed, shaping the audioData array into an audio matrix according to
* the numChannels and numSamples parameters.
* @param audioData An array of raw audio capture data, like
* from a call to getChannelData on an AudioBuffer.
* @param numChannels The number of audio channels this data represents. If 0
* is passed, then the value will be taken from the last call to
* configureAudio.
* @param numSamples The number of audio samples captured in this data packet.
* If 0 is passed, then the value will be taken from the last call to
* configureAudio.
* @param streamName The name of the MediaPipe graph stream to add the audio
* data to.
* @param timestamp The timestamp of the current frame, in ms.
*/
addAudioToStreamWithShape(
audioData: Float32Array, numChannels: number, numSamples: number,
streamName: string, timestamp: number) {
// 4 bytes for each F32
const size = audioData.length * 4;
if (this.audioSize !== size) {
if (this.audioPtr) {
this.wasmModule._free(this.audioPtr);
}
this.audioPtr = this.wasmModule._malloc(size);
this.audioSize = size;
}
this.wasmModule.HEAPF32.set(audioData, this.audioPtr! / 4);
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
this.wasmModule._addAudioToInputStream(
this.audioPtr!, numChannels, numSamples, streamNamePtr, timestamp);
});
}
/**
* Takes the relevant information from the HTML video or image element, and
* passes it into the WebGL-based graph for processing on the given stream at
* the given timestamp. Can be used for additional auxiliary GpuBuffer input
* streams. Processing will not occur until a blocking call (like
* processVideoGl or finishProcessing) is made. For use with
* 'gl_graph_runner_internal_multi_input'.
* @param imageSource Reference to the video frame we wish to add into our
* graph.
* @param streamName The name of the MediaPipe graph stream to add the frame
* to.
* @param timestamp The timestamp of the input frame, in ms.
*/
addGpuBufferToStream(
imageSource: ImageSource, streamName: string, timestamp: number): void {
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
const [width, height] =
this.bindTextureToStream(imageSource, streamNamePtr);
this.wasmModule._addBoundTextureToStream(
streamNamePtr, width, height, timestamp);
});
}
/**
* Sends a boolean packet into the specified stream at the given timestamp.
* @param data The boolean data to send.
* @param streamName The name of the graph input stream to send data into.
* @param timestamp The timestamp of the input data, in ms.
*/
addBoolToStream(data: boolean, streamName: string, timestamp: number): void {
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
this.wasmModule._addBoolToInputStream(data, streamNamePtr, timestamp);
});
}
/**
* Sends a double packet into the specified stream at the given timestamp.
* @param data The double data to send.
* @param streamName The name of the graph input stream to send data into.
* @param timestamp The timestamp of the input data, in ms.
*/
addDoubleToStream(data: number, streamName: string, timestamp: number): void {
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
this.wasmModule._addDoubleToInputStream(data, streamNamePtr, timestamp);
});
}
/**
* Sends a float packet into the specified stream at the given timestamp.
* @param data The float data to send.
* @param streamName The name of the graph input stream to send data into.
* @param timestamp The timestamp of the input data, in ms.
*/
addFloatToStream(data: number, streamName: string, timestamp: number): void {
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
// NOTE: _addFloatToStream and _addIntToStream are reserved for JS
// Calculators currently; we may want to revisit this naming scheme in the
// future.
this.wasmModule._addFloatToInputStream(data, streamNamePtr, timestamp);
});
}
/**
* Sends an integer packet into the specified stream at the given timestamp.
* @param data The integer data to send.
* @param streamName The name of the graph input stream to send data into.
* @param timestamp The timestamp of the input data, in ms.
*/
addIntToStream(data: number, streamName: string, timestamp: number): void {
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
this.wasmModule._addIntToInputStream(data, streamNamePtr, timestamp);
});
}
/**
* Sends a string packet into the specified stream at the given timestamp.
* @param data The string data to send.
* @param streamName The name of the graph input stream to send data into.
* @param timestamp The timestamp of the input data, in ms.
*/
addStringToStream(data: string, streamName: string, timestamp: number): void {
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
this.wrapStringPtr(data, (dataPtr: number) => {
this.wasmModule._addStringToInputStream(
dataPtr, streamNamePtr, timestamp);
});
});
}
/**
* Sends a Record<string, string> packet into the specified stream at the
* given timestamp.
* @param data The records to send (will become a
* std::flat_hash_map<std::string, std::string).
* @param streamName The name of the graph input stream to send data into.
* @param timestamp The timestamp of the input data, in ms.
*/
addStringRecordToStream(
data: Record<string, string>, streamName: string,
timestamp: number): void {
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
this.wrapStringPtrPtr(Object.keys(data), (keyList: number) => {
this.wrapStringPtrPtr(Object.values(data), (valueList: number) => {
this.wasmModule._addFlatHashMapToInputStream(
keyList, valueList, Object.keys(data).length, streamNamePtr,
timestamp);
});
});
});
}
/**
* Sends a serialized protobuffer packet into the specified stream at the
* given timestamp, to be parsed into the specified protobuffer type.
* @param data The binary (serialized) raw protobuffer data.
* @param protoType The C++ namespaced type this protobuffer data corresponds
* to. It will be converted to this type when output as a packet into the
* graph.
* @param streamName The name of the graph input stream to send data into.
* @param timestamp The timestamp of the input data, in ms.
*/
addProtoToStream(
data: Uint8Array, protoType: string, streamName: string,
timestamp: number): void {
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
this.wrapStringPtr(protoType, (protoTypePtr: number) => {
// Deep-copy proto data into Wasm heap
const dataPtr = this.wasmModule._malloc(data.length);
// TODO: Ensure this is the fastest way to copy this data.
this.wasmModule.HEAPU8.set(data, dataPtr);
this.wasmModule._addProtoToInputStream(
dataPtr, data.length, protoTypePtr, streamNamePtr, timestamp);
this.wasmModule._free(dataPtr);
});
});
}
/**
* Sends an empty packet into the specified stream at the given timestamp,
* effectively advancing that input stream's timestamp bounds without
* sending additional data packets.
* @param streamName The name of the graph input stream to send the empty
* packet into.
* @param timestamp The timestamp of the empty packet, in ms.
*/
addEmptyPacketToStream(streamName: string, timestamp: number): void {
this.wrapStringPtr(streamName, (streamNamePtr: number) => {
this.wasmModule._addEmptyPacketToInputStream(streamNamePtr, timestamp);
});
}
/**
* Attaches a boolean packet to the specified input_side_packet.
* @param data The boolean data to send.
* @param sidePacketName The name of the graph input side packet to send data
* into.
*/
addBoolToInputSidePacket(data: boolean, sidePacketName: string): void {
this.wrapStringPtr(sidePacketName, (sidePacketNamePtr: number) => {
this.wasmModule._addBoolToInputSidePacket(data, sidePacketNamePtr);
});
}
/**
* Attaches a double packet to the specified input_side_packet.
* @param data The double data to send.
* @param sidePacketName The name of the graph input side packet to send data
* into.
*/
addDoubleToInputSidePacket(data: number, sidePacketName: string): void {
this.wrapStringPtr(sidePacketName, (sidePacketNamePtr: number) => {
this.wasmModule._addDoubleToInputSidePacket(data, sidePacketNamePtr);
});
}
/**
* Attaches a float packet to the specified input_side_packet.
* @param data The float data to send.
* @param sidePacketName The name of the graph input side packet to send data
* into.
*/
addFloatToInputSidePacket(data: number, sidePacketName: string): void {
this.wrapStringPtr(sidePacketName, (sidePacketNamePtr: number) => {
this.wasmModule._addFloatToInputSidePacket(data, sidePacketNamePtr);
});
}
/**
* Attaches a integer packet to the specified input_side_packet.
* @param data The integer data to send.
* @param sidePacketName The name of the graph input side packet to send data
* into.
*/
addIntToInputSidePacket(data: number, sidePacketName: string): void {
this.wrapStringPtr(sidePacketName, (sidePacketNamePtr: number) => {
this.wasmModule._addIntToInputSidePacket(data, sidePacketNamePtr);
});
}
/**
* Attaches a string packet to the specified input_side_packet.
* @param data The string data to send.
* @param sidePacketName The name of the graph input side packet to send data
* into.
*/
addStringToInputSidePacket(data: string, sidePacketName: string): void {
this.wrapStringPtr(sidePacketName, (sidePacketNamePtr: number) => {
this.wrapStringPtr(data, (dataPtr: number) => {
this.wasmModule._addStringToInputSidePacket(dataPtr, sidePacketNamePtr);
});
});
}
/**
* Attaches a serialized proto packet to the specified input_side_packet.
* @param data The binary (serialized) raw protobuffer data.
* @param protoType The C++ namespaced type this protobuffer data corresponds
* to. It will be converted to this type for use in the graph.
* @param sidePacketName The name of the graph input side packet to send data
* into.
*/
addProtoToInputSidePacket(
data: Uint8Array, protoType: string, sidePacketName: string): void {
this.wrapStringPtr(sidePacketName, (sidePacketNamePtr: number) => {
this.wrapStringPtr(protoType, (protoTypePtr: number) => {
// Deep-copy proto data into Wasm heap
const dataPtr = this.wasmModule._malloc(data.length);
// TODO: Ensure this is the fastest way to copy this data.
this.wasmModule.HEAPU8.set(data, dataPtr);
this.wasmModule._addProtoToInputSidePacket(
dataPtr, data.length, protoTypePtr, sidePacketNamePtr);
this.wasmModule._free(dataPtr);
});
});
}
/**
* Attaches a boolean packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab boolean
* data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior.
*/
attachBoolListener(
outputStreamName: string, callbackFcn: SimpleListener<boolean>): void {
// Set up our TS listener to receive any packets for this stream.
this.setListener(outputStreamName, callbackFcn);
// Tell our graph to listen for bool packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachBoolListener(outputStreamNamePtr);
});
}
/**
* Attaches a bool[] packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab
* std::vector<bool> data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior.
*/
attachBoolVectorListener(
outputStreamName: string, callbackFcn: SimpleListener<boolean[]>): void {
// Set up our TS listener to receive any packets for this stream.
this.setVectorListener(outputStreamName, callbackFcn);
// Tell our graph to listen for std::vector<bool> packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachBoolVectorListener(outputStreamNamePtr);
});
}
/**
* Attaches an int packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab int
* data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior.
*/
attachIntListener(
outputStreamName: string, callbackFcn: SimpleListener<number>): void {
// Set up our TS listener to receive any packets for this stream.
this.setListener(outputStreamName, callbackFcn);
// Tell our graph to listen for int packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachIntListener(outputStreamNamePtr);
});
}
/**
* Attaches an int[] packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab
* std::vector<int> data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior.
*/
attachIntVectorListener(
outputStreamName: string, callbackFcn: SimpleListener<number[]>): void {
// Set up our TS listener to receive any packets for this stream.
this.setVectorListener(outputStreamName, callbackFcn);
// Tell our graph to listen for std::vector<int> packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachIntVectorListener(outputStreamNamePtr);
});
}
/**
* Attaches a double packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab double
* data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior.
*/
attachDoubleListener(
outputStreamName: string, callbackFcn: SimpleListener<number>): void {
// Set up our TS listener to receive any packets for this stream.
this.setListener(outputStreamName, callbackFcn);
// Tell our graph to listen for double packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachDoubleListener(outputStreamNamePtr);
});
}
/**
* Attaches a double[] packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab
* std::vector<double> data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior.
*/
attachDoubleVectorListener(
outputStreamName: string, callbackFcn: SimpleListener<number[]>): void {
// Set up our TS listener to receive any packets for this stream.
this.setVectorListener(outputStreamName, callbackFcn);
// Tell our graph to listen for std::vector<double> packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachDoubleVectorListener(outputStreamNamePtr);
});
}
/**
* Attaches a float packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab float
* data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior.
*/
attachFloatListener(
outputStreamName: string, callbackFcn: SimpleListener<number>): void {
// Set up our TS listener to receive any packets for this stream.
this.setListener(outputStreamName, callbackFcn);
// Tell our graph to listen for float packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachFloatListener(outputStreamNamePtr);
});
}
/**
* Attaches a float[] packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab
* std::vector<float> data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior.
*/
attachFloatVectorListener(
outputStreamName: string, callbackFcn: SimpleListener<number[]>): void {
// Set up our TS listener to receive any packets for this stream.
this.setVectorListener(outputStreamName, callbackFcn);
// Tell our graph to listen for std::vector<float> packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachFloatVectorListener(outputStreamNamePtr);
});
}
/**
* Attaches a string packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab string
* data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior.
*/
attachStringListener(
outputStreamName: string, callbackFcn: SimpleListener<string>): void {
// Set up our TS listener to receive any packets for this stream.
this.setListener(outputStreamName, callbackFcn);
// Tell our graph to listen for string packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachStringListener(outputStreamNamePtr);
});
}
/**
* Attaches a string[] packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab
* std::vector<std::string> data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior.
*/
attachStringVectorListener(
outputStreamName: string, callbackFcn: SimpleListener<string[]>): void {
// Set up our TS listener to receive any packets for this stream.
this.setVectorListener(outputStreamName, callbackFcn);
// Tell our graph to listen for std::vector<string> packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachStringVectorListener(outputStreamNamePtr);
});
}
/**
* Attaches a serialized proto packet listener to the specified output_stream.
* @param outputStreamName The name of the graph output stream to grab binary
* serialized proto data from (in Uint8Array format).
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that by default the data is only guaranteed to
* exist for the duration of the callback, and the callback will be called
* inline, so it should not perform overly complicated (or any async)
* behavior. If the proto data needs to be able to outlive the call, you
* may set the optional makeDeepCopy parameter to true, or can manually
* deep-copy the data yourself.
* @param makeDeepCopy Optional convenience parameter which, if set to true,
* will override the default memory management behavior and make a deep
* copy of the underlying data, rather than just returning a view into the
* C++-managed memory. At the cost of a data copy, this allows the
* returned data to outlive the callback lifetime (and it will be cleaned
* up automatically by JS garbage collection whenever the user is finished
* with it).
*/
attachProtoListener(
outputStreamName: string, callbackFcn: SimpleListener<Uint8Array>,
makeDeepCopy?: boolean): void {
// Set up our TS listener to receive any packets for this stream.
this.setListener(outputStreamName, callbackFcn);
// Tell our graph to listen for binary serialized proto data packets on this
// stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachProtoListener(
outputStreamNamePtr, makeDeepCopy || false);
});
}
/**
* Attaches a listener for an array of serialized proto packets to the
* specified output_stream.
* @param outputStreamName The name of the graph output stream to grab a
* vector of binary serialized proto data from (in Uint8Array[] format).
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that by default the data is only guaranteed to
* exist for the duration of the callback, and the callback will be called
* inline, so it should not perform overly complicated (or any async)
* behavior. If the proto data needs to be able to outlive the call, you
* may set the optional makeDeepCopy parameter to true, or can manually
* deep-copy the data yourself.
* @param makeDeepCopy Optional convenience parameter which, if set to true,
* will override the default memory management behavior and make a deep
* copy of the underlying data, rather than just returning a view into the
* C++-managed memory. At the cost of a data copy, this allows the
* returned data to outlive the callback lifetime (and it will be cleaned
* up automatically by JS garbage collection whenever the user is finished
* with it).
*/
attachProtoVectorListener(
outputStreamName: string, callbackFcn: SimpleListener<Uint8Array[]>,
makeDeepCopy?: boolean): void {
// Set up our TS listener to receive any packets for this stream.
this.setVectorListener(outputStreamName, callbackFcn);
// Tell our graph to listen for a vector of binary serialized proto packets
// on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachProtoVectorListener(
outputStreamNamePtr, makeDeepCopy || false);
});
}
/**
* Attaches an audio packet listener to the specified output_stream, to be
* given a Float32Array as output.
* @param outputStreamName The name of the graph output stream to grab audio
* data from.
* @param callbackFcn The function that will be called back with the data, as
* it is received. Note that the data is only guaranteed to exist for the
* duration of the callback, and the callback will be called inline, so it
* should not perform overly complicated (or any async) behavior. If the
* audio data needs to be able to outlive the call, you may set the
* optional makeDeepCopy parameter to true, or can manually deep-copy the
* data yourself.
* @param makeDeepCopy Optional convenience parameter which, if set to true,
* will override the default memory management behavior and make a deep
* copy of the underlying data, rather than just returning a view into the
* C++-managed memory. At the cost of a data copy, this allows the
* returned data to outlive the callback lifetime (and it will be cleaned
* up automatically by JS garbage collection whenever the user is finished
* with it).
*/
attachAudioListener(
outputStreamName: string, callbackFcn: SimpleListener<Float32Array>,
makeDeepCopy?: boolean): void {
if (!this.wasmModule._attachAudioListener) {
console.warn(
'Attempting to use attachAudioListener without support for ' +
'output audio. Is build dep ":gl_graph_runner_audio_out" missing?');
}
// Set up our TS listener to receive any packets for this stream, and
// additionally reformat our Uint8Array into a Float32Array for the user.
this.setListener<Uint8Array>(outputStreamName, (data, timestamp) => {
// Should be very fast
const floatArray =
new Float32Array(data.buffer, data.byteOffset, data.length / 4);
callbackFcn(floatArray, timestamp);
});
// Tell our graph to listen for string packets on this stream.
this.wrapStringPtr(outputStreamName, (outputStreamNamePtr: number) => {
this.wasmModule._attachAudioListener(
outputStreamNamePtr, makeDeepCopy || false);
});
}
/**
* Forces all queued-up packets to be pushed through the MediaPipe graph as
* far as possible, performing all processing until no more processing can be
* done.
*/
finishProcessing(): void {
this.wasmModule._waitUntilIdle();
}
}
// Quick private helper to run the given script safely
async function runScript(scriptUrl: string) {
if (typeof importScripts === 'function') {
importScripts(scriptUrl.toString());
} else {
const script = document.createElement('script');
script.setAttribute('src', scriptUrl);
script.setAttribute('crossorigin', 'anonymous');
return new Promise<void>((resolve) => {
script.addEventListener('load', () => {
resolve();
}, false);
script.addEventListener('error', () => {
resolve();
}, false);
document.body.appendChild(script);
});
}
}
/**
* Helper type macro for use with createMediaPipeLib. Allows us to easily infer
* the type of a mixin-extended GraphRunner. Example usage:
* const GraphRunnerConstructor =
* SupportImage(SupportSerialization(GraphRunner));
* let mediaPipe: ReturnType<typeof GraphRunnerConstructor>;
* ...
* mediaPipe = await createMediaPipeLib(GraphRunnerConstructor, ...);
*/
// tslint:disable-next-line:no-any
export type ReturnType<T> = T extends (...args: unknown[]) => infer R ? R : any;
/**
* Global function to initialize Wasm blob and load runtime assets for a
* specialized MediaPipe library. This allows us to create a requested
* subclass inheriting from GraphRunner.
* @param constructorFcn The name of the class to instantiate via "new".
* @param wasmLoaderScript Url for the wasm-runner script; produced by the build
* process.
* @param assetLoaderScript Url for the asset-loading script; produced by the
* build process.
* @param fileLocator A function to override the file locations for assets
* loaded by the MediaPipe library.
* @return promise A promise which will resolve when initialization has
* completed successfully.
*/
export async function createMediaPipeLib<LibType>(
constructorFcn: WasmMediaPipeConstructor<LibType>,
wasmLoaderScript?: string|null,
assetLoaderScript?: string|null,
glCanvas?: HTMLCanvasElement|OffscreenCanvas|null,
fileLocator?: FileLocator): Promise<LibType> {
const scripts = [];
// Run wasm-loader script here
if (wasmLoaderScript) {
scripts.push(wasmLoaderScript);
}
// Run asset-loader script here
if (assetLoaderScript) {
scripts.push(assetLoaderScript);
}
// Load scripts in parallel, browser will execute them in sequence.
if (scripts.length) {
await Promise.all(scripts.map(runScript));
}
if (!self.ModuleFactory) {
throw new Error('ModuleFactory not set.');
}
// Until asset scripts work nicely with MODULARIZE, when we are given both
// self.Module and a fileLocator, we manually merge them into self.Module and
// use that. TODO: Remove this when asset scripts are fixed.
if (self.Module && fileLocator) {
const moduleFileLocator = self.Module as FileLocator;
moduleFileLocator.locateFile = fileLocator.locateFile;
if (fileLocator.mainScriptUrlOrBlob) {
moduleFileLocator.mainScriptUrlOrBlob = fileLocator.mainScriptUrlOrBlob;
}
}
// TODO: Ensure that fileLocator is passed in by all users
// and make it required
const module =
await self.ModuleFactory(self.Module as FileLocator || fileLocator);
// Don't reuse factory or module seed
self.ModuleFactory = self.Module = undefined;
return new constructorFcn(module, glCanvas);
}
/**
* Global function to initialize Wasm blob and load runtime assets for a generic
* MediaPipe library.
* @param wasmLoaderScript Url for the wasm-runner script; produced by the build
* process.
* @param assetLoaderScript Url for the asset-loading script; produced by the
* build process.
* @param fileLocator A function to override the file locations for assets
* loaded by the MediaPipe library.
* @return promise A promise which will resolve when initialization has
* completed successfully.
*/
export async function createGraphRunner(
wasmLoaderScript?: string,
assetLoaderScript?: string,
glCanvas?: HTMLCanvasElement|OffscreenCanvas|null,
fileLocator?: FileLocator): Promise<GraphRunner> {
return createMediaPipeLib(
GraphRunner, wasmLoaderScript, assetLoaderScript, glCanvas,
fileLocator);
}