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. 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. */
  24. bitsPerSecond?: number;
  25. }
  26. interface MediaRecorderConstructor {
  27. /**
  28. * A reference to the prototype.
  29. */
  30. readonly prototype: MediaRecorder;
  31. /**
  32. * Creates a new MediaRecorder.
  33. * @param stream Defines the stream to record.
  34. * @param options Defines the options for the recorder available in the type MediaRecorderOptions.
  35. */
  36. new(stream: MediaStream, options?: MediaRecorderOptions): MediaRecorder;
  37. }
  38. /**
  39. * MediaRecoreder object available in some browsers.
  40. */
  41. declare var MediaRecorder: MediaRecorderConstructor;
  42. /**
  43. * This represents the different options avilable for the video capture.
  44. */
  45. export interface VideoRecorderOptions {
  46. /** Defines the mime type of the video */
  47. mimeType: string;
  48. /** Defines the video the video should be recorded at */
  49. fps: number;
  50. /** Defines the chunk size for the recording data */
  51. recordChunckSize: number;
  52. /** The audio tracks to attach to the record */
  53. audioTracks?: MediaStreamTrack[];
  54. }
  55. /**
  56. * This can helps recording videos from BabylonJS.
  57. * This is based on the available WebRTC functionalities of the browser.
  58. *
  59. * @see http://doc.babylonjs.com/how_to/render_scene_on_a_video
  60. */
  61. export class VideoRecorder {
  62. private static readonly _defaultOptions = {
  63. mimeType: "video/webm",
  64. fps: 25,
  65. recordChunckSize: 3000
  66. };
  67. /**
  68. * Returns wehther or not the VideoRecorder is available in your browser.
  69. * @param engine Defines the Babylon Engine to check the support for
  70. * @returns true if supported otherwise false
  71. */
  72. public static IsSupported(engine: Engine): boolean {
  73. const canvas = engine.getRenderingCanvas();
  74. return (!!canvas && typeof (<any>canvas).captureStream === "function");
  75. }
  76. private readonly _options: VideoRecorderOptions;
  77. private _canvas: Nullable<HTMLCanvasElement>;
  78. private _mediaRecorder: Nullable<MediaRecorder>;
  79. private _recordedChunks: any[];
  80. private _fileName: Nullable<string>;
  81. private _resolve: Nullable<(blob: Blob) => void>;
  82. private _reject: Nullable<(error: any) => void>;
  83. /**
  84. * True wether a recording is already in progress.
  85. */
  86. public get isRecording(): boolean {
  87. return !!this._canvas && this._canvas.isRecording;
  88. }
  89. /**
  90. * Create a new VideoCapture object which can help converting what you see in Babylon to
  91. * a video file.
  92. * @param engine Defines the BabylonJS Engine you wish to record
  93. * @param options Defines options that can be used to customized 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
  122. * functions.
  123. */
  124. public stopRecording(): void {
  125. if (!this._canvas || !this._mediaRecorder) {
  126. return;
  127. }
  128. if (!this.isRecording) {
  129. return;
  130. }
  131. this._canvas.isRecording = false;
  132. this._mediaRecorder.stop();
  133. }
  134. /**
  135. * Starts recording the canvas for a max duration specified in parameters.
  136. * @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.
  137. * @param maxDuration Defines the maximum recording time in seconds.
  138. * It default 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. }