Home Reference Source

src/media/StreamRecorder.js

import {EventEmitter} from '../eventemitter';
import {StreamRecorderStatus} from './StreamRecorderStatus';

/**
 * @typedef {Object} StopEvent
 * @property {Blob} data A Blob with the recorded data. For more information about
 * how to process a Blob you can check
 * [Sending and receiving Binary Data](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data)
 * or [FileSaver.js](https://github.com/eligrey/FileSaver.js/)
 * @property {number} startTime Timestamp when the recording was started
 * @property {number} stopTime Timestamp when the recording was stoped
 * @property {StreamRecorder} target The StreamRecorder object
 */

/**
 * This class allows to record a stream. Instances of this class are normally obtained using
 * {@link Call#createLocalStreamRecorders} or {@link Call#createRemoteStreamRecorders}
 *
 * Note that this class uses internally a
 * [MediaRecorder](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder).
 * According to its [specification](https://w3c.github.io/mediacapture-record/MediaRecorder.html),
 * there is not a list of supported container formats, audio codecs or video codecs that must
 * be supported for every browser and the default value for them is platform-specific.
 *
 * ## Events
 *
 * This class contains an instance of {EventEmitter} that emits the next events:
 *
 * - **`start`** is emitted after the recording is started.
 * - **`pause`** is emitted every time the recording is paused.
 * - **`resume`** is emitted every time the recording is resumed.
 * - **`stop`** is emitted after the recording is stoped. It contains a {@link StopEvent}
 *
 * You can check EventEmitter2 documentation for additional information about
 * adding and removing listeners for this events events.
 *
 * @see https://github.com/asyncly/EventEmitter2#api
 *
 * @example <caption>Basic usage</caption>
 * let streamRecorder = call.remoteStreams[0].createStreamRecorder();
 * streamRecorder.start();
 * streamRecorder.emitter.on('stop', (data) => {
 *   console.log(data);
 *   // Here you can download the data, upload to a server..
 * });
 * streamRecorder.stop();
 */
export class StreamRecorder {
	/**
	 * @param {MediaStream} mediaStream The media stream that is going to be recorded
	 */
	constructor(mediaStream) {
		/**
		 * @private
		 * @type {MediaStream}
		 */
		this._mediaStream = mediaStream;

		/**
		 * @private
		 * @type {MediaRecorder}
		 */
		this._mediaRecorder = null;

		/**
		 * @private
		 * @type {Blob[]}
		 */
		this._chunks = [];

		/**
		 * @private
		 * @type {Status}
		 */
		this._status = StreamRecorderStatus.INACTIVE;

		/**
		 * @type {EventEmitter}
		 */
		this.emitter = new EventEmitter();

		/**
		 * Indicates if the audio of the call must be recorded
		 * @type {boolean}
		 */
		this.isAudioRecordingEnabled = true;

		/**
 		 * Indicates if the video of the call must be recorded
 		 * @type {boolean}
 		 */
		this.isVideoRecordingEnabled = true;
	}

	/**
	 * @type {StreamRecorderStatus}
	 */
	get status() {
		return this._status;
	}

	/**
	 * Starts the recording
	 * @returns {undefined}
	 */
	start() {
		if (this._mediaRecorder !== null) {
			throw Error('The StreamRecorder object must be stopped before starting it again');
		}
		let mediaStream;
		switch (true) {
			case this.isAudioRecordingEnabled && this.isVideoRecordingEnabled:
				mediaStream = this._mediaStream;
				break;
			case this.isAudioRecordingEnabled && !this.isVideoRecordingEnabled:
				mediaStream = window.MediaStream(this._mediaStream.getAudioTracks());
				break;
			case !this.isAudioRecordingEnabled && this.isVideoRecordingEnabled:
				mediaStream = window.MediaStream(this._mediaStream.getVideoTracks());
				break;
			default:
				throw Error('At least audio or video recording must be enabled');
		}
		this._mediaRecorder = new window.MediaRecorder(mediaStream);

		this._mediaRecorder.onstart = () => {
			this._status = StreamRecorderStatus.RECORDING;
			this._recordingStartTime = Date.now();
			this.emitter.emit('start');
		};
		this._mediaRecorder.onstop = () => {
			this._status = StreamRecorderStatus.INACTIVE;
			this.emitter.emit('stop', {
				data: new Blob(this._chunks, {
					type: this._chunks[0].type,
				}),
				startTime: this._recordingStartTime,
				endTime: Date.now(),
				target: this,
			});
			this._chunks = [];
		};
		this._mediaRecorder.ondataavailable = (event) => {
			this._chunks.push(event.data);
		};
		this._mediaRecorder.onpause = () => {
			this._status = StreamRecorderStatus.PAUSED;
			this.emitter.emit('pause');
		};
		this._mediaRecorder.onresume = () => {
			this._status = StreamRecorderStatus.RECORDING;
			this.emitter.emit('resume');
		};
		this._mediaRecorder.onerror = (event) => {
			this.emitter.emit('error', event);
		};

		this._mediaRecorder.start();
	}

	/**
	 * Stops the recording
	 *
	 * This method can only be called after the {@link MediaStream#start} method has between
	 * called. The stop event is received after calling this method with the recorded data.
	 * @returns {undefined}
	 */
	stop() {
		if (this._mediaRecorder === null) {
			throw Error('The StreamRecorder object must be started before');
		}
		this._mediaRecorder.stop();
		this._mediaRecorder = null;
	}

	/**
	 * Pauses the recording
	 * @returns {undefined}
	 */
	pause() {
		if (this._mediaRecorder === null) {
			throw Error('The StreamRecorder object must be started before');
		}
		this._mediaRecorder.pause();
	}

	/**
	 * Resumes the recording
	 * @returns {undefined}
	 */
	resume() {
		if (this._mediaRecorder === null) {
			throw Error('The StreamRecorder object must be started before');
		}
		this._mediaRecorder.resume();
	}

}