Pārlūkot izejas kodu

ref/feat: add QualityController (#1223)

Refactors the way send/receive video constraints are managed and puts the high level
logic in a separate module. See QualityController.js for high level overview on how
the constraints are managed now.

Adds events to JitsiConference fired whenever it starts new jvb/p2p session.

Adds event to JingleSessionPC.js when remote party signals receive max frame height.
Also adds signaling of the local recv preference for the p2p mode(only existed for JVB).
dev1
Paweł Domas 5 gadus atpakaļ
vecāks
revīzija
fb494bbd75
Revīzijas autora e-pasta adrese nav piesaistīta nevienam kontam

+ 48
- 49
JitsiConference.js Parādīt failu

61
     createJingleEvent,
61
     createJingleEvent,
62
     createP2PEvent
62
     createP2PEvent
63
 } from './service/statistics/AnalyticsEvents';
63
 } from './service/statistics/AnalyticsEvents';
64
+import { QualityController } from './modules/qualitycontrol/QualityController';
64
 import * as XMPPEvents from './service/xmpp/XMPPEvents';
65
 import * as XMPPEvents from './service/xmpp/XMPPEvents';
65
 
66
 
66
 const logger = getLogger(__filename);
67
 const logger = getLogger(__filename);
237
     this.recordingManager = new RecordingManager(this.room);
238
     this.recordingManager = new RecordingManager(this.room);
238
     this._conferenceJoinAnalyticsEventSent = false;
239
     this._conferenceJoinAnalyticsEventSent = false;
239
 
240
 
240
-    /**
241
-     * Max frame height that the user prefers to send to the remote participants.
242
-     * @type {number}
243
-     */
244
-    this.maxFrameHeight = null;
245
-
246
     if (browser.supportsInsertableStreams()) {
241
     if (browser.supportsInsertableStreams()) {
247
         this._e2eeCtx = new E2EEContext({ salt: this.options.name });
242
         this._e2eeCtx = new E2EEContext({ salt: this.options.name });
248
     }
243
     }
356
         this.eventManager.setupRTCListeners();
351
         this.eventManager.setupRTCListeners();
357
     }
352
     }
358
 
353
 
354
+    this.qualityController = new QualityController(this);
355
+
359
     this.participantConnectionStatus
356
     this.participantConnectionStatus
360
         = new ParticipantConnectionStatusHandler(
357
         = new ParticipantConnectionStatusHandler(
361
             this.rtc,
358
             this.rtc,
621
         new Error('The conference is has been already left'));
618
         new Error('The conference is has been already left'));
622
 };
619
 };
623
 
620
 
621
+/**
622
+ * Returns the currently active media session if any.
623
+ *
624
+ * @returns {JingleSessionPC|undefined}
625
+ * @private
626
+ */
627
+JitsiConference.prototype._getActiveMediaSession = function() {
628
+    return this.isP2PActive() ? this.p2pJingleSession : this.jvbJingleSession;
629
+};
630
+
631
+/**
632
+ * Returns an array containing all media sessions existing in this conference.
633
+ *
634
+ * @returns {Array<JingleSessionPC>}
635
+ * @private
636
+ */
637
+JitsiConference.prototype._getMediaSessions = function() {
638
+    const sessions = [];
639
+
640
+    this.jvbJingleSession && sessions.push(this.jvbJingleSession);
641
+    this.p2pJingleSession && sessions.push(this.p2pJingleSession);
642
+
643
+    return sessions;
644
+};
645
+
624
 /**
646
 /**
625
  * Returns name of this conference.
647
  * Returns name of this conference.
626
  */
648
  */
1727
     if (this.p2pJingleSession === session) {
1749
     if (this.p2pJingleSession === session) {
1728
         logger.info('P2P setAnswer');
1750
         logger.info('P2P setAnswer');
1729
 
1751
 
1730
-        // Apply pending video constraints.
1731
-        if (this.pendingVideoConstraintsOnP2P) {
1732
-            this.p2pJingleSession.setSenderVideoConstraint(this.maxFrameHeight)
1733
-                .catch(err => {
1734
-                    logger.error(`Sender video constraints failed on p2p session - ${err}`);
1735
-                });
1736
-        }
1737
-
1738
         // Setup E2EE.
1752
         // Setup E2EE.
1739
         const localTracks = this.getLocalTracks();
1753
         const localTracks = this.getLocalTracks();
1740
 
1754
 
1743
         }
1757
         }
1744
 
1758
 
1745
         this.p2pJingleSession.setAnswer(answer);
1759
         this.p2pJingleSession.setAnswer(answer);
1760
+        this.eventEmitter.emit(JitsiConferenceEvents._MEDIA_SESSION_STARTED, this.p2pJingleSession);
1746
     }
1761
     }
1747
 };
1762
 };
1748
 
1763
 
1917
                 // to be turned off here.
1932
                 // to be turned off here.
