Просмотр исходного кода

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).
master
Paweł Domas 5 лет назад
Родитель
Сommit
fb494bbd75
Аккаунт пользователя с таким Email не найден

+ 48
- 49
JitsiConference.js Просмотреть файл

@@ -61,6 +61,7 @@ import {
61 61
     createJingleEvent,
62 62
     createP2PEvent
63 63
 } from './service/statistics/AnalyticsEvents';
64
+import { QualityController } from './modules/qualitycontrol/QualityController';
64 65
 import * as XMPPEvents from './service/xmpp/XMPPEvents';
65 66
 
66 67
 const logger = getLogger(__filename);
@@ -237,12 +238,6 @@ export default function JitsiConference(options) {
237 238
     this.recordingManager = new RecordingManager(this.room);
238 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 241
     if (browser.supportsInsertableStreams()) {
247 242
         this._e2eeCtx = new E2EEContext({ salt: this.options.name });
248 243
     }
@@ -356,6 +351,8 @@ JitsiConference.prototype._init = function(options = {}) {
356 351
         this.eventManager.setupRTCListeners();
357 352
     }
358 353
 
354
+    this.qualityController = new QualityController(this);
355
+
359 356
     this.participantConnectionStatus
360 357
         = new ParticipantConnectionStatusHandler(
361 358
             this.rtc,
@@ -621,6 +618,31 @@ JitsiConference.prototype.leave = function() {
621 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 647
  * Returns name of this conference.
626 648
  */
@@ -1727,14 +1749,6 @@ JitsiConference.prototype.onCallAccepted = function(session, answer) {
1727 1749
     if (this.p2pJingleSession === session) {
1728 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 1752
         // Setup E2EE.
1739 1753
         const localTracks = this.getLocalTracks();
1740 1754
 
@@ -1743,6 +1757,7 @@ JitsiConference.prototype.onCallAccepted = function(session, answer) {
1743 1757
         }
1744 1758
 
1745 1759
         this.p2pJingleSession.setAnswer(answer);
1760
+        this.eventEmitter.emit(JitsiConferenceEvents._MEDIA_SESSION_STARTED, this.p2pJingleSession);
1746 1761
     }
1747 1762
 };
1748 1763
 
@@ -1917,13 +1932,15 @@ JitsiConference.prototype._acceptJvbIncomingCall = function(
1917 1932
                 // to be turned off here.
1918 1933
                 if (this.isP2PActive() && this.jvbJingleSession) {
1919 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 1946
                 // Setup E2EE.
@@ -2697,14 +2714,9 @@ JitsiConference.prototype._acceptP2PIncomingCall = function(
2697 2714
         () => {
2698 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 2721
             // Setup E2EE.
2710 2722
             for (const track of localTracks) {
@@ -3007,6 +3019,9 @@ JitsiConference.prototype._setP2PStatus = function(newStatus) {
3007 3019
         JitsiConferenceEvents.P2P_STATUS,
3008 3020
         this,
3009 3021
         this.p2p);
3022
+    this.eventEmitter.emit(
3023
+        JitsiConferenceEvents._MEDIA_SESSION_ACTIVE_CHANGED,
3024
+        this._getActiveMediaSession());
3010 3025
 
3011 3026
     // Refresh connection interrupted/restored
3012 3027
     this.eventEmitter.emit(
@@ -3314,13 +3329,12 @@ JitsiConference.prototype.getSpeakerStats = function() {
3314 3329
  * Sets the maximum video size the local participant should receive from remote
3315 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 3333
  * this receiver is willing to receive.
3319 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,22 +3345,7 @@ JitsiConference.prototype.setReceiverVideoConstraint = function(
3331 3345
  * successful and rejected otherwise.
3332 3346
  */
3333 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 Просмотреть файл

@@ -148,6 +148,20 @@ export const LOCK_STATE_CHANGED = 'conference.lock_state_changed';
148 148
  */
149 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 166
  * Indicates that the conference had changed to members only enabled/disabled.
153 167
  * The first argument of this event is a <tt>boolean</tt> which when set to

+ 86
- 0
modules/RTC/MockClasses.js Просмотреть файл

@@ -0,0 +1,86 @@
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 Просмотреть файл

@@ -0,0 +1,112 @@
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 Просмотреть файл

@@ -1,6 +1,7 @@
1 1
 /* global __filename */
2 2
 import { getLogger } from 'jitsi-meet-logger';
3 3
 import * as JingleSessionState from './JingleSessionState';
4
+import Listenable from '../util/Listenable';
4 5
 
5 6
 const logger = getLogger(__filename);
6 7
 
@@ -9,7 +10,7 @@ const logger = getLogger(__filename);
9 10
  * have different implementations depending on the underlying interface used
10 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 15
     /* eslint-disable max-params */
15 16
 
@@ -34,6 +35,7 @@ export default class JingleSession {
34 35
             mediaConstraints,
35 36
             iceConfig,
36 37
             isInitiator) {
38
+        super();
37 39
         this.sid = sid;
38 40
         this.localJid = localJid;
39 41
         this.remoteJid = remoteJid;

+ 99
- 16
modules/xmpp/JingleSessionPC.js Просмотреть файл

@@ -11,6 +11,7 @@ import { integerHash } from '../util/StringUtils';
11 11
 import browser from './../browser';
12 12
 import JingleSession from './JingleSession';
13 13
 import * as JingleSessionState from './JingleSessionState';
14
+import MediaSessionEvents from './MediaSessionEvents';
14 15
 import SDP from './SDP';
15 16
 import SDPDiffer from './SDPDiffer';
16 17
 import SDPUtil from './SDPUtil';
@@ -90,6 +91,18 @@ export default class JingleSessionPC extends JingleSession {
90 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 106
     /* eslint-disable max-params */
94 107
 
95 108
     /**
@@ -173,6 +186,13 @@ export default class JingleSessionPC extends JingleSession {
173 186
          */
174 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 197
          * Indicates whether or not this session is willing to send/receive
178 198
          * video media. When set to <tt>false</tt> the underlying peer
@@ -221,6 +241,13 @@ export default class JingleSessionPC extends JingleSession {
221 241
          */
222 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 252
          * The signaling layer implementation.
226 253
          * @type {SignalingLayerImpl}
@@ -566,6 +593,17 @@ export default class JingleSessionPC extends JingleSession {
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 608
      * Sends given candidate in Jingle 'transport-info' message.
571 609
      * @param {RTCIceCandidate} candidate the WebRTC ICE candidate instance
@@ -1028,7 +1066,7 @@ export default class JingleSessionPC extends JingleSession {
1028 1066
                     if (this.state === JingleSessionState.PENDING) {
1029 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 1070
                         // the initial O/A cycle. We want to adjust the video
1033 1071
                         // media direction only in the local SDP and the Jingle
1034 1072
                         // contents direction included in the initial
@@ -1039,8 +1077,11 @@ export default class JingleSessionPC extends JingleSession {
1039 1077
                         // Changing media direction in the remote SDP will mess
1040 1078
                         // up our SDP translation chain (simulcast, video mute,
1041 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,17 +1256,14 @@ export default class JingleSessionPC extends JingleSession {
1215 1256
 
1216 1257
     /**
1217 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 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 1267
             = $iq({
1230 1268
                 to: this.remoteJid,
1231 1269
                 type: 'set'
@@ -1238,11 +1276,16 @@ export default class JingleSessionPC extends JingleSession {
1238 1276
                 })
1239 1277
                 .c('content', {
1240 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 1290
         this.connection.sendIQ(
1248 1291
             sessionModify,
@@ -1251,6 +1294,28 @@ export default class JingleSessionPC extends JingleSession {
1251 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 1320
      * Sends Jingle 'transport-accept' message which is a response to
1256 1321
      * 'transport-replace'.
@@ -1336,7 +1401,13 @@ export default class JingleSessionPC extends JingleSession {
1336 1401
      * successful and rejected otherwise.
1337 1402
      */
1338 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,7 +2158,7 @@ export default class JingleSessionPC extends JingleSession {
2087 2158
                 // 'inactive' on video media in remote SDP will mess up our SDP
2088 2159
                 // translation chain (simulcast, RTX, video mute etc.).
2089 2160
                 if (this.isP2P && isSessionActive) {
2090
-                    this.sendContentModify(videoActive);
2161
+                    this.sendContentModify();
2091 2162
                 }
2092 2163
             }
2093 2164
 
@@ -2134,6 +2205,18 @@ export default class JingleSessionPC extends JingleSession {
2134 2205
     modifyContents(jingleContents) {
2135 2206
         const newVideoSenders
2136 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 2221
         if (newVideoSenders === null) {
2139 2222
             logger.error(

+ 122
- 0
modules/xmpp/JingleSessionPC.spec.js Просмотреть файл

@@ -0,0 +1,122 @@
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 Просмотреть файл

@@ -0,0 +1,6 @@
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 Просмотреть файл

@@ -0,0 +1,71 @@
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 Просмотреть файл

@@ -1,52 +1,9 @@
1
-import { default as XmppConnection } from './XmppConnection';
2 1
 import { $iq, Strophe } from 'strophe.js';
2
+
3 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 9
  * Creates any IQ.

Загрузка…
Отмена
Сохранить