Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/spreed.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2022-08-11 11:41:51 +0300
committerGitHub <noreply@github.com>2022-08-11 11:41:51 +0300
commite40448aee54206bfd6518adf7466d3b42d28a04a (patch)
tree7f3d33bef3cdfe85f1de1c763bb59adba1704f38 /src
parent61d0fe08b5f011f47dd3306fd0b791efda88d1d0 (diff)
parent1e1c4bc35908e51833c99388bb2622c2f3f5813c (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.js169
-rw-r--r--src/utils/media/pipeline/BlackVideoEnforcer.spec.js1505
-rw-r--r--src/utils/webrtc/simplewebrtc/localmedia.js71
-rw-r--r--src/utils/webrtc/simplewebrtc/peer.js24
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)
}
}