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/utils
diff options
context:
space:
mode:
authorDaniel Calviño Sánchez <danxuliu@gmail.com>2022-04-19 03:47:06 +0300
committerDaniel Calviño Sánchez <danxuliu@gmail.com>2022-04-20 17:02:59 +0300
commit4063922525645ea815d5d71d8cc01877c0a97187 (patch)
treea5763c0e3e714af24bfb163ea48f3d9549b28537 /src/utils
parent6c002a6df1b23e34746790ead4ca29591f426ef9 (diff)
Add helper class to block a remote video when not needed
A remote video is not needed when it was explicitly disabled by the local user (independently of whether it is enabled by the remote participant) or when it is not visible. Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/webrtc/RemoteVideoBlocker.js131
-rw-r--r--src/utils/webrtc/RemoteVideoBlocker.spec.js336
2 files changed, 467 insertions, 0 deletions
diff --git a/src/utils/webrtc/RemoteVideoBlocker.js b/src/utils/webrtc/RemoteVideoBlocker.js
new file mode 100644
index 000000000..2a8a65cbd
--- /dev/null
+++ b/src/utils/webrtc/RemoteVideoBlocker.js
@@ -0,0 +1,131 @@
+/**
+ *
+ * @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/>.
+ *
+ */
+
+/**
+ * Helper to block the remote video when not needed.
+ *
+ * A remote video is not needed if the local user explicitly disabled it
+ * (independently of whether the remote user (and thus owner of the remote
+ * video) has it enabled or not) or if it is not visible.
+ *
+ * The remote video is not immediately hidden when no longer visible; a few
+ * seconds are waited to avoid blocking and unblocking on layout changes.
+ *
+ * "increaseVisibleCounter()" can be called several times by the same view, but
+ * "decreaseVisibleCounter()" must have been called a corresponding number of
+ * times once the view is destroyed.
+ *
+ * A single RemoteVideoBlocker is assumed to be associated with its
+ * CallParticipantModel, and it is also assumed to be the only element blocking
+ * and unblocking the video. Otherwise the result is undefined.
+ *
+ * Note that the RemoteVideoBlocker can be used on participants that do not have
+ * a video at all (for example, because they do not have a camera or they do not
+ * have video permissions). In that case the CallParticipantModel will block the
+ * video if needed if it becomes available.
+ *
+ * @param {object} callParticipantModel the model to block/unblock the video on.
+ */
+export default function RemoteVideoBlocker(callParticipantModel) {
+ this._model = callParticipantModel
+
+ // Keep track of the blocked state here, as the Peer object may not block
+ // the video if some features are missing, and even if the video is blocked
+ // the attribute will not be updated right away but once the renegotiation
+ // is done.
+ this._blocked = false
+
+ this._enabled = true
+ this._visibleCounter = 1
+
+ this._blockVideoTimeout = null
+
+ // Block by default if not shown after creation.
+ this.decreaseVisibleCounter()
+}
+
+RemoteVideoBlocker.prototype = {
+
+ isVideoEnabled() {
+ return this._enabled
+ },
+
+ setVideoEnabled(enabled) {
+ this._enabled = enabled
+
+ const hadBlockVideoTimeout = this._blockVideoTimeout
+
+ clearTimeout(this._blockVideoTimeout)
+ this._blockVideoTimeout = null
+
+ if (!this._visibleCounter && !hadBlockVideoTimeout) {
+ return
+ }
+
+ this._setVideoBlocked(!enabled)
+ },
+
+ increaseVisibleCounter() {
+ this._visibleCounter++
+
+ clearTimeout(this._blockVideoTimeout)
+ this._blockVideoTimeout = null
+
+ if (!this._enabled) {
+ return
+ }
+
+ this._setVideoBlocked(false)
+ },
+
+ decreaseVisibleCounter() {
+ if (this._visibleCounter <= 0) {
+ console.error('Visible counter decreased when not visible')
+
+ return
+ }
+
+ this._visibleCounter--
+
+ if (this._visibleCounter > 0 || !this._enabled) {
+ return
+ }
+
+ clearTimeout(this._blockVideoTimeout)
+
+ this._blockVideoTimeout = setTimeout(() => {
+ this._setVideoBlocked(true)
+
+ this._blockVideoTimeout = null
+ }, 5000)
+ },
+
+ _setVideoBlocked(blocked) {
+ if (this._blocked === blocked) {
+ return
+ }
+
+ this._blocked = blocked
+
+ this._model.setVideoBlocked(blocked)
+ },
+
+}
diff --git a/src/utils/webrtc/RemoteVideoBlocker.spec.js b/src/utils/webrtc/RemoteVideoBlocker.spec.js
new file mode 100644
index 000000000..700b2918b
--- /dev/null
+++ b/src/utils/webrtc/RemoteVideoBlocker.spec.js
@@ -0,0 +1,336 @@
+/**
+ *
+ * @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 RemoteVideoBlocker from './RemoteVideoBlocker'
+
+describe('RemoteVideoBlocker', () => {
+ let callParticipantModel
+ let remoteVideoBlocker
+
+ beforeEach(() => {
+ jest.useFakeTimers()
+
+ callParticipantModel = {
+ setVideoBlocked: jest.fn()
+ }
+
+ remoteVideoBlocker = new RemoteVideoBlocker(callParticipantModel)
+ })
+
+ test('blocks the video by default if not shown in some seconds', () => {
+ jest.advanceTimersByTime(4000)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1000)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+
+ describe('set video enabled', () => {
+ test('immediately blocks the video', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+
+ remoteVideoBlocker.setVideoEnabled(false)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(false)
+ })
+ test('immediately unblocks the video', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+
+ remoteVideoBlocker.setVideoEnabled(false)
+ remoteVideoBlocker.setVideoEnabled(true)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(2)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenNthCalledWith(2, false)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+ })
+
+ describe('set video visible', () => {
+ test('does nothing if shown', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0)
+ expect(remoteVideoBlocker._visibleCounter).toBe(1)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+
+ test('does nothing if hidden without showing first', () => {
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(remoteVideoBlocker._visibleCounter).toBe(0)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+
+ test('blocks the video after some seconds when hidden', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.advanceTimersByTime(4000)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1000)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+ expect(remoteVideoBlocker._visibleCounter).toBe(0)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+
+ test('does nothing if shown again before blocking', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.advanceTimersByTime(4000)
+
+ remoteVideoBlocker.increaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0)
+ expect(remoteVideoBlocker._visibleCounter).toBe(1)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+
+ test('immediately unblocks the video after showing', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ remoteVideoBlocker.increaseVisibleCounter()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(2)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenNthCalledWith(2, false)
+ expect(remoteVideoBlocker._visibleCounter).toBe(1)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+
+ test('does nothing if not fully hidden', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0)
+ expect(remoteVideoBlocker._visibleCounter).toBe(2)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+ })
+
+ describe('set video enabled and visible', () => {
+ test('immediately blocks the video if disabled when visible', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.increaseVisibleCounter()
+
+ remoteVideoBlocker.setVideoEnabled(false)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(false)
+ })
+
+ test('immediately blocks the video if disabled before blocking after hidden', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.advanceTimersByTime(4000)
+
+ remoteVideoBlocker.setVideoEnabled(false)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(false)
+ })
+
+ test('blocks the video after some seconds if hidden when enabled', () => {
+ remoteVideoBlocker.setVideoEnabled(true)
+
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.advanceTimersByTime(4000)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1000)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+
+ test('does nothing if disabled when hidden', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ remoteVideoBlocker.setVideoEnabled(false)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(false)
+ })
+
+ test('does nothing if enabled when hidden', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ remoteVideoBlocker.setVideoEnabled(false)
+ remoteVideoBlocker.setVideoEnabled(true)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+
+ test('does nothing if hidden when disabled', () => {
+ remoteVideoBlocker.setVideoEnabled(false)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(false)
+ })
+
+ test('does nothing if shown when disabled', () => {
+ remoteVideoBlocker.setVideoEnabled(false)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+ remoteVideoBlocker.increaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(false)
+ })
+
+ test('immediately unblocks the video if enabled after showing', () => {
+ remoteVideoBlocker.setVideoEnabled(false)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.increaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+
+ remoteVideoBlocker.setVideoEnabled(true)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(2)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenNthCalledWith(2, false)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+
+ test('immediately unblocks the video if shown after enabled', () => {
+ remoteVideoBlocker.increaseVisibleCounter()
+ remoteVideoBlocker.decreaseVisibleCounter()
+
+ jest.runAllTimers()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true)
+
+ remoteVideoBlocker.setVideoEnabled(false)
+ remoteVideoBlocker.setVideoEnabled(true)
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1)
+
+ remoteVideoBlocker.increaseVisibleCounter()
+
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(2)
+ expect(callParticipantModel.setVideoBlocked).toHaveBeenNthCalledWith(2, false)
+
+ expect(remoteVideoBlocker.isVideoEnabled()).toBe(true)
+ })
+ })
+})