1918
                 if (this.isP2PActive() && this.jvbJingleSession) {
1933
                 if (this.isP2PActive() && this.jvbJingleSession) {
1919
                     this._suspendMediaTransferForJvbConnection();
1934
                     this._suspendMediaTransferForJvbConnection();
1920
-                } else if (this.jvbJingleSession && this.maxFrameHeight) {
1921
-                    // Apply user preferred max frame height if it was called before this
1922
-                    // jingle session was created.
1923
-                    this.jvbJingleSession.setSenderVideoConstraint(this.maxFrameHeight)
1924
-                        .catch(err => {
1925
-                            logger.error(`Sender video constraints failed on jvb session - ${err}`);
1926
-                        });
1935
+                }
1936
+
1937
+                this.eventEmitter.emit(
1938
+                    JitsiConferenceEvents._MEDIA_SESSION_STARTED,
1939
+                    jingleSession);
1940
+                if (!this.isP2PActive()) {
1941
+                    this.eventEmitter.emit(
1942
+                        JitsiConferenceEvents._MEDIA_SESSION_ACTIVE_CHANGED,
1943
+                        jingleSession);
1927
                 }
1944
                 }
1928
 
1945
 
1929
                 // Setup E2EE.
1946
                 // Setup E2EE.
2697
         () => {
2714
         () => {
2698
             logger.debug('Got RESULT for P2P "session-accept"');
2715
             logger.debug('Got RESULT for P2P "session-accept"');
2699
 
2716
 
2700
-            // Apply user preferred max frame height if it was called before this
2701
-            // jingle session was created.
2702
-            if (this.pendingVideoConstraintsOnP2P) {
2703
-                this.p2pJingleSession.setSenderVideoConstraint(this.maxFrameHeight)
2704
-                    .catch(err => {
2705
-                        logger.error(`Sender video constraints failed on p2p session - ${err}`);
2706
-                    });
2707
-            }
2717
+            this.eventEmitter.emit(
2718
+                JitsiConferenceEvents._MEDIA_SESSION_STARTED,
2719
+                this.p2pJingleSession);
2708
 
2720
 
2709
             // Setup E2EE.
2721
             // Setup E2EE.
2710
             for (const track of localTracks) {
2722
             for (const track of localTracks) {
3007
         JitsiConferenceEvents.P2P_STATUS,
3019
         JitsiConferenceEvents.P2P_STATUS,
3008
         this,
3020
         this,
3009
         this.p2p);
3021
         this.p2p);
3022
+    this.eventEmitter.emit(
3023
+        JitsiConferenceEvents._MEDIA_SESSION_ACTIVE_CHANGED,
3024
+        this._getActiveMediaSession());
3010
 
3025
 
3011
     // Refresh connection interrupted/restored
3026
     // Refresh connection interrupted/restored
3012
     this.eventEmitter.emit(
3027
     this.eventEmitter.emit(
3314
  * Sets the maximum video size the local participant should receive from remote
3329
  * Sets the maximum video size the local participant should receive from remote
3315
  * participants.
3330
  * participants.
3316
  *
3331
  *
3317
- * @param {number} maxFrameHeightPixels the maximum frame height, in pixels,
3332
+ * @param {number} maxFrameHeight - the maximum frame height, in pixels,
3318
  * this receiver is willing to receive.
3333
  * this receiver is willing to receive.
3319
  * @returns {void}
3334
  * @returns {void}
3320
  */
3335
  */
3321
-JitsiConference.prototype.setReceiverVideoConstraint = function(
3322
-        maxFrameHeight) {
3323
-    this.rtc.setReceiverVideoConstraint(maxFrameHeight);
3336
+JitsiConference.prototype.setReceiverVideoConstraint = function(maxFrameHeight) {
3337
+    this.qualityController.setPreferredReceiveMaxFrameHeight(maxFrameHeight);
3324
 };
3338
 };
3325
 
3339
 
3326
 /**
3340
 /**
3331
  * successful and rejected otherwise.
3345
  * successful and rejected otherwise.
3332
  */
3346
  */
3333
 JitsiConference.prototype.setSenderVideoConstraint = function(maxFrameHeight) {
3347
 JitsiConference.prototype.setSenderVideoConstraint = function(maxFrameHeight) {
3334
-    this.maxFrameHeight = maxFrameHeight;
3335
-    this.pendingVideoConstraintsOnP2P = true;
3336
-    const promises = [];
3337
-
3338
-    // We have to always set the sender video constraints on the jvb connection
3339
-    // when we switch from p2p to jvb connection since we need to check if the
3340
-    // tracks constraints have been modified when in p2p.
3341
-    if (this.jvbJingleSession) {
3342
-        promises.push(this.jvbJingleSession.setSenderVideoConstraint(maxFrameHeight));
3343
-    }
3344
-    if (this.p2pJingleSession) {
3345
-        this.pendingVideoConstraintsOnP2P = false;
3346
-        promises.push(this.p2pJingleSession.setSenderVideoConstraint(maxFrameHeight));
3347
-    }
3348
-
3349
-    return Promise.all(promises);
3348
+    return this.qualityController.setPreferredSendMaxFrameHeight(maxFrameHeight);
3350
 };
3349
 };
3351
 
3350
 
3352
 /**
3351
 /**

+ 14
- 0
JitsiConferenceEvents.js Parādīt failu

148
  */
148
  */
149
 export const SERVER_REGION_CHANGED = 'conference.server_region_changed';
149
 export const SERVER_REGION_CHANGED = 'conference.server_region_changed';
150
 
150
 
151
+/**
152
+ * An event(library-private) fired when a new media session is added to the conference.
153
+ * @type {string}
154
+ * @private
155
+ */
156
+export const _MEDIA_SESSION_STARTED = 'conference.media_session.started';
157
+
158
+/**
159
+ * An event(library-private) fired when the conference switches the currently active media session.
160
+ * @type {string}
161
+ * @private
162
+ */
163
+export const _MEDIA_SESSION_ACTIVE_CHANGED = 'conference.media_session.active_changed';
164
+
151
 /**
165
 /**
152
  * Indicates that the conference had changed to members only enabled/disabled.
166
  * Indicates that the conference had changed to members only enabled/disabled.
153
  * The first argument of this event is a <tt>boolean</tt> which when set to
167
  * The first argument of this event is a <tt>boolean</tt> which when set to

+ 86
- 0
modules/RTC/MockClasses.js Parādīt failu

1
+/* eslint-disable no-empty-function */
2
+
3
+/**
4
+ * Mock {@link TraceablePeerConnection} - add things as needed, but only things useful for all tests.
5
+ */
6
+export class MockPeerConnection {
7
+    /**
8
+     * {@link TraceablePeerConnection.localDescription}.
9
+     *
10
+     * @returns {Object}
11
+     */
12
+    get localDescription() {
13
+        return {
14
+            sdp: ''
15
+        };
16
+    }
17
+
18
+    /**
19
+     * {@link TraceablePeerConnection.remoteDescription}.
20
+     *
21
+     * @returns {Object}
22
+     */
23
+    get remoteDescription() {
24
+        return {
25
+            sdp: ''
26
+        };
27
+    }
28
+
29
+    /**
30
+     * {@link TraceablePeerConnection.createAnswer}.
31
+     *
32
+     * @returns {Promise<Object>}
33
+     */
34
+    createAnswer() {
35
+        return Promise.resolve(/* answer */{});
36
+    }
37
+
38
+    /**
39
+     * {@link TraceablePeerConnection.setLocalDescription}.
40
+     *
41
+     * @returns {Promise<void>}
42
+     */
43
+    setLocalDescription() {
44
+        return Promise.resolve();
45
+    }
46
+
47
+    /**
48
+     * {@link TraceablePeerConnection.setRemoteDescription}.
49
+     *
50
+     * @returns {Promise<void>}
51
+     */
52
+    setRemoteDescription() {
53
+        return Promise.resolve();
54
+    }
55
+
56
+    /**
57
+     * {@link TraceablePeerConnection.setSenderVideoConstraint}.
58
+     */
59
+    setSenderVideoConstraint() {
60
+    }
61
+
62
+    /**
63
+     * {@link TraceablePeerConnection.setVideoTransferActive}.
64
+     */
65
+    setVideoTransferActive() {
66
+        return false;
67
+    }
68
+}
69
+
70
+/**
71
+ * Mock {@link RTC} - add things as needed, but only things useful for all tests.
72
+ */
73
+export class MockRTC {
74
+    /**
75
+     * {@link RTC.createPeerConnection}.
76
+     *
77
+     * @returns {MockPeerConnection}
78
+     */
79
+    createPeerConnection() {
80
+        this.pc = new MockPeerConnection();
81
+
82
+        return this.pc;
83
+    }
84
+}
85
+
86
+/* eslint-enable no-empty-function */

+ 112
- 0
modules/qualitycontrol/QualityController.js Parādīt failu

1
+import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
2
+import MediaSessionEvents from '../xmpp/MediaSessionEvents';
3
+
4
+/**
5
+ * The class manages send and receive video constraints across media sessions({@link JingleSessionPC}) which belong to
6
+ * {@link JitsiConference}. It finds the lowest common value, between the local user's send preference and
7
+ * the remote party's receive preference. Also this module will consider only the active session's receive value,
8
+ * because local tracks are shared and while JVB may have no preference, the remote p2p may have and they may be totally
9
+ * different.
10
+ */
11
+export class QualityController {
12
+    /**
13
+     * Creates new instance for a given conference.
14
+     *
15
+     * @param {JitsiConference} conference - the conference instance for which the new instance will be managing
16
+     * the quality constraints.
17
+     */
18
+    constructor(conference) {
19
+        this.conference = conference;
20
+        this.conference.on(
21
+            JitsiConferenceEvents._MEDIA_SESSION_STARTED,
22
+            session => this._onMediaSessionStarted(session));
23
+        this.conference.on(
24
+            JitsiConferenceEvents._MEDIA_SESSION_ACTIVE_CHANGED,
25
+            () => this._propagateSendMaxFrameHeight());
26
+    }
27
+
28
+    /**
29
+     * Handles the {@link JitsiConferenceEvents.MEDIA_SESSION_STARTED}, that is when the conference creates new media
30
+     * session. It doesn't mean it's already active though. For example the JVB connection may be created after
31
+     * the conference has entered the p2p mode already.
32
+     *
33
+     * @param {JingleSessionPC} mediaSession - the started media session.
34
+     * @private
35
+     */
36
+    _onMediaSessionStarted(mediaSession) {
37
+        mediaSession.addListener(
38
+            MediaSessionEvents.REMOTE_VIDEO_CONSTRAINTS_CHANGED,
39
+            session => {
40
+                if (session === this.conference._getActiveMediaSession()) {
41
+                    this._propagateSendMaxFrameHeight();
42
+                }
43
+            });
44
+        this.preferredReceiveMaxFrameHeight
45
+            && mediaSession.setReceiverVideoConstraint(this.preferredReceiveMaxFrameHeight);
46
+    }
47
+
48
+    /**
49
+     * Figures out the send video constraint as specified by {@link selectSendMaxFrameHeight} and sets it on all media
50
+     * sessions for the reasons mentioned in this class description.
51
+     *
52
+     * @returns {Promise<void[]>}
53
+     * @private
54
+     */
55
+    _propagateSendMaxFrameHeight() {
56
+        const sendMaxFrameHeight = this.selectSendMaxFrameHeight();
57
+        const promises = [];
58
+
59
+        if (!sendMaxFrameHeight) {
60
+            return Promise.resolve();
61
+        }
62
+
63
+        for (const session of this.conference._getMediaSessions()) {
64
+            promises.push(session.setSenderVideoConstraint(sendMaxFrameHeight));
65
+        }
66
+
67
+        return Promise.all(promises);
68
+    }
69
+
70
+    /**
71
+     * Selects the lowest common value for the local video send constraint by looking at local user's preference and
72
+     * the active media session's receive preference set by the remote party.
73
+     *
74
+     * @returns {number|undefined}
75
+     */
76
+    selectSendMaxFrameHeight() {
77
+        const activeMediaSession = this.conference._getActiveMediaSession();
78
+        const remoteRecvMaxFrameHeight = activeMediaSession && activeMediaSession.getRemoteRecvMaxFrameHeight();
79
+
80
+        if (this.preferredSendMaxFrameHeight && remoteRecvMaxFrameHeight) {
81
+            return Math.min(this.preferredSendMaxFrameHeight, remoteRecvMaxFrameHeight);
82
+        } else if (remoteRecvMaxFrameHeight) {
83
+            return remoteRecvMaxFrameHeight;
84
+        }
85
+
86
+        return this.preferredSendMaxFrameHeight;
87
+    }
88
+
89
+    /**
90
+     * Sets local preference for max receive video frame height.
91
+     * @param {number|undefined} maxFrameHeight - the new value.
92
+     */
93
+    setPreferredReceiveMaxFrameHeight(maxFrameHeight) {
94
+        this.preferredReceiveMaxFrameHeight = maxFrameHeight;
95
+
96
+        for (const session of this.conference._getMediaSessions()) {
97
+            maxFrameHeight && session.setReceiverVideoConstraint(maxFrameHeight);
98
+        }
99
+    }
100
+
101
+    /**
102
+     * Sets local preference for max send video frame height.
103
+     *
104
+     * @param {number} maxFrameHeight - the new value to set.
105
+     * @returns {Promise<void[]>} - resolved when the operation is complete.
106
+     */
107
+    setPreferredSendMaxFrameHeight(maxFrameHeight) {
108
+        this.preferredSendMaxFrameHeight = maxFrameHeight;
109
+
110
+        return this._propagateSendMaxFrameHeight();
111
+    }
112
+}

+ 3
- 1
modules/xmpp/JingleSession.js Parādīt failu

1
 /* global __filename */
1
 /* global __filename */
2
 import { getLogger } from 'jitsi-meet-logger';
2
 import { getLogger } from 'jitsi-meet-logger';
3
 import * as JingleSessionState from './JingleSessionState';
3
 import * as JingleSessionState from './JingleSessionState';
4
+import Listenable from '../util/Listenable';
4
 
5
 
5
 const logger = getLogger(__filename);
6
 const logger = getLogger(__filename);
6
 
7
 
9
  * have different implementations depending on the underlying interface used
10
  * have different implementations depending on the underlying interface used
10
  * (i.e. WebRTC and ORTC) and here we hold the code common to all of them.
11
  * (i.e. WebRTC and ORTC) and here we hold the code common to all of them.
11
  */
12
  */
12
-export default class JingleSession {
13
+export default class JingleSession extends Listenable {
13
 
14
 
14
     /* eslint-disable max-params */
15
     /* eslint-disable max-params */
15
 
16
 
34
             mediaConstraints,
35
             mediaConstraints,
35
             iceConfig,
36
             iceConfig,
36
             isInitiator) {
37
             isInitiator) {
38
+        super();
37
         this.sid = sid;
39
         this.sid = sid;
38
         this.localJid = localJid;
40
         this.localJid = localJid;
39
         this.remoteJid = remoteJid;
41
         this.remoteJid = remoteJid;

+ 99
- 16
modules/xmpp/JingleSessionPC.js Parādīt failu

11
 import browser from './../browser';
11
 import browser from './../browser';
12
 import JingleSession from './JingleSession';
12
 import JingleSession from './JingleSession';
13
 import * as JingleSessionState from './JingleSessionState';
13
 import * as JingleSessionState from './JingleSessionState';
14
+import MediaSessionEvents from './MediaSessionEvents';
14
 import SDP from './SDP';
15
 import SDP from './SDP';
15
 import SDPDiffer from './SDPDiffer';
16
 import SDPDiffer from './SDPDiffer';
16
 import SDPUtil from './SDPUtil';
17
 import SDPUtil from './SDPUtil';
90
         return null;
91
         return null;
91
     }
92
     }
92
 
93
 
94
+    /**
95
+     * Parses the video max frame height value out of the 'content-modify' IQ.
96
+     *
97
+     * @param {jQuery} jingleContents - A jQuery selector pointing to the '>jingle' element.
98
+     * @returns {Number|null}
99
+     */
100
+    static parseMaxFrameHeight(jingleContents) {
101
+        const maxFrameHeightSel = jingleContents.find('>content[name="video"]>max-frame-height');
102
+
103
+        return maxFrameHeightSel.length ? Number(maxFrameHeightSel.text()) : null;
104
+    }
105
+
93
     /* eslint-disable max-params */
106
     /* eslint-disable max-params */
94
 
107
 
95
     /**
108
     /**
173
          */
186
          */
174
         this._gatheringStartedTimestamp = null;
187
         this._gatheringStartedTimestamp = null;
175
 
188
 
189
+        /**
190
+         * Local preference for the receive video max frame height.
191
+         *
192
+         * @type {Number|undefined}
193
+         */
194
+        this.localRecvMaxFrameHeight = undefined;
195
+
176
         /**
196
         /**
177
          * Indicates whether or not this session is willing to send/receive
197
          * Indicates whether or not this session is willing to send/receive
178
          * video media. When set to <tt>false</tt> the underlying peer
198
          * video media. When set to <tt>false</tt> the underlying peer
221
          */
241
          */
222
         this.isP2P = isP2P;
242
         this.isP2P = isP2P;
223
 
243
 
244
+        /**
245
+         * Remote preference for the receive video max frame height.
246
+         *
247
+         * @type {Number|undefined}
248
+         */
249
+        this.remoteRecvMaxFrameHeight = undefined;
250
+
224
         /**
251
         /**
225
          * The signaling layer implementation.
252
          * The signaling layer implementation.
226
          * @type {SignalingLayerImpl}
253
          * @type {SignalingLayerImpl}
566
         }
593
         }
567
     }
594
     }
568
 
595
 
596
+    /**
597
+     * Remote preference for receive video max frame height.
598
+     *
599
+     * @returns {Number|undefined}
600
+     */
601
+    getRemoteRecvMaxFrameHeight() {
602
+        return this.isP2P
603
+            ? this.remoteRecvMaxFrameHeight
604
+            : undefined; // FIXME George: put a getter for the JVB's preference here
605
+    }
606
+
569
     /**
607
     /**
570
      * Sends given candidate in Jingle 'transport-info' message.
608
      * Sends given candidate in Jingle 'transport-info' message.
571
      * @param {RTCIceCandidate} candidate the WebRTC ICE candidate instance
609
      * @param {RTCIceCandidate} candidate the WebRTC ICE candidate instance
1028
                     if (this.state === JingleSessionState.PENDING) {
1066
                     if (this.state === JingleSessionState.PENDING) {
1029
                         this.state = JingleSessionState.ACTIVE;
1067
                         this.state = JingleSessionState.ACTIVE;
1030
 
1068
 
1031
-                        // Sync up video transfer active/inactive only after
1069
+                        // #1 Sync up video transfer active/inactive only after
1032
                         // the initial O/A cycle. We want to adjust the video
1070
                         // the initial O/A cycle. We want to adjust the video
1033
                         // media direction only in the local SDP and the Jingle
1071
                         // media direction only in the local SDP and the Jingle
1034
                         // contents direction included in the initial
1072
                         // contents direction included in the initial
1039
                         // Changing media direction in the remote SDP will mess
1077
                         // Changing media direction in the remote SDP will mess
1040
                         // up our SDP translation chain (simulcast, video mute,
1078
                         // up our SDP translation chain (simulcast, video mute,
1041
                         // RTX etc.)
1079
                         // RTX etc.)
1042
-                        if (this.isP2P && !this._localVideoActive) {
1043
-                            this.sendContentModify(this._localVideoActive);
1080
+                        //
1081
+                        // #2 Sends the max frame height if it was set, before the session-initiate/accept
1082
+                        if (this.isP2P
1083
+                            && (!this._localVideoActive || this.localRecvMaxFrameHeight)) {
1084
+                            this.sendContentModify();
1044
                         }
1085
                         }
1045
                     }
1086
                     }
1046
 
1087
 
1215
 
1256
 
1216
     /**
1257
     /**
1217
      * Will send 'content-modify' IQ in order to ask the remote peer to
1258
      * Will send 'content-modify' IQ in order to ask the remote peer to
1218
-     * either stop or resume sending video media.
1219
-     * @param {boolean} videoTransferActive <tt>false</tt> to let the other peer
1220
-     * know that we're not sending nor interested in receiving video contents.
1221
-     * When set to <tt>true</tt> remote peer will be asked to resume video
1222
-     * transfer.
1259
+     * either stop or resume sending video media or to adjust sender's video constraints.
1223
      * @private
1260
      * @private
1224
      */
1261
      */
1225
-    sendContentModify(videoTransferActive) {
1226
-        const newSendersValue = videoTransferActive ? 'both' : 'none';
1262
+    sendContentModify() {
1263
+        const maxFrameHeight = this.localRecvMaxFrameHeight;
1264
+        const senders = this._localVideoActive ? 'both' : 'none';
1227
 
1265
 
1228
-        const sessionModify
1266
+        let sessionModify
1229
             = $iq({
1267
             = $iq({
1230
                 to: this.remoteJid,
1268
                 to: this.remoteJid,
1231
                 type: 'set'
1269
                 type: 'set'
1238
                 })
1276
                 })
1239
                 .c('content', {
1277
                 .c('content', {
1240
                     name: 'video',
1278
                     name: 'video',
1241
-                    senders: newSendersValue
1279
+                    senders
1242
                 });
1280
                 });
1243
 
1281
 
1244
-        logger.info(
1245
-            `Sending content-modify, video senders: ${newSendersValue}`);
1282
+        if (typeof maxFrameHeight !== 'undefined') {
1283
+            sessionModify = sessionModify
1284
+                .c('max-frame-height', { xmlns: 'http://jitsi.org/jitmeet/video' })
1285
+                .t(maxFrameHeight);
1286
+        }
1287
+
1288
+        logger.info(`${this} sending content-modify, video senders: ${senders}, max frame height: ${maxFrameHeight}`);
1246
 
1289
 
1247
         this.connection.sendIQ(
1290
         this.connection.sendIQ(
1248
             sessionModify,
1291
             sessionModify,
1251
             IQ_TIMEOUT);
1294
             IQ_TIMEOUT);
1252
     }
1295
     }
1253
 
1296
 
1297
+    /**
1298
+     * Adjust the preference for max video frame height that the local party is willing to receive. Signals
1299
+     * the remote party.
1300
+     *
1301
+     * @param {Number} maxFrameHeight - the new value to set.
1302
+     */
1303
+    setReceiverVideoConstraint(maxFrameHeight) {
1304
+        logger.info(`${this} setReceiverVideoConstraint - max frame height: ${maxFrameHeight}`);
1305
+
1306
+        this.localRecvMaxFrameHeight = maxFrameHeight;
1307
+
1308
+        if (this.isP2P) {
1309
+            // Tell the remote peer about our receive constraint. If Jingle session is not yet active the state will
1310
+            // be synced after offer/answer.
1311
+            if (this.state === JingleSessionState.ACTIVE) {
1312
+                this.sendContentModify();
1313
+            }
1314
+        } else {
1315
+            this.rtc.setReceiverVideoConstraint(maxFrameHeight);
1316
+        }
1317
+    }
1318
+
1254
     /**
1319
     /**
1255
      * Sends Jingle 'transport-accept' message which is a response to
1320
      * Sends Jingle 'transport-accept' message which is a response to
1256
      * 'transport-replace'.
1321
      * 'transport-replace'.
1336
      * successful and rejected otherwise.
1401
      * successful and rejected otherwise.
1337
      */
1402
      */
1338
     setSenderVideoConstraint(maxFrameHeight) {
1403
     setSenderVideoConstraint(maxFrameHeight) {
1339
-        return this.peerconnection.setSenderVideoConstraint(maxFrameHeight);
1404
+        if (this._assertNotEnded()) {
1405
+            logger.info(`${this} setSenderVideoConstraint: ${maxFrameHeight}`);
1406
+
1407
+            return this.peerconnection.setSenderVideoConstraint(maxFrameHeight);
1408
+        }
1409
+
1410
+        return Promise.resolve();
1340
     }
1411
     }
1341
 
1412
 
1342
     /**
1413
     /**
2087
                 // 'inactive' on video media in remote SDP will mess up our SDP
2158
                 // 'inactive' on video media in remote SDP will mess up our SDP
2088
                 // translation chain (simulcast, RTX, video mute etc.).
2159
                 // translation chain (simulcast, RTX, video mute etc.).
2089
                 if (this.isP2P && isSessionActive) {
2160
                 if (this.isP2P && isSessionActive) {
2090
-                    this.sendContentModify(videoActive);
2161
+                    this.sendContentModify();
2091
                 }
2162
                 }
2092
             }
2163
             }
2093
 
2164
 
2134
     modifyContents(jingleContents) {
2205
     modifyContents(jingleContents) {
2135
         const newVideoSenders
2206
         const newVideoSenders
2136
             = JingleSessionPC.parseVideoSenders(jingleContents);
2207
             = JingleSessionPC.parseVideoSenders(jingleContents);
2208
+        const newMaxFrameHeight
2209
+            = JingleSessionPC.parseMaxFrameHeight(jingleContents);
2210
+
2211
+        // frame height is optional in our content-modify protocol
2212
+        if (newMaxFrameHeight) {
2213
+            logger.info(`${this} received remote max frame height: ${newMaxFrameHeight}`);
2214
+            this.remoteRecvMaxFrameHeight = newMaxFrameHeight;
2215
+            this.eventEmitter.emit(
2216
+                MediaSessionEvents.REMOTE_VIDEO_CONSTRAINTS_CHANGED,
2217
+                this,
2218
+                newMaxFrameHeight);
2219
+        }
2137
 
2220
 
2138
         if (newVideoSenders === null) {
2221
         if (newVideoSenders === null) {
2139
             logger.error(
2222
             logger.error(

+ 122
- 0
modules/xmpp/JingleSessionPC.spec.js Parādīt failu

1
+/* global $, jQuery */
2
+import { MockRTC } from '../RTC/MockClasses';
3
+
4
+import JingleSessionPC from './JingleSessionPC';
5
+import * as JingleSessionState from './JingleSessionState';
6
+import MediaSessionEvents from './MediaSessionEvents';
7
+import { MockChatRoom, MockStropheConnection } from './MockClasses';
8
+
9
+/**
10
+ * Creates 'content-modify' Jingle IQ.
11
+ * @param {string} senders - 'both' or 'none'.
12
+ * @param {number|undefined} maxFrameHeight - the receive max video frame height.
13
+ * @returns {jQuery}
14
+ */
15
+function createContentModify(senders = 'both', maxFrameHeight) {
16
+    const modifyContentsIq = jQuery.parseXML(
17
+        '<jingle action="content-modify" initiator="peer2" sid="sid12345" xmlns="urn:xmpp:jingle:1">'
18
+        + `<content name="video" senders="${senders}">`
19
+        + `<max-frame-height xmlns="http://jitsi.org/jitmeet/video">${maxFrameHeight}</max-frame-height>`
20
+        + '</content>'
21
+        + '</jingle>');
22
+
23
+    return $(modifyContentsIq).find('>jingle');
24
+}
25
+
26
+describe('JingleSessionPC', () => {
27
+    let jingleSession;
28
+    let connection;
29
+    let rtc;
30
+    const offerIQ = {
31
+        find: () => {
32
+            return {
33
+                // eslint-disable-next-line no-empty-function
34
+                each: () => { }
35
+            };
36
+        }
37
+    };
38
+
39
+    const SID = 'sid12345';
40
+
41
+    beforeEach(() => {
42
+        connection = new MockStropheConnection();
43
+        jingleSession = new JingleSessionPC(
44
+            SID,
45
+            'peer1',
46
+            'peer2',
47
+            connection,
48
+            { },
49
+            { },
50
+            true,
51
+            false);
52
+
53
+        rtc = new MockRTC();
54
+
55
+        jingleSession.initialize(
56
+            /* ChatRoom */ new MockChatRoom(),
57
+            /* RTC */ rtc,
58
+            /* options */ { });
59
+
60
+        // eslint-disable-next-line no-empty-function
61
+        // connection.connect('jid', undefined, () => { }); */
62
+    });
63
+
64
+    describe('send/receive video constraints', () => {
65
+        it('sends content-modify with recv frame size', () => {
66
+            const sendIQSpy = spyOn(connection, 'sendIQ').and.callThrough();
67
+
68
+            jingleSession.setReceiverVideoConstraint(180);
69
+
70
+            expect(jingleSession.getState()).toBe(JingleSessionState.PENDING);
71
+
72
+            return new Promise((resolve, reject) => {
73
+                jingleSession.acceptOffer(
74
+                    offerIQ,
75
+                    resolve,
76
+                    reject,
77
+                    /* local tracks */ []);
78
+            }).then(() => {
79
+                expect(jingleSession.getState()).toBe(JingleSessionState.ACTIVE);
80
+
81
+                // FIXME content-modify is sent before session-accept
82
+                expect(sendIQSpy.calls.count()).toBe(2);
83
+
84
+                expect(sendIQSpy.calls.first().args[0].toString()).toBe(
85
+                    '<iq to="peer2" type="set" xmlns="jabber:client">'
86
+                    + '<jingle action="content-modify" initiator="peer2" sid="sid12345" xmlns="urn:xmpp:jingle:1">'
87
+                    + '<content name="video" senders="both">'
88
+                    + '<max-frame-height xmlns="http://jitsi.org/jitmeet/video">180</max-frame-height>'
89
+                    + '</content>'
90
+                    + '</jingle>'
91
+                    + '</iq>');
92
+            });
93
+        });
94
+        it('fires an event when remote peer sends content-modify', () => {
95
+            let remoteRecvMaxFrameHeight;
96
+            const remoteVideoConstraintsListener = (session, maxFrameHeight) => {
97
+                remoteRecvMaxFrameHeight = maxFrameHeight;
98
+            };
99
+
100
+            jingleSession.addListener(
101
+                MediaSessionEvents.REMOTE_VIDEO_CONSTRAINTS_CHANGED,
102
+                remoteVideoConstraintsListener);
103
+
104
+            return new Promise((resolve, reject) => {
105
+                jingleSession.acceptOffer(
106
+                    offerIQ,
107
+                    resolve,
108
+                    reject,
109
+                    /* local tracks */ []);
110
+            }).then(() => {
111
+                jingleSession.modifyContents(createContentModify('both', 180));
112
+                expect(remoteRecvMaxFrameHeight).toBe(180);
113
+
114
+                jingleSession.modifyContents(createContentModify('both', 360));
115
+                expect(remoteRecvMaxFrameHeight).toBe(360);
116
+
117
+                jingleSession.modifyContents(createContentModify('both', 180));
118
+                expect(remoteRecvMaxFrameHeight).toBe(180);
119
+            });
120
+        });
121
+    });
122
+});

+ 6
- 0
modules/xmpp/MediaSessionEvents.js Parādīt failu

1
+export default {
2
+    /**
3
+     * Event triggered when the remote party signals it's receive video max frame height.
4
+     */
5
+    REMOTE_VIDEO_CONSTRAINTS_CHANGED: 'media_session.REMOTE_VIDEO_CONSTRAINTS_CHANGED'
6
+};

+ 71
- 0
modules/xmpp/MockClasses.js Parādīt failu

1
+import { Strophe } from 'strophe.js';
2
+import Listenable from '../util/Listenable';
3
+
4
+/* eslint-disable no-empty-function */
5
+
6
+/**
7
+ * Mock {@link ChatRoom}.
8
+ */
9
+export class MockChatRoom {
10
+    /**
11
+     * {@link ChatRoom.addPresenceListener}.
12
+     */
13
+    addPresenceListener() {
14
+    }
15
+}
16
+
17
+/**
18
+ * Mock Strophe connection.
19
+ */
20
+export class MockStropheConnection extends Listenable {
21
+    /**
22
+     * A constructor...
23
+     */
24
+    constructor() {
25
+        super();
26
+        this.sentIQs = [];
27
+    }
28
+
29
+    /**
30
+     * XMPP service URL.
31
+     *
32
+     * @returns {string}
33
+     */
34
+    get service() {
35
+        return 'wss://localhost/xmpp-websocket';
36
+    }
37
+
38
+    /**
39
+     * {@see Strophe.Connection.connect}
40
+     */
41
+    connect(jid, pass, callback) {
42
+        this._connectCb = callback;
43
+    }
44
+
45
+    /**
46
+     * {@see Strophe.Connection.disconnect}
47
+     */
48
+    disconnect() {
49
+        this.simulateConnectionState(Strophe.Status.DISCONNECTING);
50
+        this.simulateConnectionState(Strophe.Status.DISCONNECTED);
51
+    }
52
+
53
+    /**
54
+     * Simulates transition to the new connection status.
55
+     *
56
+     * @param {Strophe.Status} newState - The new connection status to set.
57
+     * @returns {void}
58
+     */
59
+    simulateConnectionState(newState) {
60
+        this._connectCb(newState);
61
+    }
62
+
63
+    /**
64
+     * {@see Strophe.Connection.sendIQ}.
65
+     */
66
+    sendIQ(iq, resultCb) {
67
+        this.sentIQs.push(iq);
68
+        resultCb && resultCb();
69
+    }
70
+}
71
+/* eslint-enable no-empty-function */

+ 3
- 46
modules/xmpp/XmppConnection.spec.js Parādīt failu

1
-import { default as XmppConnection } from './XmppConnection';
2
 import { $iq, Strophe } from 'strophe.js';
1
 import { $iq, Strophe } from 'strophe.js';
2
+
3
 import { nextTick } from '../util/TestUtils';
3
 import { nextTick } from '../util/TestUtils';
4
 
4
 
5
-/**
6
- * Mock Strophe connection.
7
- */
8
-class MockStropheConnection {
9
-    /**
10
-     * XMPP service URL.
11
-     *
12
-     * @returns {string}
13
-     */
14
-    get service() {
15
-        return 'wss://localhost/xmpp-websocket';
16
-    }
17
-
18
-    /**
19
-     * {@see Strophe.Connection.connect}
20
-     */
21
-    connect(jid, pass, callback) {
22
-        this._connectCb = callback;
23
-    }
24
-
25
-    /**
26
-     * {@see Strophe.Connection.disconnect}
27
-     */
28
-    disconnect() {
29
-        this.simulateConnectionState(Strophe.Status.DISCONNECTING);
30
-        this.simulateConnectionState(Strophe.Status.DISCONNECTED);
31
-    }
32
-
33
-    /**
34
-     * Simulates transition to the new connection status.
35
-     *
36
-     * @param {Strophe.Status} newState - The new connection status to set.
37
-     * @returns {void}
38
-     */
39
-    simulateConnectionState(newState) {
40
-        this._connectCb(newState);
41
-    }
42
-
43
-    /**
44
-     * {@see Strophe.Connection.sendIQ}.
45
-     */
46
-    sendIQ(iq, resultCb) {
47
-        resultCb();
48
-    }
49
-}
5
+import { default as XmppConnection } from './XmppConnection';
6
+import { MockStropheConnection } from './MockClasses';
50
 
7
 
51
 /**
8
 /**
52
  * Creates any IQ.
9
  * Creates any IQ.

Notiek ielāde…
Atcelt
Saglabāt