Skip to content

Instantly share code, notes, and snippets.

@ThorstenBux
Last active April 7, 2021 16:02
Show Gist options
  • Save ThorstenBux/927327bd80da3b1fd83e24f3ce621f04 to your computer and use it in GitHub Desktop.
Save ThorstenBux/927327bd80da3b1fd83e24f3ce621f04 to your computer and use it in GitHub Desktop.
HTML5 canvas video recording. Taken from BabylonJS and modified to be independent of BabylonJS
/* eslint-disable no-underscore-dangle */
/**
This code has been taken from BabylonJS
https://github.com/BabylonJS/Babylon.js/blob/master/src/Misc/videoRecorder.ts
and modified by Thorsten Bux (thorsten@tripod-digital.co.nz)
* */
type Nullable<T> = T | null;
/**
* Downloads a blob in the browser
* @param blob defines the blob to download
* @param fileName defines the name of the downloaded file
*/
function download(blob: Blob, fileName: string): void {
if (navigator && navigator.msSaveBlob) {
navigator.msSaveBlob(blob, fileName);
return;
}
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
document.body.appendChild(a);
a.style.display = 'none';
a.href = url;
a.download = fileName;
a.addEventListener('click', () => {
if (a.parentElement) {
a.parentElement.removeChild(a);
}
});
a.click();
window.URL.revokeObjectURL(url);
}
interface MediaRecorder {
/** Starts recording */
start(timeSlice: number): void;
/** Stops recording */
stop(): void;
/** Event raised when an error arised. */
onerror: (event: ErrorEvent) => void;
/** Event raised when the recording stops. */
onstop: (event: Event) => void;
/** Event raised when a new chunk of data is available and should be tracked. */
ondataavailable: (event: Event) => void;
}
interface MediaRecorderOptions {
/** The mime type you want to use as the recording container for the new MediaRecorder. */
mimeType?: string;
/** The chosen bitrate for the audio component of the media. */
audioBitsPerSecond?: number;
/** The chosen bitrate for the video component of the media. */
videoBitsPerSecond?: number;
/** The chosen bitrate for the audio and video components of the media. This can be specified instead of the above two properties.
* If this is specified along with one or the other of the above properties, this will be used for the one that isn't specified. */
bitsPerSecond?: number;
}
interface MediaRecorderConstructor {
/**
* A reference to the prototype.
*/
readonly prototype: MediaRecorder;
/**
* Creates a new MediaRecorder.
* @param stream Defines the stream to record.
* @param options Defines the options for the recorder available in the type MediaRecorderOptions.
*/
new (stream: MediaStream, options?: MediaRecorderOptions): MediaRecorder;
}
/**
* MediaRecoreder object available in some browsers.
*/
declare let MediaRecorder: MediaRecorderConstructor;
/**
* This represents the different options available for the video capture.
*/
export interface VideoRecorderOptions {
/** Defines the mime type of the video. */
mimeType: string;
/** Defines the FPS the video should be recorded at. */
fps: number;
/** Defines the chunk size for the recording data. */
recordChunckSize: number;
/** The audio tracks to attach to the recording. */
audioTracks?: MediaStreamTrack[];
}
/**
* This can help with recording videos from BabylonJS.
* This is based on the available WebRTC functionalities of the browser.
*
* @see http://doc.babylonjs.com/how_to/render_scene_on_a_video
*/
export class VideoRecorder {
private static readonly _defaultOptions = {
mimeType: 'video/webm',
fps: 25,
recordChunckSize: 3000,
};
/**
* Returns whether or not the VideoRecorder is available in your browser.
* @param engine Defines the Babylon Engine.
* @returns true if supported otherwise false.
*/
public static IsSupported(canvas: HTMLCanvasElement): boolean {
// Need to cast canvas to any because HTMLCanvasElement doesn't have captureStream yet.
return !!canvas && typeof (canvas as any).captureStream === 'function';
}
private readonly _options: VideoRecorderOptions;
private _canvas: Nullable<HTMLCanvasElement>;
private _mediaRecorder: Nullable<MediaRecorder>;
private _recordedChunks: any[] = [];
private _fileName: Nullable<string> = null;
private _resolve: Nullable<(blob: Blob) => void> = null;
private _reject: Nullable<(error: any) => void> = null;
/**
* True when a recording is already in progress.
*/
public get isRecording(): boolean {
return !!this._canvas && (this._canvas as any).isRecording;
}
/**
* Create a new VideoCapture object which can help converting what you see in Babylon to a video file.
* @param engine Defines the BabylonJS Engine you wish to record.
* @param options Defines options that can be used to customize the capture.
*/
constructor(canvas: HTMLCanvasElement, options: Nullable<VideoRecorderOptions> = null) {
if (!VideoRecorder.IsSupported(canvas)) {
throw new Error('Your browser does not support recording so far.');
}
if (!canvas) {
throw new Error('The babylon engine must have a canvas to be recorded');
}
this._canvas = canvas;
(this._canvas as any).isRecording = false;
this._options = {
...VideoRecorder._defaultOptions,
...options,
};
const stream = (this._canvas as any).captureStream(this._options.fps);
if (this._options.audioTracks) {
for (const track of this._options.audioTracks) {
stream.addTrack(track);
}
}
this._mediaRecorder = new MediaRecorder(stream, { mimeType: this._options.mimeType });
this._mediaRecorder.ondataavailable = this._handleDataAvailable.bind(this);
this._mediaRecorder.onerror = this._handleError.bind(this);
this._mediaRecorder.onstop = this._handleStop.bind(this);
}
/**
* Stops the current recording before the default capture timeout passed in the startRecording function.
*/
public stopRecording(): void {
if (!this._canvas || !this._mediaRecorder) {
return;
}
if (!this.isRecording) {
return;
}
(this._canvas as any).isRecording = false;
this._mediaRecorder.stop();
}
/**
* Starts recording the canvas for a max duration specified in parameters.
* @param fileName Defines the name of the file to be downloaded when the recording stop.
* If null no automatic download will start and you can rely on the promise to get the data back.
* @param maxDuration Defines the maximum recording time in seconds.
* It defaults to 7 seconds. A value of zero will not stop automatically, you would need to call stopRecording manually.
* @return A promise callback at the end of the recording with the video data in Blob.
*/
public startRecording(fileName: Nullable<string> = 'babylonjs.webm', maxDuration = 7): Promise<Blob> {
if (!this._canvas || !this._mediaRecorder) {
throw new Error('Recorder has already been disposed');
}
if (this.isRecording) {
throw new Error('Recording already in progress');
}
if (maxDuration > 0) {
setTimeout(() => {
this.stopRecording();
}, maxDuration * 1000);
}
this._fileName = fileName;
this._recordedChunks = [];
this._resolve = null;
this._reject = null;
(this._canvas as any).isRecording = true;
this._mediaRecorder.start(this._options.recordChunckSize);
return new Promise<Blob>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
}
/**
* Releases internal resources used during the recording.
*/
public dispose() {
this._canvas = null;
this._mediaRecorder = null;
this._recordedChunks = [];
this._fileName = null;
this._resolve = null;
this._reject = null;
}
private _handleDataAvailable(event: any): void {
if (event.data.size > 0) {
this._recordedChunks.push(event.data);
}
}
private _handleError(event: ErrorEvent): void {
this.stopRecording();
if (this._reject) {
this._reject(event.error);
} else {
// eslint-disable-next-line new-cap
throw new event.error();
}
}
private _handleStop(): void {
this.stopRecording();
const superBuffer = new Blob(this._recordedChunks);
if (this._resolve) {
this._resolve(superBuffer);
}
window.URL.createObjectURL(superBuffer);
if (this._fileName) {
download(superBuffer, this._fileName);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment