소스 검색

feat(codec-selection): Use the new codec selection API (#2520)

* feat(codec-selection): Use the new codec selection API
https://github.com/w3ctag/design-reviews/issues/836. This allows the client to seamlessly switch between the codecs without having to trigger a renegotiation.
This feature is behind the flag testing.enableCodecSelectionAPI in config.js

* fix(stats): Fix local resolution stats.
The video codec for the local video sources needs to identified differently now, from the codecs field in the RTCRtpSendParameters returned by the browser. We no longer munge the remote SDP to switch to a different codec.

* feat(stats): Feed encodeTime stats for all local SSRCs to the codec selection mechanism.

* fix(codec-selection) Continue to mumge SDP for selecting H.264.

* feat(codec-selection) Make screenshare codec configurable.
If no 'screenshareCodec' is set under videoQuality or p2p settings, AV1 will be selected as default.

* squash: Address review comments

* Update modules/RTC/CodecSelection.js

Co-authored-by: Saúl Ibarra Corretgé <s@saghul.net>

* fix(codec-selection) Add codec to existing stats

---------

Co-authored-by: Saúl Ibarra Corretgé <s@saghul.net>
release-8443
Jaya Allamsetty 1 년 전
부모
커밋
6bcc577acd
No account linked to committer's email address

+ 14
- 5
JitsiConference.js 파일 보기

@@ -390,14 +390,20 @@ JitsiConference.prototype._init = function(options = {}) {
390 390
                 ? config.videoQuality.mobileCodecPreferenceOrder
391 391
                 : config.videoQuality?.codecPreferenceOrder,
392 392
             disabledCodec: _getCodecMimeType(config.videoQuality?.disabledCodec),
393
-            preferredCodec: _getCodecMimeType(config.videoQuality?.preferredCodec)
393
+            preferredCodec: _getCodecMimeType(config.videoQuality?.preferredCodec),
394
+            screenshareCodec: browser.isMobileDevice()
395
+                ? _getCodecMimeType(config.videoQuality?.mobileScreenshareCodec)
396
+                : _getCodecMimeType(config.videoQuality?.screenshareCodec)
394 397
         },
395 398
         p2p: {
396 399
             preferenceOrder: browser.isMobileDevice() && config.p2p?.mobileCodecPreferenceOrder
397 400
                 ? config.p2p.mobileCodecPreferenceOrder
398 401
                 : config.p2p?.codecPreferenceOrder,
399 402
             disabledCodec: _getCodecMimeType(config.p2p?.disabledCodec),
400
-            preferredCodec: _getCodecMimeType(config.p2p?.preferredCodec)
403
+            preferredCodec: _getCodecMimeType(config.p2p?.preferredCodec),
404
+            screenshareCodec: browser.isMobileDevice()
405
+                ? _getCodecMimeType(config.p2p?.mobileScreenshareCodec)
406
+                : _getCodecMimeType(config.p2p?.screenshareCodec)
401 407
         }
402 408
     };
403 409
 
@@ -2198,7 +2204,8 @@ JitsiConference.prototype._acceptJvbIncomingCall = function(jingleSession, jingl
2198 2204
                 ...this.options.config,
2199 2205
                 codecSettings: {
2200 2206
                     mediaType: MediaType.VIDEO,
2201
-                    codecList: this.codecSelection.getCodecPreferenceList('jvb')
2207
+                    codecList: this.codecSelection.getCodecPreferenceList('jvb'),
2208
+                    screenshareCodec: this.codecSelection.getScreenshareCodec('jvb')
2202 2209
                 },
2203 2210
                 enableInsertableStreams: this.isE2EEEnabled() || FeatureFlags.isRunInLiteModeEnabled()
2204 2211
             });
@@ -2908,7 +2915,8 @@ JitsiConference.prototype._acceptP2PIncomingCall = function(jingleSession, jingl
2908 2915
             ...this.options.config,
2909 2916
             codecSettings: {
2910 2917
                 mediaType: MediaType.VIDEO,
2911
-                codecList: this.codecSelection.getCodecPreferenceList('p2p')
2918
+                codecList: this.codecSelection.getCodecPreferenceList('p2p'),
2919
+                screenshareCodec: this.codecSelection.getScreenshareCodec('p2p')
2912 2920
             },
2913 2921
             enableInsertableStreams: this.isE2EEEnabled() || FeatureFlags.isRunInLiteModeEnabled()
2914 2922
         });
@@ -3263,7 +3271,8 @@ JitsiConference.prototype._startP2PSession = function(remoteJid) {
3263 3271
             ...this.options.config,
3264 3272
             codecSettings: {
3265 3273
                 mediaType: MediaType.VIDEO,
3266
-                codecList: this.codecSelection.getCodecPreferenceList('p2p')
3274
+                codecList: this.codecSelection.getCodecPreferenceList('p2p'),
3275
+                screenshareCodec: this.codecSelection.getScreenshareCodec('p2p')
3267 3276
             },
3268 3277
             enableInsertableStreams: this.isE2EEEnabled() || FeatureFlags.isRunInLiteModeEnabled()
3269 3278
         });

+ 5
- 0
JitsiConferenceEventManager.js 파일 보기

@@ -734,6 +734,11 @@ JitsiConferenceEventManager.prototype.setupStatisticsListeners = function() {
734 734
             JitsiConferenceEvents.BEFORE_STATISTICS_DISPOSED);
735 735
     });
736 736
 
737
+    conference.statistics.addEncodeTimeStatsListener((tpc, stats) => {
738
+        conference.eventEmitter.emit(
739
+            JitsiConferenceEvents.ENCODE_TIME_STATS_RECEIVED, tpc, stats);
740
+    });
741
+
737 742
     // if we are in startSilent mode we will not be sending/receiving so nothing to detect
738 743
     if (!conference.options.config.startSilent) {
739 744
         conference.statistics.addByteSentStatsListener((tpc, stats) => {

+ 3
- 0
JitsiConferenceEvents.spec.ts 파일 보기

@@ -27,6 +27,7 @@ describe( "/JitsiConferenceEvents members", () => {
27 27
         E2EE_VERIFICATION_AVAILABLE,
28 28
         E2EE_VERIFICATION_READY,
29 29
         E2EE_VERIFICATION_COMPLETED,
30
+        ENCODE_TIME_STATS_RECEIVED,
30 31
         ENDPOINT_MESSAGE_RECEIVED,
31 32
         ENDPOINT_STATS_RECEIVED,
32 33
         JVB121_STATUS,
@@ -166,6 +167,7 @@ describe( "/JitsiConferenceEvents members", () => {
166 167
         expect( BREAKOUT_ROOMS_MOVE_TO_ROOM ).toBe( 'conference.breakout-rooms.move-to-room' );
167 168
         expect( BREAKOUT_ROOMS_UPDATED ).toBe( 'conference.breakout-rooms.updated' );
168 169
         expect( METADATA_UPDATED ).toBe( 'conference.metadata.updated' );
170
+        expect( ENCODE_TIME_STATS_RECEIVED ).toBe( 'conference.encode_time_stats_received' );
169 171
 
170 172
         expect( JitsiConferenceEvents ).toBeDefined();
171 173
 
@@ -188,6 +190,7 @@ describe( "/JitsiConferenceEvents members", () => {
188 190
         expect( JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED ).toBe( 'conference.dominantSpeaker' );
189 191
         expect( JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP ).toBe( 'conference.createdTimestamp' );
190 192
         expect( JitsiConferenceEvents.DTMF_SUPPORT_CHANGED ).toBe( 'conference.dtmfSupportChanged' );
193
+        expect( JitsiConferenceEvents.ENCODE_TIME_STATS_RECEIVED ).toBe( 'conference.encode_time_stats_received' );
191 194
         expect( JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED ).toBe( 'conference.endpoint_message_received' );
192 195
         expect( JitsiConferenceEvents.ENDPOINT_STATS_RECEIVED ).toBe( 'conference.endpoint_stats_received' );
193 196
         expect( JitsiConferenceEvents.JVB121_STATUS ).toBe( 'conference.jvb121Status' );

+ 6
- 0
JitsiConferenceEvents.ts 파일 보기

@@ -193,6 +193,11 @@ export enum JitsiConferenceEvents {
193 193
 
194 194
     E2EE_VERIFICATION_READY = 'conference.e2ee.verification.ready',
195 195
 
196
+    /**
197
+     * Indicates that the encode time stats for the local video sources has been received.
198
+     */
199
+    ENCODE_TIME_STATS_RECEIVED = 'conference.encode_time_stats_received',
200
+
196 201
     /**
197 202
      * Indicates that a message from another participant is received on data
198 203
      * channel.
@@ -518,6 +523,7 @@ export const DTMF_SUPPORT_CHANGED = JitsiConferenceEvents.DTMF_SUPPORT_CHANGED;
518 523
 export const E2EE_VERIFICATION_AVAILABLE = JitsiConferenceEvents.E2EE_VERIFICATION_AVAILABLE;
519 524
 export const E2EE_VERIFICATION_COMPLETED = JitsiConferenceEvents.E2EE_VERIFICATION_COMPLETED;
520 525
 export const E2EE_VERIFICATION_READY = JitsiConferenceEvents.E2EE_VERIFICATION_READY;
526
+export const ENCODE_TIME_STATS_RECEIVED = JitsiConferenceEvents.ENCODE_TIME_STATS_RECEIVED;
521 527
 export const ENDPOINT_MESSAGE_RECEIVED = JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED;
522 528
 export const ENDPOINT_STATS_RECEIVED = JitsiConferenceEvents.ENDPOINT_STATS_RECEIVED;
523 529
 export const FORWARDED_SOURCES_CHANGED = JitsiConferenceEvents.FORWARDED_SOURCES_CHANGED;

+ 101
- 8
modules/RTC/CodecSelection.js 파일 보기

@@ -30,14 +30,16 @@ export class CodecSelection {
30 30
      * @param {string} options.p2p settings (codec list, preferred and disabled) for the p2p connection.
31 31
      */
32 32
     constructor(conference, options) {
33
+        this.codecPreferenceOrder = {};
33 34
         this.conference = conference;
35
+        this.encodeTimeStats = new Map();
34 36
         this.options = options;
35
-        this.codecPreferenceOrder = {};
37
+        this.screenshareCodec = {};
36 38
         this.visitorCodecs = [];
37 39
 
38 40
         for (const connectionType of Object.keys(options)) {
39 41
             // eslint-disable-next-line prefer-const
40
-            let { disabledCodec, preferredCodec, preferenceOrder } = options[connectionType];
42
+            let { disabledCodec, preferredCodec, preferenceOrder, screenshareCodec } = options[connectionType];
41 43
             const supportedCodecs = new Set(this._getSupportedVideoCodecs(connectionType));
42 44
 
43 45
             // Default preference codec order when no codec preferences are set in config.js
@@ -89,6 +91,11 @@ export class CodecSelection {
89 91
 
90 92
             logger.info(`Codec preference order for ${connectionType} connection is ${selectedOrder}`);
91 93
             this.codecPreferenceOrder[connectionType] = selectedOrder;
94
+
95
+            // Set the preferred screenshare codec.
96
+            if (screenshareCodec && supportedCodecs.has(screenshareCodec)) {
97
+                this.screenshareCodec[connectionType] = screenshareCodec;
98
+            }
92 99
         }
93 100
 
94 101
         this.conference.on(
@@ -103,6 +110,9 @@ export class CodecSelection {
103 110
         this.conference.on(
104 111
             JitsiConferenceEvents.USER_LEFT,
105 112
             () => this._selectPreferredCodec());
113
+        this.conference.on(
114
+            JitsiConferenceEvents.ENCODE_TIME_STATS_RECEIVED,
115
+            (tpc, stats) => this._processEncodeTimeStats(tpc, stats));
106 116
     }
107 117
 
108 118
     /**
@@ -127,6 +137,84 @@ export class CodecSelection {
127 137
         return supportedCodecs;
128 138
     }
129 139
 
140
+    /**
141
+     * Processes the encode time stats received for all the local video sources.
142
+     *
143
+     * @param {TraceablePeerConnection} tpc - the peerconnection for which stats were gathered.
144
+     * @param {Object} stats - the encode time stats for local video sources.
145
+     * @returns {void}
146
+     */
147
+    _processEncodeTimeStats(tpc, stats) {
148
+        const activeSession = this.conference.getActiveMediaSession();
149
+
150
+        // Process stats only for the active media session.
151
+        if (activeSession.peerconnection !== tpc) {
152
+            return;
153
+        }
154
+
155
+        const statsPerTrack = new Map();
156
+
157
+        for (const ssrc of stats.keys()) {
158
+            const { codec, encodeTime, qualityLimitationReason, resolution, timestamp } = stats.get(ssrc);
159
+            const track = tpc.getTrackBySSRC(ssrc);
160
+            let existingStats = statsPerTrack.get(track.rtcId);
161
+            const encodeResolution = Math.min(resolution.height, resolution.width);
162
+            const ssrcStats = {
163
+                encodeResolution,
164
+                encodeTime,
165
+                qualityLimitationReason
166
+            };
167
+
168
+            if (existingStats) {
169
+                existingStats.codec = codec;
170
+                existingStats.timestamp = timestamp;
171
+                existingStats.trackStats.push(ssrcStats);
172
+            } else {
173
+                existingStats = {
174
+                    codec,
175
+                    timestamp,
176
+                    trackStats: [ ssrcStats ]
177
+                };
178
+
179
+                statsPerTrack.set(track.rtcId, existingStats);
180
+            }
181
+        }
182
+
183
+        // Aggregate the stats for multiple simulcast streams with different SSRCs but for the same video stream.
184
+        for (const trackId of statsPerTrack.keys()) {
185
+            const { codec, timestamp, trackStats } = statsPerTrack.get(trackId);
186
+            const totalEncodeTime = trackStats
187
+                .map(stat => stat.encodeTime)
188
+                .reduce((totalValue, currentValue) => totalValue + currentValue, 0);
189
+            const avgEncodeTime = totalEncodeTime / trackStats.length;
190
+            const { qualityLimitationReason = 'none' }
191
+                = trackStats.find(stat => stat.qualityLimitationReason !== 'none') ?? {};
192
+            const encodeResolution = trackStats
193
+                .map(stat => stat.encodeResolution)
194
+                .reduce((resolution, currentValue) => Math.max(resolution, currentValue), 0);
195
+            const localTrack = this.conference.getLocalVideoTracks().find(t => t.rtcId === trackId);
196
+
197
+            const exisitingStats = this.encodeTimeStats.get(trackId);
198
+            const sourceStats = {
199
+                avgEncodeTime,
200
+                codec,
201
+                encodeResolution,
202
+                qualityLimitationReason,
203
+                localTrack,
204
+                timestamp
205
+            };
206
+
207
+            if (exisitingStats) {
208
+                exisitingStats.push(sourceStats);
209
+            } else {
210
+                this.encodeTimeStats.set(trackId, [ sourceStats ]);
211
+            }
212
+
213
+            logger.debug(`Encode stats for ${localTrack}: codec=${codec}, time=${avgEncodeTime},`
214
+                + `resolution=${encodeResolution}, qualityLimitationReason=${qualityLimitationReason}`);
215
+        }
216
+    }
217
+
130 218
     /**
131 219
      * Sets the codec on the media session based on the codec preference order configured in config.js and the supported
132 220
      * codecs published by the remote participants in their presence.
@@ -139,9 +227,7 @@ export class CodecSelection {
139 227
         if (!session) {
140 228
             return;
141 229
         }
142
-        const currentCodecOrder = session.peerconnection.getConfiguredVideoCodecs();
143 230
         const isJvbSession = session === this.conference.jvbJingleSession;
144
-
145 231
         let localPreferredCodecOrder = isJvbSession ? this.codecPreferenceOrder.jvb : this.codecPreferenceOrder.p2p;
146 232
 
147 233
         // E2EE is curently supported only for VP8 codec.
@@ -195,10 +281,7 @@ export class CodecSelection {
195 281
             return;
196 282
         }
197 283
 
198
-        // Reconfigure the codecs on the media session.
199
-        if (!selectedCodecOrder.every((val, index) => val === currentCodecOrder[index])) {
200
-            session.setVideoCodecs(selectedCodecOrder);
201
-        }
284
+        session.setVideoCodecs(selectedCodecOrder);
202 285
     }
203 286
 
204 287
     /**
@@ -225,4 +308,14 @@ export class CodecSelection {
225 308
     getCodecPreferenceList(connectionType) {
226 309
         return this.codecPreferenceOrder[connectionType];
227 310
     }
311
+
312
+    /**
313
+     * Returns the preferred screenshare codec for the given connection type.
314
+     *
315
+     * @param {String} connectionType The media connection type, 'p2p' or 'jvb'.
316
+     * @returns CodecMimeType
317
+     */
318
+    getScreenshareCodec(connectionType) {
319
+        return this.screenshareCodec[connectionType];
320
+    }
228 321
 }

+ 6
- 6
modules/RTC/CodecSelection.spec.js 파일 보기

@@ -135,7 +135,7 @@ describe('Codec Selection', () => {
135 135
             participant1 = new MockParticipant('remote-1');
136 136
             conference.addParticipant(participant1, [ 'vp9', 'vp8' ]);
137 137
 
138
-            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(0);
138
+            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
139 139
 
140 140
             // Add a third user joining the call with a subset of codecs.
141 141
             participant2 = new MockParticipant('remote-2');
@@ -145,7 +145,7 @@ describe('Codec Selection', () => {
145 145
 
146 146
             // Make p2 leave the call
147 147
             conference.removeParticipant(participant2);
148
-            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
148
+            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(3);
149 149
         });
150 150
 
151 151
         it('and remote endpoints use the old codec selection logic (RN)', () => {
@@ -163,7 +163,7 @@ describe('Codec Selection', () => {
163 163
 
164 164
             // Make p1 leave the call
165 165
             conference.removeParticipant(participant1);
166
-            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(2);
166
+            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(3);
167 167
         });
168 168
     });
169 169
 
@@ -185,7 +185,7 @@ describe('Codec Selection', () => {
185 185
             participant1 = new MockParticipant('remote-1');
186 186
             conference.addParticipant(participant1, [ 'vp9', 'vp8', 'h264' ]);
187 187
 
188
-            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(0);
188
+            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
189 189
 
190 190
             // Add a third user joining the call with a subset of codecs.
191 191
             participant2 = new MockParticipant('remote-2');
@@ -195,7 +195,7 @@ describe('Codec Selection', () => {
195 195
 
196 196
             // Make p2 leave the call
197 197
             conference.removeParticipant(participant2);
198
-            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
198
+            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(3);
199 199
         });
200 200
 
201 201
         it('and remote endpoint prefers a codec that is locally disabled', () => {
@@ -221,7 +221,7 @@ describe('Codec Selection', () => {
221 221
 
222 222
             // Make p1 leave the call
223 223
             conference.removeParticipant(participant1);
224
-            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(2);
224
+            expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(3);
225 225
         });
226 226
     });
227 227
 });

+ 94
- 12
modules/RTC/TraceablePeerConnection.js 파일 보기

@@ -265,6 +265,12 @@ export default function TraceablePeerConnection(
265 265
     browser.supportsCodecPreferences()
266 266
         && logger.info('Using RTCRtpTransceiver#setCodecPreferences for codec selection');
267 267
 
268
+    /**
269
+     * Flag used to indicate if the codecs are configured using the codec selection API without having the need to
270
+     * trigger a renegotiation for the change to be effective.
271
+     */
272
+    this._usesCodecSelectionAPI = this.options.usesCodecSelectionAPI;
273
+
268 274
     /**
269 275
      * Indicates whether an audio track has ever been added to the peer connection.
270 276
      */
@@ -807,8 +813,8 @@ TraceablePeerConnection.prototype.getTargetVideoBitrates = function() {
807 813
 };
808 814
 
809 815
 /**
810
- * Tries to find {@link JitsiTrack} for given SSRC number. It will search both
811
- * local and remote tracks bound to this instance.
816
+ * Tries to find {@link JitsiTrack} for given SSRC number. It will search both local and remote tracks bound to this
817
+ * instance.
812 818
  * @param {number} ssrc
813 819
  * @return {JitsiTrack|null}
814 820
  */
@@ -817,7 +823,9 @@ TraceablePeerConnection.prototype.getTrackBySSRC = function(ssrc) {
817 823
         throw new Error(`SSRC ${ssrc} is not a number`);
818 824
     }
819 825
     for (const localTrack of this.localTracks.values()) {
820
-        if (this.getLocalSSRC(localTrack) === ssrc) {
826
+        const { ssrcs } = this.localSSRCs.get(localTrack.rtcId);
827
+
828
+        if (ssrcs.find(localSsrc => Number(localSsrc) === ssrc)) {
821 829
             return localTrack;
822 830
         }
823 831
     }
@@ -1421,8 +1429,10 @@ TraceablePeerConnection.prototype._mungeCodecOrder = function(description) {
1421 1429
         }
1422 1430
 
1423 1431
         // Reorder the codecs based on the preferred settings.
1424
-        for (const codec of this.codecSettings.codecList.slice().reverse()) {
1425
-            SDPUtil.preferCodec(mLine, codec, this.isP2P);
1432
+        if (!this.usesCodecSelectionAPI()) {
1433
+            for (const codec of this.codecSettings.codecList.slice().reverse()) {
1434
+                SDPUtil.preferCodec(mLine, codec, this.isP2P);
1435
+            }
1426 1436
         }
1427 1437
     }
1428 1438
 
@@ -1593,6 +1603,15 @@ TraceablePeerConnection.prototype._assertTrackBelongs = function(
1593 1603
  * video in the local SDP.
1594 1604
  */
1595 1605
 TraceablePeerConnection.prototype.getConfiguredVideoCodec = function() {
1606
+    const localVideoTrack = this.getLocalVideoTracks()[0];
1607
+
1608
+    if (this.usesCodecSelectionAPI() && localVideoTrack) {
1609
+        const rtpSender = this.findSenderForTrack(localVideoTrack.getTrack());
1610
+        const { codecs } = rtpSender.getParameters();
1611
+
1612
+        return codecs[0].mimeType.split('/')[1].toLowerCase();
1613
+    }
1614
+
1596 1615
     const sdp = this.peerconnection.remoteDescription?.sdp;
1597 1616
     const defaultCodec = CodecMimeType.VP8;
1598 1617
 
@@ -1673,6 +1692,10 @@ TraceablePeerConnection.prototype.setVideoCodecs = function(codecList) {
1673 1692
     }
1674 1693
 
1675 1694
     this.codecSettings.codecList = codecList;
1695
+
1696
+    if (this.usesCodecSelectionAPI()) {
1697
+        this.configureVideoSenderEncodings();
1698
+    }
1676 1699
 };
1677 1700
 
1678 1701
 /**
@@ -1863,6 +1886,18 @@ TraceablePeerConnection.prototype.removeTrackFromPc = function(localTrack) {
1863 1886
     return this.tpcUtils.replaceTrack(localTrack, null).then(() => false);
1864 1887
 };
1865 1888
 
1889
+/**
1890
+ * Returns true if the codec selection API is used for switching between codecs for the video sources.
1891
+ *
1892
+ * @returns {boolean}
1893
+ */
1894
+TraceablePeerConnection.prototype.usesCodecSelectionAPI = function() {
1895
+    // Browser throws an error when H.264 is set on the encodings. Therefore, munge the SDP when H.264 needs to be
1896
+    // selected.
1897
+    // TODO: Remove this check when the above issue is fixed.
1898
+    return this._usesCodecSelectionAPI && this.codecSettings.codecList[0] !== CodecMimeType.H264;
1899
+};
1900
+
1866 1901
 TraceablePeerConnection.prototype.createDataChannel = function(label, opts) {
1867 1902
     this.trace('createDataChannel', label, opts);
1868 1903
 
@@ -1914,6 +1949,33 @@ TraceablePeerConnection.prototype._adjustRemoteMediaDirection = function(remoteD
1914 1949
     });
1915 1950
 };
1916 1951
 
1952
+/**
1953
+ * Returns the codec to be used for screenshare based on the supported codecs and the preferred codec requested
1954
+ * through config.js setting.
1955
+ *
1956
+ * @param {CodecMimeType} defaultCodec - the preferred codec for video tracks.
1957
+ * @returns {CodecMimeType}
1958
+ */
1959
+TraceablePeerConnection.prototype._getPreferredCodecForScreenshare = function(defaultCodec) {
1960
+    // Use the same codec for both camera and screenshare if the client doesn't support the codec selection API.
1961
+    if (!this.usesCodecSelectionAPI()) {
1962
+        return defaultCodec;
1963
+    }
1964
+
1965
+    const { screenshareCodec } = this.codecSettings;
1966
+
1967
+    if (screenshareCodec && this.codecSettings.codecList.find(c => c === screenshareCodec)) {
1968
+        return screenshareCodec;
1969
+    }
1970
+
1971
+    // Default to AV1 for screenshare if its supported and is not overriden through config.js.
1972
+    if (this.codecSettings.codecList.find(c => c === CodecMimeType.AV1)) {
1973
+        return CodecMimeType.AV1;
1974
+    }
1975
+
1976
+    return defaultCodec;
1977
+};
1978
+
1917 1979
 /**
1918 1980
  * Munges the stereo flag as well as the opusMaxAverageBitrate in the SDP, based
1919 1981
  * on values set through config.js, if present.
@@ -2121,17 +2183,20 @@ TraceablePeerConnection.prototype.configureAudioSenderEncodings = function(local
2121 2183
  * @returns {Promise} promise that will be resolved when the operation is successful and rejected otherwise.
2122 2184
  */
2123 2185
 TraceablePeerConnection.prototype.configureVideoSenderEncodings = function(localVideoTrack = null) {
2186
+    const preferredCodec = this.codecSettings.codecList[0];
2187
+
2124 2188
     if (localVideoTrack) {
2125 2189
         return this.setSenderVideoConstraints(
2126 2190
             this._senderMaxHeights.get(localVideoTrack.getSourceName()),
2127
-            localVideoTrack);
2191
+            localVideoTrack,
2192
+            preferredCodec);
2128 2193
     }
2129 2194
     const promises = [];
2130 2195
 
2131 2196
     for (const track of this.getLocalVideoTracks()) {
2132 2197
         const maxHeight = this._senderMaxHeights.get(track.getSourceName()) ?? VIDEO_QUALITY_LEVELS[0].height;
2133 2198
 
2134
-        promises.push(this.setSenderVideoConstraints(maxHeight, track));
2199
+        promises.push(this.setSenderVideoConstraints(maxHeight, track, preferredCodec));
2135 2200
     }
2136 2201
 
2137 2202
     return Promise.allSettled(promises);
@@ -2230,9 +2295,10 @@ TraceablePeerConnection.prototype.setRemoteDescription = function(description) {
2230 2295
  *
2231 2296
  * @param {number} frameHeight - The max frame height to be imposed on the outgoing video stream.
2232 2297
  * @param {JitsiLocalTrack} - The local track for which the sender constraints have to be applied.
2298
+ * @param {preferredCodec} - The video codec that needs to be configured on the sender associated with the video source.
2233 2299
  * @returns {Promise} promise that will be resolved when the operation is successful and rejected otherwise.
2234 2300
  */
2235
-TraceablePeerConnection.prototype.setSenderVideoConstraints = function(frameHeight, localVideoTrack) {
2301
+TraceablePeerConnection.prototype.setSenderVideoConstraints = function(frameHeight, localVideoTrack, preferredCodec) {
2236 2302
     if (frameHeight < 0 || isNaN(frameHeight)) {
2237 2303
         throw new Error(`Invalid frameHeight: ${frameHeight}`);
2238 2304
     }
@@ -2248,8 +2314,10 @@ TraceablePeerConnection.prototype.setSenderVideoConstraints = function(frameHeig
2248 2314
         return Promise.resolve();
2249 2315
     }
2250 2316
 
2317
+    const codec = preferredCodec ?? this.codecSettings.codecList[0];
2318
+
2251 2319
     return this._updateVideoSenderParameters(
2252
-        () => this._updateVideoSenderEncodings(frameHeight, localVideoTrack));
2320
+        () => this._updateVideoSenderEncodings(frameHeight, localVideoTrack, codec));
2253 2321
 };
2254 2322
 
2255 2323
 /**
@@ -2274,10 +2342,12 @@ TraceablePeerConnection.prototype._updateVideoSenderParameters = function(nextFu
2274 2342
  *
2275 2343
  * @param {number} frameHeight - The max frame height to be imposed on the outgoing video stream.
2276 2344
  * @param {JitsiLocalTrack} - The local track for which the sender constraints have to be applied.
2345
+ * @param {preferredCodec} - The video codec that needs to be configured on the sender associated with the video source.
2277 2346
  * @returns {Promise} promise that will be resolved when the operation is successful and rejected otherwise.
2278 2347
  */
2279
-TraceablePeerConnection.prototype._updateVideoSenderEncodings = function(frameHeight, localVideoTrack) {
2348
+TraceablePeerConnection.prototype._updateVideoSenderEncodings = function(frameHeight, localVideoTrack, preferredCodec) {
2280 2349
     const videoSender = this.findSenderForTrack(localVideoTrack.getTrack());
2350
+    const isScreensharingTrack = localVideoTrack.getVideoType() === VideoType.DESKTOP;
2281 2351
 
2282 2352
     if (!videoSender) {
2283 2353
         return Promise.resolve();
@@ -2288,7 +2358,7 @@ TraceablePeerConnection.prototype._updateVideoSenderEncodings = function(frameHe
2288 2358
         return Promise.resolve();
2289 2359
     }
2290 2360
 
2291
-    const isSharingLowFpsScreen = localVideoTrack.getVideoType() === VideoType.DESKTOP && this._capScreenshareBitrate;
2361
+    const isSharingLowFpsScreen = isScreensharingTrack && this._capScreenshareBitrate;
2292 2362
 
2293 2363
     // Set the degradation preference.
2294 2364
     const preference = isSharingLowFpsScreen
@@ -2298,7 +2368,8 @@ TraceablePeerConnection.prototype._updateVideoSenderEncodings = function(frameHe
2298 2368
     parameters.degradationPreference = preference;
2299 2369
 
2300 2370
     // Calculate the encodings active state based on the resolution requested by the bridge.
2301
-    const codec = this.getConfiguredVideoCodec();
2371
+    const codecForCamera = preferredCodec ?? this.getConfiguredVideoCodec(localVideoTrack);
2372
+    const codec = isScreensharingTrack ? this._getPreferredCodecForScreenshare(codecForCamera) : codecForCamera;
2302 2373
     const activeState = this.tpcUtils.calculateEncodingsActiveState(localVideoTrack, codec, frameHeight);
2303 2374
     let bitrates = this.tpcUtils.calculateEncodingsBitrates(localVideoTrack, codec, frameHeight);
2304 2375
     const scalabilityModes = this.tpcUtils.calculateEncodingsScalabilityMode(localVideoTrack, codec, frameHeight);
@@ -2321,6 +2392,7 @@ TraceablePeerConnection.prototype._updateVideoSenderEncodings = function(frameHe
2321 2392
         if (parameters.encodings.hasOwnProperty(idx)) {
2322 2393
             const {
2323 2394
                 active = undefined,
2395
+                codec: currentCodec = undefined,
2324 2396
                 maxBitrate = undefined,
2325 2397
                 scalabilityMode = undefined,
2326 2398
                 scaleResolutionDownBy = undefined
@@ -2353,6 +2425,16 @@ TraceablePeerConnection.prototype._updateVideoSenderEncodings = function(frameHe
2353 2425
             } else {
2354 2426
                 parameters.encodings[idx].scalabilityMode = undefined;
2355 2427
             }
2428
+
2429
+            const expectedPattern = `${MediaType.VIDEO}/${codec.toUpperCase()}`;
2430
+
2431
+            // Configure the codec here if its supported.
2432
+            if (this.usesCodecSelectionAPI() && currentCodec?.mimeType !== expectedPattern) {
2433
+                const matchingCodec = parameters.codecs.find(pt => pt.mimeType === expectedPattern);
2434
+
2435
+                parameters.encodings[idx].codec = matchingCodec;
2436
+                needsUpdate = true;
2437
+            }
2356 2438
         }
2357 2439
     }
2358 2440
 

+ 12
- 0
modules/browser/BrowserCapabilities.js 파일 보기

@@ -140,6 +140,18 @@ export default class BrowserCapabilities extends BrowserDetection {
140 140
             && !this.isWebKitBased();
141 141
     }
142 142
 
143
+    /**
144
+     * Checks if the browser supports the new codec selection API, i.e., checks if dictionary member
145
+     * RTCRtpEncodingParameters.codec as defined in
146
+     * https://w3c.github.io/webrtc-extensions/#dom-rtcrtpencodingparameters-codec is supported by the browser. It
147
+     * allows the application to change the current codec used by each RTCRtpSender without a renegotiation.
148
+     *
149
+     * @returns {boolean}
150
+     */
151
+    supportsCodecSelectionAPI() {
152
+        return this.isChromiumBased() && this.isEngineVersionGreaterThan(125);
153
+    }
154
+
143 155
     /**
144 156
      * Returns true if the browser supports Dependency Descriptor header extension.
145 157
      *

+ 37
- 3
modules/statistics/RTPStatsCollector.js 파일 보기

@@ -84,6 +84,10 @@ SsrcStats.prototype.setCodec = function(codec) {
84 84
     this.codec = codec || '';
85 85
 };
86 86
 
87
+SsrcStats.prototype.setEncodeStats = function(encodeStats) {
88
+    this.encodeStats = encodeStats || {};
89
+};
90
+
87 91
 /**
88 92
  *
89 93
  */
@@ -493,6 +497,7 @@ StatsCollector.prototype._calculateFps = function(now, before, fieldName) {
493 497
  */
494 498
 StatsCollector.prototype.processStatsReport = function() {
495 499
     const byteSentStats = {};
500
+    const encodedTimeStatsPerSsrc = new Map();
496 501
 
497 502
     this.currentStatsReport.forEach(now => {
498 503
         const before = this.previousStatsReport ? this.previousStatsReport.get(now.id) : null;
@@ -638,13 +643,38 @@ StatsCollector.prototype.processStatsReport = function() {
638 643
 
639 644
             if (codec) {
640 645
                 /**
641
-                 * The mime type has the following form: video/VP8 or audio/ISAC,
642
-                 * so we what to keep just the type after the '/', audio and video
643
-                 * keys will be added on the processing side.
646
+                 * The mime type has the following form: video/VP8 or audio/ISAC, so we what to keep just the type
647
+                 * after the '/', audio and video keys will be added on the processing side.
644 648
                  */
645 649
                 const codecShortType = codec.mimeType.split('/')[1];
646 650
 
647 651
                 codecShortType && ssrcStats.setCodec(codecShortType);
652
+
653
+                // Calculate the encodeTime stat for outbound video streams.
654
+                const track = this.peerconnection.getTrackBySSRC(ssrc);
655
+
656
+                if (now.type === 'outbound-rtp'
657
+                    && now.active
658
+                    && track?.isVideoTrack()
659
+                    && this.peerconnection.usesCodecSelectionAPI()
660
+                    && before?.totalEncodeTime
661
+                    && before?.framesEncoded
662
+                    && now.frameHeight
663
+                    && now.frameWidth) {
664
+                    const encodeTimeDelta = now.totalEncodeTime - before.totalEncodeTime;
665
+                    const framesEncodedDelta = now.framesEncoded - before.framesEncoded;
666
+                    const encodeTimePerFrameInMs = 1000 * encodeTimeDelta / framesEncodedDelta;
667
+                    const encodeTimeStats = {
668
+                        codec: codecShortType,
669
+                        encodeTime: encodeTimePerFrameInMs,
670
+                        qualityLimitationReason: now.qualityLimitationReason,
671
+                        resolution,
672
+                        timestamp: now.timestamp
673
+                    };
674
+
675
+                    encodedTimeStatsPerSsrc.set(ssrc, encodeTimeStats);
676
+                    ssrcStats.setEncodeStats(encodedTimeStatsPerSsrc);
677
+                }
648 678
             }
649 679
 
650 680
         // Continue to use the 'track' based stats for Firefox and Safari and older versions of Chromium.
@@ -692,5 +722,9 @@ StatsCollector.prototype.processStatsReport = function() {
692 722
         this.eventEmitter.emit(StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
693 723
     }
694 724
 
725
+    if (encodedTimeStatsPerSsrc.size) {
726
+        this.eventEmitter.emit(StatisticsEvents.ENCODE_TIME_STATS, this.peerconnection, encodedTimeStatsPerSsrc);
727
+    }
728
+
695 729
     this._processAndEmitReport();
696 730
 };

+ 8
- 0
modules/statistics/statistics.js 파일 보기

@@ -203,6 +203,14 @@ Statistics.prototype.removeConnectionStatsListener = function(listener) {
203 203
         listener);
204 204
 };
205 205
 
206
+Statistics.prototype.addEncodeTimeStatsListener = function(listener) {
207
+    this.eventEmitter.on(StatisticsEvents.ENCODE_TIME_STATS, listener);
208
+};
209
+
210
+Statistics.prototype.removeEncodeTimeStatsListener = function(listener) {
211
+    this.eventEmitter.removeListener(StatisticsEvents.ENCODE_TIME_STATS, listener);
212
+};
213
+
206 214
 Statistics.prototype.addByteSentStatsListener = function(listener) {
207 215
     this.eventEmitter.on(StatisticsEvents.BYTE_SENT_STATS, listener);
208 216
 };

+ 19
- 2
modules/xmpp/JingleSessionPC.js 파일 보기

@@ -3,6 +3,7 @@ import $ from 'jquery';
3 3
 import { $build, $iq, Strophe } from 'strophe.js';
4 4
 
5 5
 import { JitsiTrackEvents } from '../../JitsiTrackEvents';
6
+import { CodecMimeType } from '../../service/RTC/CodecMimeType';
6 7
 import { MediaDirection } from '../../service/RTC/MediaDirection';
7 8
 import { MediaType } from '../../service/RTC/MediaType';
8 9
 import {
@@ -399,6 +400,8 @@ export default class JingleSessionPC extends JingleSession {
399 400
         pcOptions.capScreenshareBitrate = false;
400 401
         pcOptions.codecSettings = options.codecSettings;
401 402
         pcOptions.enableInsertableStreams = options.enableInsertableStreams;
403
+        pcOptions.usesCodecSelectionAPI = this.usesCodecSelectionAPI
404
+            = browser.supportsCodecSelectionAPI() && options.testing?.enableCodecSelectionAPI;
402 405
 
403 406
         if (options.videoQuality) {
404 407
             const settings = Object.entries(options.videoQuality)
@@ -1174,14 +1177,28 @@ export default class JingleSessionPC extends JingleSession {
1174 1177
      * Updates the codecs on the peerconnection and initiates a renegotiation for the
1175 1178
      * new codec config to take effect.
1176 1179
      *
1177
-     * @param {CodecMimeType} preferred the preferred codec.
1178
-     * @param {CodecMimeType} disabled the codec that needs to be disabled.
1180
+     * @param {Array<CodecMimeType>} codecList the preferred codecs.
1179 1181
      */
1180 1182
     setVideoCodecs(codecList) {
1183
+
1181 1184
         if (this._assertNotEnded()) {
1182 1185
             logger.info(`${this} setVideoCodecs: ${codecList}`);
1183 1186
             this.peerconnection.setVideoCodecs(codecList);
1184 1187
 
1188
+            // Browser throws an error when H.264 is set on the encodings. Therefore, munge the SDP when H.264 needs to
1189
+            // be selected.
1190
+            // TODO: Remove this check when the above issue is fixed.
1191
+            if (this.usesCodecSelectionAPI && codecList[0] !== CodecMimeType.H264) {
1192
+                return;
1193
+            }
1194
+
1195
+            // Skip renegotiation when the selected codec order matches with that of the remote SDP.
1196
+            const currentCodecOrder = this.peerconnection.getConfiguredVideoCodecs();
1197
+
1198
+            if (codecList.every((val, index) => val === currentCodecOrder[index])) {
1199
+                return;
1200
+            }
1201
+
1185 1202
             // Initiate a renegotiate for the codec setting to take effect.
1186 1203
             const workFunction = finishedCallback => {
1187 1204
                 this._renegotiate()

+ 3
- 0
service/statistics/Events.spec.ts 파일 보기

@@ -8,6 +8,7 @@ describe( "/service/statistics/Events members", () => {
8 8
         BEFORE_DISPOSED,
9 9
         BYTE_SENT_STATS,
10 10
         CONNECTION_STATS,
11
+        ENCODE_TIME_STATS,
11 12
         LONG_TASKS_STATS,
12 13
         Events,
13 14
         ...others
@@ -18,6 +19,7 @@ describe( "/service/statistics/Events members", () => {
18 19
         expect( BEFORE_DISPOSED ).toBe( 'statistics.before_disposed' );
19 20
         expect( BYTE_SENT_STATS ).toBe( 'statistics.byte_sent_stats' );
20 21
         expect( CONNECTION_STATS ).toBe( 'statistics.connectionstats' );
22
+        expect( ENCODE_TIME_STATS ).toBe( 'statistics.encode_time_stats' );
21 23
         expect( LONG_TASKS_STATS ).toBe( 'statistics.long_tasks_stats' );
22 24
 
23 25
         expect( Events ).toBeDefined();
@@ -26,6 +28,7 @@ describe( "/service/statistics/Events members", () => {
26 28
         expect( Events.BEFORE_DISPOSED ).toBe( 'statistics.before_disposed' );
27 29
         expect( Events.BYTE_SENT_STATS ).toBe( 'statistics.byte_sent_stats' );
28 30
         expect( Events.CONNECTION_STATS ).toBe( 'statistics.connectionstats' );
31
+        expect( Events.ENCODE_TIME_STATS ).toBe( 'statistics.encode_time_stats' );
29 32
         expect( Events.LONG_TASKS_STATS ).toBe( 'statistics.long_tasks_stats' );
30 33
     } );
31 34
 

+ 6
- 0
service/statistics/Events.ts 파일 보기

@@ -31,6 +31,11 @@ export enum Events {
31 31
      */
32 32
     CONNECTION_STATS = 'statistics.connectionstats',
33 33
 
34
+    /**
35
+     * An event carrying the encode time stats for all the local video sources.
36
+     */
37
+    ENCODE_TIME_STATS = 'statistics.encode_time_stats',
38
+
34 39
     /**
35 40
      * An event carrying performance stats.
36 41
      */
@@ -42,4 +47,5 @@ export const AUDIO_LEVEL = Events.AUDIO_LEVEL;
42 47
 export const BEFORE_DISPOSED = Events.BEFORE_DISPOSED;
43 48
 export const BYTE_SENT_STATS = Events.BYTE_SENT_STATS;
44 49
 export const CONNECTION_STATS = Events.CONNECTION_STATS;
50
+export const ENCODE_TIME_STATS = Events.ENCODE_TIME_STATS;
45 51
 export const LONG_TASKS_STATS = Events.LONG_TASKS_STATS;

Loading…
취소
저장