diff options
author | Joas Schilling <213943+nickvergessen@users.noreply.github.com> | 2022-08-11 11:41:51 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-11 11:41:51 +0300 |
commit | e40448aee54206bfd6518adf7466d3b42d28a04a (patch) | |
tree | 7f3d33bef3cdfe85f1de1c763bb59adba1704f38 /src | |
parent | 61d0fe08b5f011f47dd3306fd0b791efda88d1d0 (diff) | |
parent | 1e1c4bc35908e51833c99388bb2622c2f3f5813c (diff) |
Merge pull request #7673 from nextcloud/enforce-zero-information-content-video-tracks
Reduce sent information with disabled videos
Diffstat (limited to 'src')
-rw-r--r-- | src/utils/media/pipeline/BlackVideoEnforcer.js | 169 | ||||
-rw-r--r-- | src/utils/media/pipeline/BlackVideoEnforcer.spec.js | 1505 | ||||
-rw-r--r-- | src/utils/webrtc/simplewebrtc/localmedia.js | 71 | ||||
-rw-r--r-- | src/utils/webrtc/simplewebrtc/peer.js | 24 |
4 files changed, 1745 insertions, 24 deletions
diff --git a/src/utils/media/pipeline/BlackVideoEnforcer.js b/src/utils/media/pipeline/BlackVideoEnforcer.js new file mode 100644 index 000000000..76dcd3af8 --- /dev/null +++ b/src/utils/media/pipeline/BlackVideoEnforcer.js @@ -0,0 +1,169 @@ +/** + * + * @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. + * + * -------------------- + * | | + * ---> | BlackVideoEnforcer | ---> + * | | + * -------------------- + */ +export default class BlackVideoEnforcer extends TrackSinkSource { + + constructor() { + super() + + this._addInputTrackSlot() + this._addOutputTrackSlot() + } + + _handleInputTrack(trackId, newTrack, oldTrack) { + if (oldTrack && this._startBlackVideoWhenTrackEndedHandler) { + oldTrack.removeEventListener('ended', this._startBlackVideoWhenTrackEndedHandler) + this._startBlackVideoWhenTrackEndedHandler = null + } + + 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) + + // Sometimes Chromium does not render one or more frames to the stream + // captured from a canvas, so repeat the drawing several times for + // several seconds to work around that. + this._renderInterval = setInterval(() => { + outputCanvasContext.fillRect(0, 0, outputCanvasElement.width, outputCanvasElement.height) + }, 100) + + this._setOutputTrack('default', this._outputStream.getVideoTracks()[0]) + + this._disableOrRemoveOutputTrackTimeout = setTimeout(() => { + clearTimeout(this._disableOrRemoveOutputTrackTimeout) + this._disableOrRemoveOutputTrackTimeout = null + + clearInterval(this._renderInterval) + this._renderInterval = null + + if (this.getInputTrack()) { + this._setOutputTrackEnabled('default', false) + } else { + this._stopBlackVideo() + this._setOutputTrack('default', null) + } + }, 5000) + } + + _stopBlackVideo() { + if (!this._outputStream) { + return + } + + clearTimeout(this._disableOrRemoveOutputTrackTimeout) + this._disableOrRemoveOutputTrackTimeout = null + + clearInterval(this._renderInterval) + this._renderInterval = 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..70c20f1da --- /dev/null +++ b/src/utils/media/pipeline/BlackVideoEnforcer.spec.js @@ -0,0 +1,1505 @@ +/** + * + * @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) + clearInterval(blackVideoEnforcer._renderInterval) + }) + + afterAll(() => { + jest.restoreAllMocks() + }) + + const DISABLE_OR_REMOVE_TIMEOUT = 5000 + + 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('sets black video track as its output track and later 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() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + blackVideoEnforcer._setInputTrack('default', null) + + 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) + expect(blackVideoTracks[1].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(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 720, 540, STOPPED) + }) + + test('sets black video track as its output track and later 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() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + blackVideoEnforcer._setInputTrack('default', null) + + 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) + expect(blackVideoTracks[1].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(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 720, 540, STOPPED) + }) + + test('sets black video track as its output track and later removes output track when removing null track', () => { + 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, 640, 480) + + 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, 640, 480, STOPPED) + }) + + test('sets black video track as its output track and later removes output track 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(1) + expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1]) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTrackCount).toBe(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 640, 480) + + outputTrackSetHandler.mockClear() + outputTrackEnabledHandler.mockClear() + + jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1) + + expect(outputTrackSetHandler).toHaveBeenCalledTimes(0) + expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0) + expect(blackVideoTracks[1].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(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 640, 480, 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('sets black video track as its output track and later 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() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + blackVideoEnforcer._setInputTrack('default', null) + + 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) + expect(blackVideoTracks[1].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(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 720, 540, STOPPED) + }) + + test('sets black video track as its output track and later 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() + + expectedTrackEnabledStateInOutputTrackSetEvent = true + + blackVideoEnforcer._setInputTrack('default', null) + + 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) + expect(blackVideoTracks[1].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(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 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(2) + assertBlackVideoTrack(0, 720, 540, STOPPED) + assertBlackVideoTrack(1, 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 ab99bf681..7ae5df837 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 @@ -39,6 +40,7 @@ function LocalMedia(opts) { this._localMediaActive = false this.localStreams = [] + this.sentStreams = [] this.localScreens = [] if (!webrtcIndex.mediaDevicesManager.isSupported()) { @@ -57,6 +59,8 @@ function LocalMedia(opts) { this.emit('virtualBackgroundLoadFailed') }) + this._blackVideoEnforcer = new BlackVideoEnforcer() + this._speakingMonitor = new SpeakingMonitor() this._speakingMonitor.on('speaking', () => { this.emit('speaking') @@ -78,6 +82,10 @@ function LocalMedia(opts) { this._trackToStream.addInputTrackSlot('audio') this._trackToStream.addInputTrackSlot('video') + this._trackToSentStream = new TrackToStream() + this._trackToSentStream.addInputTrackSlot('audio') + this._trackToSentStream.addInputTrackSlot('video') + this._handleStreamSetBound = this._handleStreamSet.bind(this) this._handleTrackReplacedBound = this._handleTrackReplaced.bind(this) this._handleTrackEnabledBound = this._handleTrackEnabled.bind(this) @@ -87,12 +95,16 @@ function LocalMedia(opts) { this._audioTrackEnabler.connectTrackSink('default', this._speakingMonitor) this._audioTrackEnabler.connectTrackSink('default', this._trackToStream, 'audio') + this._audioTrackEnabler.connectTrackSink('default', this._trackToSentStream, 'audio') this._videoTrackEnabler.connectTrackSink('default', this._videoTrackConstrainer) this._videoTrackConstrainer.connectTrackSink('default', this._virtualBackground) this._virtualBackground.connectTrackSink('default', this._trackToStream, 'video') + this._virtualBackground.connectTrackSink('default', this._blackVideoEnforcer, 'default') + + this._blackVideoEnforcer.connectTrackSink('default', this._trackToSentStream, 'video') } util.inherits(LocalMedia, WildEmitter) @@ -166,6 +178,7 @@ LocalMedia.prototype.start = function(mediaConstraints, cb, context) { this._mediaDevicesSource.start(retryNoVideoCallback).then(() => { self.localStreams.push(self._trackToStream.getStream()) + self.sentStreams.push(self._trackToSentStream.getStream()) self.emit('localStream', self._trackToStream.getStream()) @@ -173,6 +186,10 @@ LocalMedia.prototype.start = function(mediaConstraints, cb, context) { self._trackToStream.on('trackReplaced', self._handleTrackReplacedBound) self._trackToStream.on('trackEnabled', self._handleTrackEnabledBound) + self._trackToSentStream.on('streamSet', self._handleStreamSetBound) + self._trackToSentStream.on('trackReplaced', self._handleTrackReplacedBound) + self._trackToSentStream.on('trackEnabled', self._handleTrackEnabledBound) + self._localMediaActive = true if (cb) { @@ -190,6 +207,10 @@ LocalMedia.prototype.start = function(mediaConstraints, cb, context) { self._trackToStream.on('trackReplaced', self._handleTrackReplacedBound) self._trackToStream.on('trackEnabled', self._handleTrackEnabledBound) + self._trackToSentStream.on('streamSet', self._handleStreamSetBound) + self._trackToSentStream.on('trackReplaced', self._handleTrackReplacedBound) + self._trackToSentStream.on('trackEnabled', self._handleTrackEnabledBound) + self._localMediaActive = true if (cb) { @@ -204,7 +225,7 @@ LocalMedia.prototype._handleStreamSet = function(trackToStream, newStream, oldSt } if (newStream) { - this.localStreams.push(newStream) + trackToStream === this._trackToStream ? this.localStreams.push(newStream) : this.sentStreams.push(newStream) } // "streamSet" is always emitted along with "trackReplaced", so the @@ -212,16 +233,24 @@ LocalMedia.prototype._handleStreamSet = function(trackToStream, newStream, oldSt } LocalMedia.prototype._handleTrackReplaced = function(trackToStream, newTrack, oldTrack) { - // "localStreamChanged" is expected to be emitted also when the tracks of - // the stream change, even if the stream itself is the same. - this.emit('localStreamChanged', trackToStream.getStream()) - this.emit('localTrackReplaced', newTrack, oldTrack, trackToStream.getStream()) + if (trackToStream === this._trackToStream) { + // "localStreamChanged" is expected to be emitted also when the tracks + // of the stream change, even if the stream itself is the same. + this.emit('localStreamChanged', trackToStream.getStream()) + this.emit('localTrackReplaced', newTrack, oldTrack, trackToStream.getStream()) + } else { + this.emit('sentTrackReplaced', newTrack, oldTrack, trackToStream.getStream()) + } } LocalMedia.prototype._handleTrackEnabled = function(trackToStream, track) { // MediaStreamTrack does not emit an event when the enabled property // changes, so it needs to be explicitly notified. - this.emit('localTrackEnabledChanged', track, trackToStream.getStream()) + if (trackToStream === this._trackToStream) { + this.emit('localTrackEnabledChanged', track, trackToStream.getStream()) + } else { + this.emit('sentTrackEnabledChanged', track, trackToStream.getStream()) + } } LocalMedia.prototype.stop = function() { @@ -231,6 +260,10 @@ LocalMedia.prototype.stop = function() { this._trackToStream.off('trackReplaced', this._handleTrackReplacedBound) this._trackToStream.off('trackEnabled', this._handleTrackEnabledBound) + this._trackToSentStream.off('streamSet', this._handleStreamSetBound) + this._trackToSentStream.off('trackReplaced', this._handleTrackReplacedBound) + this._trackToSentStream.off('trackEnabled', this._handleTrackEnabledBound) + this.stopStream() this.stopScreenShare() @@ -239,12 +272,16 @@ LocalMedia.prototype.stop = function() { LocalMedia.prototype.stopStream = function() { const stream = this._trackToStream.getStream() + const sentStream = this._trackToSentStream.getStream() this._mediaDevicesSource.stop() if (stream) { this._removeStream(stream) } + if (sentStream) { + this._removeStream(sentStream) + } } LocalMedia.prototype.startScreenShare = function(mode, constraints, cb) { @@ -431,12 +468,22 @@ LocalMedia.prototype._removeStream = function(stream) { if (idx > -1) { this.localStreams.splice(idx, 1) this.emit('localStreamStopped', stream) - } else { - idx = this.localScreens.indexOf(stream) - if (idx > -1) { - this.localScreens.splice(idx, 1) - this.emit('localScreenStopped', stream) - } + + return + } + + idx = this.sentStreams.indexOf(stream) + if (idx > -1) { + this.sentStreams.splice(idx, 1) + this.emit('sentStreamStopped', stream) + + return + } + + idx = this.localScreens.indexOf(stream) + if (idx > -1) { + this.localScreens.splice(idx, 1) + this.emit('localScreenStopped', stream) } } diff --git a/src/utils/webrtc/simplewebrtc/peer.js b/src/utils/webrtc/simplewebrtc/peer.js index edf7885ea..df67298fd 100644 --- a/src/utils/webrtc/simplewebrtc/peer.js +++ b/src/utils/webrtc/simplewebrtc/peer.js @@ -70,7 +70,7 @@ function Peer(options) { if (sender.track) { // The stream is not known, but it is only used when the // track is added, so it can be ignored here. - self.handleLocalTrackEnabledChanged(sender.track, null) + self.handleSentTrackEnabledChanged(sender.track, null) } }) @@ -122,7 +122,7 @@ function Peer(options) { this.broadcaster = options.broadcaster } } else { - this.parent.localStreams.forEach(function(stream) { + this.parent.sentStreams.forEach(function(stream) { stream.getTracks().forEach(function(track) { if (track.kind !== 'video' || self.sendVideoIfAvailable) { self.pc.addTrack(track, stream) @@ -130,13 +130,13 @@ function Peer(options) { }) }) - this.handleLocalTrackReplacedBound = this.handleLocalTrackReplaced.bind(this) + this.handleSentTrackReplacedBound = this.handleSentTrackReplaced.bind(this) // TODO What would happen if the track is replaced while the peer is // still negotiating the offer and answer? - this.parent.on('localTrackReplaced', this.handleLocalTrackReplacedBound) + this.parent.on('sentTrackReplaced', this.handleSentTrackReplacedBound) - this.handleLocalTrackEnabledChangedBound = this.handleLocalTrackEnabledChanged.bind(this) - this.parent.on('localTrackEnabledChanged', this.handleLocalTrackEnabledChangedBound) + this.handleSentTrackEnabledChangedBound = this.handleSentTrackEnabledChanged.bind(this) + this.parent.on('sentTrackEnabledChanged', this.handleSentTrackEnabledChangedBound) } } @@ -675,13 +675,13 @@ Peer.prototype.end = function() { } this.pc.close() this.handleStreamRemoved() - this.parent.off('localTrackReplaced', this.handleLocalTrackReplacedBound) - this.parent.off('localTrackEnabledChanged', this.handleLocalTrackEnabledChangedBound) + this.parent.off('sentTrackReplaced', this.handleSentTrackReplacedBound) + this.parent.off('sentTrackEnabledChanged', this.handleSentTrackEnabledChangedBound) this.parent.emit('peerEnded', this) } -Peer.prototype.handleLocalTrackReplaced = function(newTrack, oldTrack, stream) { +Peer.prototype.handleSentTrackReplaced = function(newTrack, oldTrack, stream) { this._pendingReplaceTracksQueue.push({ newTrack, oldTrack, stream }) this._processPendingReplaceTracks() @@ -864,14 +864,14 @@ Peer.prototype._replaceTrack = async function(newTrack, oldTrack, stream) { return Promise.allSettled(replaceTrackPromises) } -Peer.prototype.handleLocalTrackEnabledChanged = function(track, stream) { +Peer.prototype.handleSentTrackEnabledChanged = function(track, stream) { const sender = this.pc.getSenders().find(sender => sender.track === track) const stoppedSender = this.pc.getSenders().find(sender => sender.trackDisabled === track) if (track.enabled && stoppedSender) { - this.handleLocalTrackReplacedBound(track, track, stream) + this.handleSentTrackReplacedBound(track, track, stream) } else if (!track.enabled && sender) { - this.handleLocalTrackReplacedBound(track, track, stream) + this.handleSentTrackReplacedBound(track, track, stream) } } |