videoRecorder.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import { Nullable } from "../types";
  2. import { Tools } from "./tools";
  3. import { Engine } from "../Engines/engine";
  4. interface MediaRecorder {
  5. /** Starts recording */
  6. start(timeSlice: number): void;
  7. /** Stops recording */
  8. stop(): void;
  9. /** Event raised when an error arised. */
  10. onerror: (event: ErrorEvent) => void;
  11. /** Event raised when the recording stops. */
  12. onstop: (event: Event) => void;
  13. /** Event raised when a new chunk of data is available and should be tracked. */
  14. ondataavailable: (event: Event) => void;
  15. }
  16. interface MediaRecorderOptions {
  17. /** The mime type you want to use as the recording container for the new MediaRecorder. */
  18. mimeType?: string;
  19. /** The chosen bitrate for the audio component of the media. */
  20. audioBitsPerSecond?: number;
  21. /** The chosen bitrate for the video component of the media. */
  22. videoBitsPerSecond?: number;
  23. /** The chosen bitrate for the audio and video components of the media. This can be specified instead of the above two properties.
  24. * 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. */
  25. bitsPerSecond?: number;
  26. }
  27. interface MediaRecorderConstructor {
  28. /**
  29. * A reference to the prototype.
  30. */
  31. readonly prototype: MediaRecorder;
  32. /**
  33. * Creates a new MediaRecorder.
  34. * @param stream Defines the stream to record.
  35. * @param options Defines the options for the recorder available in the type MediaRecorderOptions.
  36. */
  37. new(stream: MediaStream, options?: MediaRecorderOptions): MediaRecorder;
  38. }
  39. /**
  40. * MediaRecoreder object available in some browsers.
  41. */
  42. declare var MediaRecorder: MediaRecorderConstructor;
  43. /**
  44. * This represents the different options available for the video capture.
  45. */
  46. export interface VideoRecorderOptions {
  47. /** Defines the mime type of the video. */
  48. mimeType: string;
  49. /** Defines the FPS the video should be recorded at. */
  50. fps: number;
  51. /** Defines the chunk size for the recording data. */
  52. recordChunckSize: number;
  53. /** The audio tracks to attach to the recording. */
  54. audioTracks?: MediaStreamTrack[];
  55. }
  56. /**
  57. * This can help with recording videos from BabylonJS.
  58. * This is based on the available WebRTC functionalities of the browser.
  59. *
  60. * @see http://doc.babylonjs.com/how_to/render_scene_on_a_video
  61. */
  62. export class VideoRecorder {
  63. private static readonly _defaultOptions = {
  64. mimeType: "video/webm",
  65. fps: 25,
  66. recordChunckSize: 3000
  67. };
  68. /**
  69. * Returns whether or not the VideoRecorder is available in your browser.
  70. * @param engine Defines the Babylon Engine.
  71. * @returns true if supported otherwise false.
  72. */
  73. public static IsSupported(engine: Engine): boolean {
  74. const canvas = engine.getRenderingCanvas();
  75. return (!!canvas && typeof (<any>canvas).captureStream === "function");
  76. }
  77. private readonly _options: VideoRecorderOptions;
  78. private _canvas: Nullable<HTMLCanvasElement>;
  79. private _mediaRecorder: Nullable<MediaRecorder>;
  80. private _recordedChunks: any[];
  81. private _fileName: Nullable<string>;
  82. private _resolve: Nullable<(blob: Blob) => void>;
  83. private _reject: Nullable<(error: any) => void>;
  84. /**
  85. * True when a recording is already in progress.
  86. */
  87. public get isRecording(): boolean {
  88. return !!this._canvas && this._canvas.isRecording;
  89. }
  90. /**
  91. * Create a new VideoCapture object which can help converting what you see in Babylon to a video file.
  92. * @param engine Defines the BabylonJS Engine you wish to record.
  93. * @param options Defines options that can be used to customize the capture.
  94. */
  95. constructor(engine: Engine, options: Nullable<VideoRecorderOptions> = null) {
  96. if (!VideoRecorder.IsSupported(engine)) {
  97. throw "Your browser does not support recording so far.";
  98. }
  99. const canvas = engine.getRenderingCanvas();
  100. if (!canvas) {
  101. throw "The babylon engine must have a canvas to be recorded";
  102. }
  103. this._canvas = canvas;
  104. this._canvas.isRecording = false;
  105. this._options = {
  106. ...VideoRecorder._defaultOptions,
  107. ...options
  108. };
  109. const stream = this._canvas.captureStream(this._options.fps);
  110. if (this._options.audioTracks) {
  111. for (let track of this._options.audioTracks) {
  112. stream.addTrack(track);
  113. }
  114. }
  115. this._mediaRecorder = new MediaRecorder(stream, { mimeType: this._options.mimeType });
  116. this._mediaRecorder.ondataavailable = this._handleDataAvailable.bind(this);
  117. this._mediaRecorder.onerror = this._handleError.bind(this);
  118. this._mediaRecorder.onstop = this._handleStop.bind(this);
  119. }
  120. /**
  121. * Stops the current recording before the default capture timeout passed in the startRecording function.
  122. */
  123. public stopRecording(): void {
  124. if (!this._canvas || !this._mediaRecorder) {
  125. return;
  126. }
  127. if (!this.isRecording) {
  128. return;
  129. }
  130. this._canvas.isRecording = false;
  131. this._mediaRecorder.stop();
  132. }
  133. /**
  134. * Starts recording the canvas for a max duration specified in parameters.
  135. * @param fileName Defines the name of the file to be downloaded when the recording stop.
  136. * If null no automatic download will start and you can rely on the promise to get the data back.
  137. * @param maxDuration Defines the maximum recording time in seconds.
  138. * It defaults to 7 seconds. A value of zero will not stop automatically, you would need to call stopRecording manually.
  139. * @return A promise callback at the end of the recording with the video data in Blob.
  140. */
  141. public startRecording(fileName: Nullable<string> = "babylonjs.webm", maxDuration = 7): Promise<Blob> {
  142. if (!this._canvas || !this._mediaRecorder) {
  143. throw "Recorder has already been disposed";
  144. }
  145. if (this.isRecording) {
  146. throw "Recording already in progress";
  147. }
  148. if (maxDuration > 0) {
  149. setTimeout(() => {
  150. this.stopRecording();
  151. }, maxDuration * 1000);
  152. }
  153. this._fileName = fileName;
  154. this._recordedChunks = [];
  155. this._resolve = null;
  156. this._reject = null;
  157. this._canvas.isRecording = true;
  158. this._mediaRecorder.start(this._options.recordChunckSize);
  159. return new Promise<Blob>((resolve, reject) => {
  160. this._resolve = resolve;
  161. this._reject = reject;
  162. });
  163. }
  164. /**
  165. * Releases internal resources used during the recording.
  166. */
  167. public dispose() {
  168. this._canvas = null;
  169. this._mediaRecorder = null;
  170. this._recordedChunks = [];
  171. this._fileName = null;
  172. this._resolve = null;
  173. this._reject = null;
  174. }
  175. private _handleDataAvailable(event: any): void {
  176. if (event.data.size > 0) {
  177. this._recordedChunks.push(event.data);
  178. }
  179. }
  180. private _handleError(event: ErrorEvent): void {
  181. this.stopRecording();
  182. if (this._reject) {
  183. this._reject(event.error);
  184. }
  185. else {
  186. throw new event.error();
  187. }
  188. }
  189. private _handleStop(): void {
  190. this.stopRecording();
  191. const superBuffer = new Blob(this._recordedChunks);
  192. if (this._resolve) {
  193. this._resolve(superBuffer);
  194. }
  195. window.URL.createObjectURL(superBuffer);
  196. if (this._fileName) {
  197. Tools.Download(superBuffer, this._fileName);
  198. }
  199. }
  200. }