diff options
author | Daniel Calviño Sánchez <danxuliu@gmail.com> | 2022-07-30 14:15:12 +0300 |
---|---|---|
committer | backportbot-nextcloud[bot] <backportbot-nextcloud[bot]@users.noreply.github.com> | 2022-08-11 11:42:34 +0300 |
commit | d68799b9cbae8f9ddd4077ede4f8ecb64dd26e16 (patch) | |
tree | 3115846df0a2a58cb14d71d876b181c93ac197b2 | |
parent | 336736c9fb15676ebd94177bf92cd202c6a52d2e (diff) |
Enforce zero-information-content video tracks
This ensures that other participants will receive a black video track
when no visible video should be sent, even if the browser does not
properly update a previously sent track when disabled or stopped.
Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
-rw-r--r-- | src/utils/media/pipeline/BlackVideoEnforcer.js | 174 | ||||
-rw-r--r-- | src/utils/media/pipeline/BlackVideoEnforcer.spec.js | 1445 | ||||
-rw-r--r-- | src/utils/webrtc/simplewebrtc/localmedia.js | 7 |
3 files changed, 1625 insertions, 1 deletions
diff --git a/src/utils/media/pipeline/BlackVideoEnforcer.js b/src/utils/media/pipeline/BlackVideoEnforcer.js new file mode 100644 index 000000000..529fc4420 --- /dev/null +++ b/src/utils/media/pipeline/BlackVideoEnforcer.js @@ -0,0 +1,174 @@ +/** + * + * @copyright Copyright (c) 2022, Daniel Calviño Sánchez (danxuliu@gmail.com) + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import TrackSinkSource from './TrackSinkSource.js' + +/** + * Processor node to enforce zero-information-content on a disabled or ended + * video track. + * + * A single input track slot with the default id is accepted. A single output + * track slot with the default id is provided. The input track must be a video + * track. The output track will be a video track. + * + * When the input track is enabled it is just bypassed to the output. However, + * if the input track is disabled or stopped a black video track is generated + * and set as the output instead; the black video track will be initially + * enabled and later automatically disabled, unless anything changes in the + * input and causes a different output track, even another black video track, to + * be set (a previous black video track is not reused, a new one is always + * generated). If the input track is removed the black video will be initially + * set as the output too, but then it will be also removed instead of disabled. + * If the input track is removed when the output is already a black video track + * a new black video track will not be set, the current one will be removed as + * soon as it would have been disabled. + * + * -------------------- + * | | + * ---> | BlackVideoEnforcer | ---> + * | | + * -------------------- + */ +export default class BlackVideoEnforcer extends TrackSinkSource { + + constructor() { + super() + + this._addInputTrackSlot() + this._addOutputTrackSlot() + } + + _handleInputTrack(trackId, newTrack, oldTrack) { + if (!newTrack && !oldTrack) { + return + } + + if (oldTrack && this._startBlackVideoWhenTrackEndedHandler) { + oldTrack.removeEventListener('ended', this._startBlackVideoWhenTrackEndedHandler) + this._startBlackVideoWhenTrackEndedHandler = null + } + + if (!newTrack && this._disableOrRemoveOutputTrackTimeout) { + return + } + + if (!newTrack && this._outputStream) { + this._stopBlackVideo() + this._setOutputTrack('default', null) + + return + } + + if (newTrack) { + this._disableRemoveTrackWhenEnded(newTrack) + + this._startBlackVideoWhenTrackEndedHandler = () => { + this._startBlackVideo(newTrack.getSettings()) + } + newTrack.addEventListener('ended', this._startBlackVideoWhenTrackEndedHandler) + } + + this._stopBlackVideo() + + if (newTrack && newTrack.enabled) { + this._setOutputTrack('default', this.getInputTrack()) + + return + } + + const trackSettings = newTrack ? newTrack.getSettings() : oldTrack?.getSettings() + this._startBlackVideo(trackSettings) + } + + _handleInputTrackEnabled(trackId, enabled) { + // Same enabled state as before, nothing to do + if ((enabled && !this._outputStream) + || (!enabled && this._outputStream)) { + return + } + + if (enabled) { + this._stopBlackVideo() + + this._setOutputTrack('default', this.getInputTrack()) + + return + } + + if (this._outputStream) { + this._setOutputTrackEnabled('default', false) + + return + } + + this._startBlackVideo(this.getInputTrack().getSettings()) + } + + _startBlackVideo(trackSettings) { + if (this._outputStream) { + return + } + + const { width, height } = trackSettings ?? { width: 640, height: 480 } + + const outputCanvasElement = document.createElement('canvas') + outputCanvasElement.width = parseInt(width, 10) + outputCanvasElement.height = parseInt(height, 10) + const outputCanvasContext = outputCanvasElement.getContext('2d') + + this._outputStream = outputCanvasElement.captureStream() + + outputCanvasContext.fillStyle = 'black' + outputCanvasContext.fillRect(0, 0, outputCanvasElement.width, outputCanvasElement.height) + + this._setOutputTrack('default', this._outputStream.getVideoTracks()[0]) + + this._disableOrRemoveOutputTrackTimeout = setTimeout(() => { + clearTimeout(this._disableOrRemoveOutputTrackTimeout) + this._disableOrRemoveOutputTrackTimeout = null + + if (this.getInputTrack()) { + this._setOutputTrackEnabled('default', false) + } else { + this._stopBlackVideo() + this._setOutputTrack('default', null) + } + }, 1000) + } + + _stopBlackVideo() { + if (!this._outputStream) { + return + } + + clearTimeout(this._disableOrRemoveOutputTrackTimeout) + this._disableOrRemoveOutputTrackTimeout = null + + this._outputStream.getTracks().forEach(track => { + this._disableRemoveTrackWhenEnded(track) + + track.stop() + }) + + this._outputStream = null + } + +} diff --git a/src/utils/media/pipeline/BlackVideoEnforcer.spec.js b/src/utils/media/pipeline/BlackVideoEnforcer.spec.js new file mode 100644 index 000000000..1ff891cc0 --- /dev/null +++ b/src/utils/media/pipeline/BlackVideoEnforcer.spec.js @@ -0,0 +1,1445 @@ +/** + * + * @copyright Copyright (c) 2022, Daniel Calviño Sánchez (danxuliu@gmail.com) + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import BlackVideoEnforcer from './BlackVideoEnforcer.js' + +/** + * Helper function to create MediaStreamTrack mocks with just the attributes and + * methods used by BlackVideoEnforcer. + * + * @param {string} id the ID of the track + */ +function newMediaStreamTrackMock(id) { + /** + * MediaStreamTrackMock constructor. + */ + function MediaStreamTrackMock() { + this._endedEventHandlers = [] + this._width = 720 + this._height = 540 + this.id = id + this.enabled = true + this.addEventListener = jest.fn((eventName, eventHandler) => { + if (eventName !== 'ended') { + return + } + + this._endedEventHandlers.push(eventHandler) + }) + this.removeEventListener = jest.fn((eventName, eventHandler) => { + if (eventName !== 'ended') { + return + } + + const index = this._endedEventHandlers.indexOf(eventHandler) + if (index !== -1) { + this._endedEventHandlers.splice(index, 1) + } + }) + this.stop = jest.fn(() => { + for (let i = 0; i < this._endedEventHandlers.length; i++) { + const handler = this._endedEventHandlers[i] + handler.apply(handler) + } + }) + this.getSettings = jest.fn(() => { + return { + width: this._width, + height: this._height, + } + }) + } + return new MediaStreamTrackMock() +} + +describe('BlackVideoEnforcer', () => { + let blackVideoEnforcer + let outputTrackSetHandler + let outputTrackEnabledHandler + let expectedTrackEnabledStateInOutputTrackSetEvent + let blackVideoTrackCount + let blackVideoTracks + + beforeAll(() => { + const originalCreateElement = document.createElement + jest.spyOn(document, 'createElement').mockImplementation((tagName, options) => { + if (tagName !== 'canvas') { + return originalCreateElement(tagName, options) + } + + return new function() { + this.getContext = jest.fn(() => { + return { + fillRect: jest.fn(), + } + }) + this.captureStream = jest.fn(() => { + const blackVideoTrackLocal = newMediaStreamTrackMock('blackVideoTrack' + blackVideoTrackCount) + blackVideoTracks[blackVideoTrackCount] = blackVideoTrackLocal + blackVideoTrackCount++ + + blackVideoTrackLocal._width = this.width + blackVideoTrackLocal._height = this.height + + return { + getVideoTracks: jest.fn(() => { + return [blackVideoTrackLocal] + }), + getTracks: jest.fn(() => { + return [blackVideoTrackLocal] + }), + } + }) + }() + }) + }) + + beforeEach(() => { + jest.useFakeTimers() + + blackVideoTrackCount = 0 + blackVideoTracks = [] + + blackVideoEnforcer = new BlackVideoEnforcer() + + expectedTrackEnabledStateInOutputTrackSetEvent = undefined + + outputTrackSetHandler = jest.fn((blackVideoEnforcer, trackId, track) => { + if (expectedTrackEnabledStateInOutputTrackSetEvent !== undefined) { + expect(track.enabled).toBe(expectedTrackEnabledStateInOutputTrackSetEvent) + } + }) + outputTrackEnabledHandler = jest.fn() + + blackVideoEnforcer.on('outputTrackSet', outputTrackSetHandler) + blackVideoEnforcer.on('outputTrackEnabled', outputTrackEnabledHandler) + }) + + afterEach(() => { + clearTimeout(blackVideoEnforcer._disableOrRemoveOutputTrackTimeout) + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + const DISABLE_OR_REMOVE_TIMEOUT = 1000 + + const STOPPED = true + + /** + * Checks that a black video track has the expected attributes. + * + * @param {number} index the index of the black video track to check. + * @param {number} width the expected width of the black video track. + * @param {number} height the expected height of the black video track. + * @param {boolean} stopped whether the black video track is expected to + * have been stopped already or not. + */ + function assertBlackVideoTrack(index, width, height, stopped = false) { + expect(blackVideoTracks[index].getSettings().width).toBe(width) + expect(blackVideoTracks[index].getSettings().height).toBe(height) + expect(blackVideoTracks[index].stop).toHaveBeenCalledTimes(stopped ? 1 : 0) + } + + describe('set input track', () => { + test('sets input track as its output track when setting enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + }) + + test('sets black video track as its output track when setting disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + }) + }) + + describe('enable/disable input track', () => { + test('sets black video track as its output track if input track is disabled', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrackEnabled('default', false) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + }) + + test('sets input track as its output track if input track is enabled', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack.enabled = true + blackVideoEnforcer._setInputTrackEnabled('default', true) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('sets input track as its output track if input track is later enabled', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack.enabled = true + blackVideoEnforcer._setInputTrackEnabled('default', true) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('does nothing if input track is enabled again', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + blackVideoEnforcer._setInputTrackEnabled('default', true) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + }) + + test('does nothing if input track is disabled again', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + blackVideoEnforcer._setInputTrackEnabled('default', false) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2 - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + }) + }) + + describe('remove input track', () => { + test('sets black video track as its output track and later removes output track when removing enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + blackVideoEnforcer._setInputTrack('default', null) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTracks[0].stop).toHaveBeenCalledTimes(0) + + expectedTrackEnabledStateInOutputTrackSetEvent = undefined + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('sets input track as its output track when setting enabled input track after removing enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + blackVideoEnforcer._setInputTrack('default', null) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack2._width = 320 + inputTrack2._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack2) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('sets black video track as its output track when setting disabled input track after removing enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + blackVideoEnforcer._setInputTrack('default', null) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack2.enabled = false + inputTrack2._width = 320 + inputTrack2._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 320, 180) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 320, 180) + }) + + test('removes output track when removing disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + blackVideoEnforcer._setInputTrack('default', null) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2 - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTracks[0].stop).toHaveBeenCalledTimes(0) + + expectedTrackEnabledStateInOutputTrackSetEvent = undefined + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('removes output track when later removing disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + blackVideoEnforcer._setInputTrack('default', null) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('does nothing when removing null track', () => { + blackVideoEnforcer._setInputTrack('default', null) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + }) + + test('does nothing when removing null track again after removing enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + blackVideoEnforcer._setInputTrack('default', null) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + blackVideoEnforcer._setInputTrack('default', null) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2 - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTracks[0].stop).toHaveBeenCalledTimes(0) + + expectedTrackEnabledStateInOutputTrackSetEvent = undefined + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + }) + + describe('stop input track', () => { + test('sets black video track as its output track when stopping enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack.stop() + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + }) + + test('sets black video track as its output track when stopping initially disabled and then enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4) + + blackVideoEnforcer._setInputTrackEnabled('default', true) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack.stop() + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 720, 540) + }) + + test('does nothing when stopping disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + inputTrack.stop() + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2 - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + }) + + test('does nothing when stopping initially enabled and then disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4) + + blackVideoEnforcer._setInputTrackEnabled('default', false) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + inputTrack.stop() + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 3 / 4 - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + }) + + test('removes output track when stopping enabled input track and then removing it', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + inputTrack.stop() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + blackVideoEnforcer._setInputTrack('default', null) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2 - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTracks[0].stop).toHaveBeenCalledTimes(0) + + expectedTrackEnabledStateInOutputTrackSetEvent = undefined + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('removes output track when stopping disabled input track and then removing it', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + inputTrack.stop() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + blackVideoEnforcer._setInputTrack('default', null) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2 - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTracks[0].stop).toHaveBeenCalledTimes(0) + + expectedTrackEnabledStateInOutputTrackSetEvent = undefined + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('sets input track as its output track when stopping enabled input track and then replacing it with another enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + inputTrack.stop() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack2._width = 320 + inputTrack2._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack2) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('sets black video track as its output track when stopping enabled input track and then replacing it with another disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + inputTrack.stop() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack2.enabled = false + inputTrack2._width = 320 + inputTrack2._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 320, 180) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 320, 180) + }) + + test('sets input track as its output track when stopping disabled input track and then replacing it with another enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + inputTrack.stop() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack2._width = 320 + inputTrack2._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack2) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('sets black video track as its output track when stopping disabled input track and then replacing it with another disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + inputTrack.stop() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack2.enabled = false + inputTrack2._width = 320 + inputTrack2._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 320, 180) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 320, 180) + }) + + test('does nothing when stopping a previously removed enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + blackVideoEnforcer._setInputTrack('default', null) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + inputTrack.stop() + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('does nothing when stopping a previously removed disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + blackVideoEnforcer._setInputTrack('default', null) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + inputTrack.stop() + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('does nothing when stopping a previously replaced enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + inputTrack2._width = 320 + inputTrack2._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + inputTrack.stop() + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + }) + + test('does nothing when stopping a previously replaced disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + inputTrack2._width = 320 + inputTrack2._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + inputTrack.stop() + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('sets black video track as its output track when stopping input track after setting it again', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4) + + inputTrack._width = 320 + inputTrack._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + inputTrack.stop() + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 320, 180) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 320, 180) + }) + }) + + describe('update input track', () => { + test('sets input track as its output track when setting same enabled input track again', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack._width = 320 + inputTrack._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + }) + + test('sets black video track as its output track when setting same disabled input track again', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack._width = 320 + inputTrack._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1]) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 320, 180) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 320, 180) + }) + + test('sets black video track as its output track when setting same now disabled input track again', () => { + const inputTrack = newMediaStreamTrackMock('input') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack.enabled = false + inputTrack._width = 320 + inputTrack._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 320, 180) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 320, 180) + }) + + test('sets input track as its output track when setting same now enabled input track again', () => { + const inputTrack = newMediaStreamTrackMock('input') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack.enabled = true + inputTrack._width = 320 + inputTrack._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + + test('sets input track as its output track when setting another enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack2) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(0) + }) + + test('sets black video track as its output track when setting another disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack2.enabled = false + inputTrack2._width = 320 + inputTrack2._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 320, 180) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 320, 180) + }) + + test('sets black video track as its output track when setting another now disabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack2.enabled = false + inputTrack2._width = 320 + inputTrack2._height = 180 + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 320, 180) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1) + expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 320, 180) + }) + + test('sets input track as its output track when setting another now enabled input track', () => { + const inputTrack = newMediaStreamTrackMock('input') + const inputTrack2 = newMediaStreamTrackMock('input2') + + inputTrack.enabled = false + blackVideoEnforcer._setInputTrack('default', inputTrack) + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + inputTrack2.enabled = true + blackVideoEnforcer._setInputTrack('default', inputTrack2) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack2) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(1) + assertBlackVideoTrack(0, 720, 540, STOPPED) + }) + }) +}) diff --git a/src/utils/webrtc/simplewebrtc/localmedia.js b/src/utils/webrtc/simplewebrtc/localmedia.js index 23bf1ba7d..5fc3d8de2 100644 --- a/src/utils/webrtc/simplewebrtc/localmedia.js +++ b/src/utils/webrtc/simplewebrtc/localmedia.js @@ -7,6 +7,7 @@ const mockconsole = require('mockconsole') // Only mediaDevicesManager is used, but it can not be assigned here due to not // being initialized yet. const webrtcIndex = require('../index.js') +const BlackVideoEnforcer = require('../../media/pipeline/BlackVideoEnforcer.js').default const MediaDevicesSource = require('../../media/pipeline/MediaDevicesSource.js').default const SpeakingMonitor = require('../../media/pipeline/SpeakingMonitor.js').default const TrackConstrainer = require('../../media/pipeline/TrackConstrainer.js').default @@ -58,6 +59,8 @@ function LocalMedia(opts) { this.emit('virtualBackgroundLoadFailed') }) + this._blackVideoEnforcer = new BlackVideoEnforcer() + this._speakingMonitor = new SpeakingMonitor() this._speakingMonitor.on('speaking', () => { this.emit('speaking') @@ -99,7 +102,9 @@ function LocalMedia(opts) { this._videoTrackConstrainer.connectTrackSink('default', this._virtualBackground) this._virtualBackground.connectTrackSink('default', this._trackToStream, 'video') - this._virtualBackground.connectTrackSink('default', this._trackToSentStream, 'video') + this._virtualBackground.connectTrackSink('default', this._blackVideoEnforcer, 'default') + + this._blackVideoEnforcer.connectTrackSink('default', this._trackToSentStream, 'video') } util.inherits(LocalMedia, WildEmitter) |