UNPKG

twilio-video

Version:

Twilio Video JavaScript Library

396 lines (355 loc) 15.1 kB
'use strict'; const { isIOS } = require('../../util/browserdetection'); const detectSilentVideo = require('../../util/detectsilentvideo'); const mixinLocalMediaTrack = require('./localmediatrack'); const VideoTrack = require('./videotrack'); const LocalMediaVideoTrack = mixinLocalMediaTrack(VideoTrack); /** * A {@link LocalVideoTrack} is a {@link VideoTrack} representing video that * your {@link LocalParticipant} can publish to a {@link Room}. It can be * enabled and disabled with {@link LocalVideoTrack#enable} and * {@link LocalVideoTrack#disable} or stopped completely with * {@link LocalVideoTrack#stop}. * @extends VideoTrack * @property {Track.ID} id - The {@link LocalVideoTrack}'s ID * @property {boolean} isMuted - Whether or not the video source has stopped sending frames to the * {@link LocalVideoTrack}; This can happen when the camera is taken over by another application, * mainly on mobile devices; When this property toggles, then <code>muted</code> and <code>unmuted</code> * events are fired appropriately * @property {boolean} isStopped - Whether or not the {@link LocalVideoTrack} is * stopped * @emits LocalVideoTrack#disabled * @emits LocalVideoTrack#enabled * @emits LocalVideoTrack#muted * @emits LocalVideoTrack#started * @emits LocalVideoTrack#stopped * @emits LocalVideoTrack#unmuted */ class LocalVideoTrack extends LocalMediaVideoTrack { /** * Construct a {@link LocalVideoTrack} from a MediaStreamTrack. * @param {MediaStreamTrack} mediaStreamTrack - The underlying MediaStreamTrack * @param {LocalTrackOptions} [options] - {@link LocalTrack} options */ constructor(mediaStreamTrack, options) { options = Object.assign({ workaroundSilentLocalVideo: isIOS() && typeof document !== 'undefined' && typeof document.createElement === 'function' }, options); super(mediaStreamTrack, options); Object.defineProperties(this, { _workaroundSilentLocalVideo: { value: options.workaroundSilentLocalVideo ? workaroundSilentLocalVideo : null }, _workaroundSilentLocalVideoCleanup: { value: null, writable: true } }); // NOTE(mmalavalli): In iOS Safari, we work around a bug where local video // MediaStreamTracks are silent (even though they are enabled, live and unmuted) // after accepting/rejecting a phone call. if (this._workaroundSilentLocalVideo) { this._workaroundSilentLocalVideoCleanup = this._workaroundSilentLocalVideo(this, document); } } toString() { return `[LocalVideoTrack #${this._instanceId}: ${this.id}]`; } /** * @private */ _checkIfCanCaptureFrames() { return super._checkIfCanCaptureFrames.call(this, this._trackSender.isPublishing); } /** * @private */ _end() { return super._end.apply(this, arguments); } /** * @private */ _setSenderMediaStreamTrack(useProcessed) { const unprocessedTrack = this.mediaStreamTrack; const mediaStreamTrack = useProcessed ? this.processedTrack : unprocessedTrack; return this._trackSender.setMediaStreamTrack(mediaStreamTrack) .catch(error => this._log.warn( 'setMediaStreamTrack failed on LocalVideoTrack RTCRtpSender', { error, mediaStreamTrack })) .then(() => { this._unprocessedTrack = useProcessed ? unprocessedTrack : null; }); } /** * Add a {@link VideoProcessor} to allow for custom processing of video frames belonging to a VideoTrack. * @param {VideoProcessor} processor - The {@link VideoProcessor} to use. * @param {AddProcessorOptions} [options] - {@link AddProcessorOptions} to provide. * @returns {this} * @example * class GrayScaleProcessor { * constructor(percentage) { * this.percentage = percentage; * } * processFrame(inputFrameBuffer, outputFrameBuffer) { * const context = outputFrameBuffer.getContext('2d'); * context.filter = `grayscale(${this.percentage}%)`; * context.drawImage(inputFrameBuffer, 0, 0, inputFrameBuffer.width, inputFrameBuffer.height); * } * } * * const localVideoTrack = Array.from(room.localParticipant.videoTracks.values())[0].track; * localVideoTrack.addProcessor(new GrayScaleProcessor(100)); */ addProcessor() { this._log.debug('Adding VideoProcessor to the LocalVideoTrack'); const result = super.addProcessor.apply(this, arguments); if (!this.processedTrack) { return this._log.warn('Unable to add a VideoProcessor to the LocalVideoTrack'); } this._log.debug('Updating LocalVideoTrack\'s MediaStreamTrack with the processed MediaStreamTrack', this.processedTrack); this._setSenderMediaStreamTrack(true); return result; } /** * Remove the previously added {@link VideoProcessor} using `addProcessor` API. * @param {VideoProcessor} processor - The {@link VideoProcessor} to remove. * @returns {this} * @example * class GrayScaleProcessor { * constructor(percentage) { * this.percentage = percentage; * } * processFrame(inputFrameBuffer, outputFrameBuffer) { * const context = outputFrameBuffer.getContext('2d'); * context.filter = `grayscale(${this.percentage}%)`; * context.drawImage(inputFrameBuffer, 0, 0, inputFrameBuffer.width, inputFrameBuffer.height); * } * } * * const localVideoTrack = Array.from(room.localParticipant.videoTracks.values())[0].track; * const grayScaleProcessor = new GrayScaleProcessor(100); * localVideoTrack.addProcessor(grayScaleProcessor); * * document.getElementById('remove-button').onclick = () => localVideoTrack.removeProcessor(grayScaleProcessor); */ removeProcessor() { this._log.debug('Removing VideoProcessor from the LocalVideoTrack'); const result = super.removeProcessor.apply(this, arguments); this._log.debug('Updating LocalVideoTrack\'s MediaStreamTrack with the original MediaStreamTrack'); this._setSenderMediaStreamTrack() .then(() => this._updateElementsMediaStreamTrack()); return result; } /** * Disable the {@link LocalVideoTrack}. This is equivalent to pausing a video source. * If a {@link VideoProcessor} is added, then <code>processedTrack</code> is also disabled. * @returns {this} * @fires VideoTrack#disabled */ disable() { const result = super.disable.apply(this, arguments); if (this.processedTrack) { this.processedTrack.enabled = false; } return result; } /** * Enable the {@link LocalVideoTrack}. This is equivalent to unpausing the video source. * If a {@link VideoProcessor} is added, then <code>processedTrack</code> is also enabled. * @returns {this} * @fires VideoTrack#enabled *//** * Enable or disable the {@link LocalVideoTrack}. This is equivalent to unpausing or pausing * the video source respectively. If a {@link VideoProcessor} is added, then <code>processedTrack</code> * is also enabled or disabled. * @param {boolean} [enabled] - Specify false to disable the * {@link LocalVideoTrack} * @returns {this} * @fires VideoTrack#disabled * @fires VideoTrack#enabled */ enable(enabled = true) { const result = super.enable.apply(this, arguments); if (this.processedTrack) { this.processedTrack.enabled = enabled; if (enabled) { this._captureFrames(); this._log.debug('Updating LocalVideoTrack\'s MediaStreamTrack with the processed MediaStreamTrack', this.processedTrack); this._setSenderMediaStreamTrack(true); } } return result; } /** * Restart the {@link LocalVideoTrack}. This stops the existing MediaStreamTrack * and creates a new MediaStreamTrack. If the {@link LocalVideoTrack} is being published * to a {@link Room}, then all the {@link RemoteParticipant}s will start receiving media * from the newly created MediaStreamTrack. You can access the new MediaStreamTrack via * the <code>mediaStreamTrack</code> property. If you want to listen to events on * the MediaStreamTrack directly, please do so in the "started" event handler. Also, * the {@link LocalVideoTrack}'s ID is no longer guaranteed to be the same as the * underlying MediaStreamTrack's ID. * @param {MediaTrackConstraints} [constraints] - The optional <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints" target="_blank">MediaTrackConstraints</a> * for restarting the {@link LocalVideoTrack}; If not specified, then the current MediaTrackConstraints * will be used; If <code>{}</code> (empty object) is specified, then the default MediaTrackConstraints * will be used * @returns {Promise<void>} Rejects with a TypeError if the {@link LocalVideoTrack} was not created * using an one of <code>createLocalVideoTrack</code>, <code>createLocalTracks</code> or <code>connect</code>; * Also rejects with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Exceptions" target="_blank">DOMException</a> * raised by <code>getUserMedia</code> when it fails * @fires LocalVideoTrack#stopped * @fires LocalVideoTrack#started * @example * const { connect, createLocalVideoTrack } = require('twilio-video'); * * // Create a LocalVideoTrack that captures video from the front-facing camera. * createLocalVideoTrack({ facingMode: 'user' }).then(function(localVideoTrack) { * return connect('token', { * name: 'my-cool-room', * tracks: [localVideoTrack] * }); * }).then(function(room) { * // Restart the LocalVideoTrack to capture video from the back-facing camera. * const localVideoTrack = Array.from(room.localParticipant.videoTracks.values())[0].track; * return localVideoTrack.restart({ facingMode: 'environment' }); * }); */ restart() { if (this._workaroundSilentLocalVideoCleanup) { this._workaroundSilentLocalVideoCleanup(); this._workaroundSilentLocalVideoCleanup = null; } const promise = super.restart.apply(this, arguments); if (this.processor) { promise.then(() => { this._restartProcessor(); }); } if (this._workaroundSilentLocalVideo) { promise.finally(() => { this._workaroundSilentLocalVideoCleanup = this._workaroundSilentLocalVideo(this, document); }); } return promise; } /** * Calls stop on the underlying MediaStreamTrack. If you choose to stop a * {@link LocalVideoTrack}, you should unpublish it after stopping. * @returns {this} * @fires LocalVideoTrack#stopped */ stop() { if (this._workaroundSilentLocalVideoCleanup) { this._workaroundSilentLocalVideoCleanup(); this._workaroundSilentLocalVideoCleanup = null; } return super.stop.apply(this, arguments); } } /** * Work around a bug where local video MediaStreamTracks are silent (even though * they are enabled, live and unmuted) after accepting/rejecting a phone call. * @private * @param {LocalVideoTrack} localVideoTrack * @param {HTMLDocument} doc * @returns {function} Cleans up listeners attached by the workaround */ function workaroundSilentLocalVideo(localVideoTrack, doc) { const { _log: log } = localVideoTrack; let { _dummyEl: el, mediaStreamTrack } = localVideoTrack; function onUnmute() { if (!localVideoTrack.isEnabled) { return; } log.info('Unmuted, checking silence'); // The dummy element is paused, so play it and then detect silence. el.play().then(() => detectSilentVideo(el, doc)).then(isSilent => { if (!isSilent) { log.info('Non-silent frames detected, so no need to restart'); return; } log.warn('Silence detected, restarting'); // NOTE(mmalavalli): If we try and restart a silent MediaStreamTrack // without stopping it first, then a NotReadableError is raised. Hence, // we stop the MediaStreamTrack here. localVideoTrack._stop(); // Restart the LocalVideoTrack. // eslint-disable-next-line consistent-return return localVideoTrack._restart(); }).catch(error => { log.warn('Failed to detect silence and restart:', error); }).finally(() => { // If silent frames were not detected, then pause the dummy element again, // if there is no processed track. el = localVideoTrack._dummyEl; if (el && !el.paused && !localVideoTrack.processedTrack) { el.pause(); } // Reset the unmute handler. mediaStreamTrack.removeEventListener('unmute', onUnmute); mediaStreamTrack = localVideoTrack.mediaStreamTrack; if (mediaStreamTrack.addEventListener) { mediaStreamTrack.addEventListener('unmute', onUnmute); } else { mediaStreamTrack.onunmute = onUnmute; } }); } // Set the unmute handler. if (mediaStreamTrack.addEventListener) { mediaStreamTrack.addEventListener('unmute', onUnmute); } else { mediaStreamTrack.onunmute = onUnmute; } return () => { if (mediaStreamTrack.removeEventListener) { mediaStreamTrack.removeEventListener('unmute', onUnmute); } else { mediaStreamTrack.onunmute = null; } }; } /** * The {@link LocalVideoTrack} was disabled, i.e. the video source was paused by the user. * @param {LocalVideoTrack} track - The {@link LocalVideoTrack} that was * disabled * @event LocalVideoTrack#disabled */ /** * The {@link LocalVideoTrack} was enabled, i.e. the video source was unpaused by the user. * @param {LocalVideoTrack} track - The {@link LocalVideoTrack} that was enabled * @event LocalVideoTrack#enabled */ /** * The {@link LocalVideoTrack} was muted because the video source stopped sending frames, most * likely due to another application taking said video source, especially on mobile devices. * @param {LocalVideoTrack} track - The {@link LocalVideoTrack} that was muted * @event LocalVideoTrack#muted */ /** * The {@link LocalVideoTrack} started. This means there is enough video data * to begin playback. * @param {LocalVideoTrack} track - The {@link LocalVideoTrack} that started * @event LocalVideoTrack#started */ /** * The {@link LocalVideoTrack} stopped, either because {@link LocalVideoTrack#stop} * or {@link LocalVideoTrack#restart} was called or because the underlying * MediaStreamTrack ended. * @param {LocalVideoTrack} track - The {@link LocalVideoTrack} that stopped * @event LocalVideoTrack#stopped */ /** * The {@link LocalVideoTrack} was unmuted because the video source resumed sending frames, * most likely due to the application that took over the said video source has released it * back to the application, especially on mobile devices. This event is also fired when * {@link LocalVideoTrack#restart} is called on a muted {@link LocalVideoTrack} with a * new video source. * @param {LocalVideoTrack} track - The {@link LocalVideoTrack} that was unmuted * @event LocalVideoTrack#unmuted */ module.exports = LocalVideoTrack;