Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jitsi/lib-jitsi-meet
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1855.0.0+3170a5b6
Choose a base ref
...
head repository: jitsi/lib-jitsi-meet
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref

Commits on Aug 23, 2024

  1. fix(JitsiConference) handle repeaded calls to leave gracefully

    There is no hardm in calling it twice since we protect against it.
    saghul committed Aug 23, 2024
    Copy the full SHA
    61a2efe View commit details

Commits on Aug 27, 2024

  1. fix(ResumeTask) reset resume timeout after running timer

    It will avoid having an extra "Canceling connection resume task" log
    line every time we schedule a timer.
    saghul committed Aug 27, 2024
    Copy the full SHA
    09333fb View commit details

Commits on Sep 4, 2024

  1. Copy the full SHA
    4116614 View commit details
  2. fix(xmpp): Fixes skipping messages when disco-info is processed late.

    When using bosh and xmpp conference-request (no jiconop on server-side) we can drop av moderation and speaker-stats messages.
    damencho committed Sep 4, 2024
    Copy the full SHA
    6771b69 View commit details
  3. fix(quality-control) Check only for cpu limitation

    Checking for send resolution vs expected resolution was unnecessarily limiting lastN and receive resolution when local camera has issues and stops sending video. Also, do not send redundant receiver constraints on the bridge channel
    jallamsetty1 committed Sep 4, 2024
    Copy the full SHA
    9ff77a9 View commit details
  4. fix(metadata): Allows non moderators to send values.

    Leaves up to the backend to process or not process them.
    damencho committed Sep 4, 2024
    Copy the full SHA
    776819a View commit details

Commits on Sep 6, 2024

  1. fix(quality) Do not set b:AS line in SDP for SVC codecs when codec se…

    …lection API is used.
    
    This was needed in older versions since the browser didn't apply maxBitrates from RTCRtpEncoderParameters on the encoder. In the newer versions this seems to be no longer the case. Also, when the codec selection API is used, we no longer renegotiate locally so if we switched codec from AV1->VP9-VP9, the AV1 bitrate setting in the SDP will still be effective resulting in a lower send resolution because of b/w limitation.
    jallamsetty1 committed Sep 6, 2024
    Copy the full SHA
    9c4db25 View commit details

Commits on Sep 10, 2024

  1. Copy the full SHA
    95e160b View commit details

Commits on Sep 12, 2024

  1. fix(deps) drop unused promise.allsettled dependency

    It's not imported, so it wasn't being bundled.
    saghul committed Sep 12, 2024
    Copy the full SHA
    5c74c39 View commit details

Commits on Sep 13, 2024

  1. fix(quality) Do not force desktop codecs on mobile.

    If the mobileCodecPreferenceOrder setting is missing, use the default order for mobile. Fixes an issue where mobile endpoints encodes using AV1 when mobile settings are missing.
    jallamsetty1 authored and saghul committed Sep 13, 2024
    Copy the full SHA
    cf14a33 View commit details
  2. Copy the full SHA
    1647ae1 View commit details

Commits on Sep 18, 2024

  1. Copy the full SHA
    5cc01b4 View commit details
  2. ref(SDP) Convert to ES6 class. (#2572)

    * ref(SDP) Convert to ES6 class.
    
    * ref(SDP) Use enum for URNs associated with XMPP extensions.
    
    * ref(XMPP) Replace more XEP URN constants with enum.
    
    * fix(RTC) Fixes mediaType lookup based on source-name.
    jallamsetty1 authored Sep 18, 2024
    Copy the full SHA
    ef92c2a View commit details

Commits on Sep 20, 2024

  1. fix(logging): Set default logging level as early as possible.

    We were setting the logging level in JitsiMeetJS.init which potentially would have overriden all external calls for setting the log level before the JitsiMeetJS.init is called. In fact this was happening for jitsi-meet.
    hristoterezov authored and saghul committed Sep 20, 2024
    Copy the full SHA
    604acf0 View commit details
  2. Copy the full SHA
    83ba892 View commit details
  3. Copy the full SHA
    5671c5d View commit details

Commits on Sep 24, 2024

  1. Copy the full SHA
    4be1919 View commit details

Commits on Sep 25, 2024

  1. fix(xmppConnection) Fix unit tests.

    The number of retries is limited to 3 now.
    jallamsetty1 committed Sep 25, 2024
    Copy the full SHA
    71f572c View commit details

Commits on Oct 2, 2024

  1. fix(transcriptions): Fixes transcription status, going offline. (#2581)

    * fix(transcriptions): Fixes transcription status, going offline.
    
    * squash: Fixes undefined error.
    damencho authored Oct 2, 2024
    Copy the full SHA
    8940b5c View commit details

Commits on Oct 3, 2024

  1. Copy the full SHA
    dbb76bf View commit details

Commits on Oct 4, 2024

  1. fix(TPC) Do not call setCodecPreferences on Firefox.

    Calling this API on Firefox is causing freezes when the local endpoint is the answerer. https://bugzilla.mozilla.org/show_bug.cgi?id=1917800
    jallamsetty1 committed Oct 4, 2024
    Copy the full SHA
    7dfad4f View commit details

Commits on Oct 8, 2024

  1. Copy the full SHA
    db2edba View commit details

Commits on Oct 11, 2024

  1. Copy the full SHA
    fac989a View commit details

Commits on Oct 22, 2024

  1. fix(ChatRoom) refactor handling of participant properties

    Avoid duplicates.
    saghul committed Oct 22, 2024
    Copy the full SHA
    6782e6c View commit details
  2. feat(JitsiParticipant) use a Map for properties, rather than an object

    See https://www.zhenghao.io/posts/object-vs-map
    
    Since these objects will change with reasonable requency, we'll create
    many shapes and consume more memory than a Map.
    saghul committed Oct 22, 2024
    Copy the full SHA
    a72936d View commit details
  3. feat(SDP) Convert SDP->Jingle directly w/o sdp-interop layer.

    * ref(SDPDiffer) Convert to ES6 class.
    Make it work directly with unified plan SDP that has multiple m-lines and add more unit tests.
    
    * ref(xmpp) translate unified-plan SDP->Jingle directly.
    Without having to run it through the SDPInterop.toPlanB cycle.
    
    * ref(SDP) Always generate the MSID for signaling it to Jicofo.
    
    * fix(SDPDiffer) Check explicitly for ssrc changes
    
    * fix(SDP): Fix comments and cleanup.
    Remove LOCAL_TRACK_SSRC_UPDATED event as the application ignores the event and no additional action needs to be taken when that event is fired.
    
    * ref(SDP) Add a helper function for parsing the 'a=ssrc-group' line.
    
    * squash: Address review comments
    jallamsetty1 authored Oct 22, 2024
    Copy the full SHA
    a7476b1 View commit details
  4. Copy the full SHA
    2605db8 View commit details
  5. ref(JingleSession) Update JSDocs.

    Remove unused method getRemoteRecvMaxFrameHeight.
    jallamsetty1 committed Oct 22, 2024
    Copy the full SHA
    111bd62 View commit details
  6. Copy the full SHA
    ce3d05e View commit details

Commits on Oct 26, 2024

  1. feat(moderator): Fires new error on connection failed for conference …

    …request failures. (#2591)
    
    * feat(moderator): Fires new error on connection failed for conference request failures.
    
    * squash: Fix tests.
    damencho authored Oct 26, 2024
    Copy the full SHA
    5d53ecd View commit details

Commits on Oct 28, 2024

  1. Copy the full SHA
    793f7ce View commit details

Commits on Oct 29, 2024

  1. feat(JingleSession) Convert Jingle->SDP directly w/o interop layer. (#…

    …2590)
    
    * feat(JingleSession) Convert Jingle->SDP directly w/o interop layer.
    
    * Update modules/RTC/TraceablePeerConnection.js
    
    Co-authored-by: Saúl Ibarra Corretgé <s@saghul.net>
    
    * ref(SDP) Introduce an enum for ssrc-group semantics.
    Address review comments, fix test faiilures for ssrc-rewriting case and add unit tests.
    
    * Update modules/RTC/TraceablePeerConnection.js
    
    Co-authored-by: Saúl Ibarra Corretgé <s@saghul.net>
    
    * squash: Address review comments
    
    ---------
    
    Co-authored-by: Saúl Ibarra Corretgé <s@saghul.net>
    jallamsetty1 and saghul authored Oct 29, 2024
    Copy the full SHA
    d51e557 View commit details

Commits on Oct 30, 2024

  1. Copy the full SHA
    9eefac1 View commit details

Commits on Nov 5, 2024

  1. fix(breakout): Clear up request sent on authentication.

    If guest authenticates in meeting to become a moderator and creates a breakout room, was not inviting jicofo and fails to join breakout room.
    Fixes jitsi/jitsi-meet#15024.
    damencho committed Nov 5, 2024
    Copy the full SHA
    c280c0e View commit details
  2. fix(quality) Sends updated receiver constraints.

    Fixes a regression for JitsiConference::setReceiverVideoConstraint and JitsiConference::setAssumedBandwidthBps.
    jallamsetty1 committed Nov 5, 2024
    Copy the full SHA
    0ef8314 View commit details

Commits on Nov 14, 2024

  1. fix(TPC) Use videoType from 'source-add' for remote track creation. (#…

    …2596)
    
    * fix(TPC) Use videoType from 'source-add' for remote track creation.
    If 'source-add' for a remote video source is received before presence for that source, videoType will default to 'camera' and the client wouldn't be able to create the virtual participant tile for rendering the desktop track.
    
    * squash: Include the videoType for no SSRC-rewriting case.
    jallamsetty1 authored Nov 14, 2024
    Copy the full SHA
    bc446e9 View commit details

Commits on Nov 15, 2024

  1. Copy the full SHA
    9652999 View commit details

Commits on Nov 19, 2024

  1. Copy the full SHA
    db24997 View commit details

Commits on Nov 25, 2024

  1. Copy the full SHA
    ad5559a View commit details
  2. Copy the full SHA
    76c66eb View commit details
  3. ref(TPC) Pass bare objects to methods that take SDP instead of passin…

    …g an instance of RTCSessionDescription.
    
    The constructor for RTCSessionDescription has been deprecated - https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription/RTCSessionDescription.
    jallamsetty1 committed Nov 25, 2024
    Copy the full SHA
    60e680e View commit details
  4. Copy the full SHA
    8768906 View commit details
  5. Copy the full SHA
    07f037e View commit details
  6. Copy the full SHA
    39c4422 View commit details

Commits on Dec 4, 2024

  1. fix(TPC) Fix H.264 simulcast.

    Regression was introduced in 07f037e. Also process encodeTime stats when H.264 is selected
    jallamsetty1 committed Dec 4, 2024
    Copy the full SHA
    144b0ca View commit details

Commits on Dec 10, 2024

  1. fix(browser) detect WebRTC APIs and mark browser unsupported if not

    Some so called "security" browser extensions override WebRTC APIs so
    even if the UA string suggests the browser is supported, it won't work.
    
    Coincidentally, this should also mark Safari in Lockdown Mode as
    unsupported, since the WebRTC APIs are not available in that case.
    saghul committed Dec 10, 2024
    Copy the full SHA
    1ab4ae5 View commit details

Commits on Dec 13, 2024

  1. feat(RTC) drop PERMISSION_PROMPT_IS_SHOWN event (#2609)

    * feat(RTC) drop PERMISSION_PROMPT_IS_SHOWN event
    
    It was never accurate since browsers have changed their behavior over
    time and can be implemented in the application more reliably.
    
    * squash: Update JitsiMeetJS.ts
    
    * squash: Update hand-crafted JitsiMeetJS.d.ts
    
    ---------
    
    Co-authored-by: Дамян Минков <damencho@jitsi.org>
    saghul and damencho authored Dec 13, 2024
    Copy the full SHA
    d7dc753 View commit details

Commits on Jan 8, 2025

  1. fix(JingleSession) Do not signal recvonly SSRCs generated by Firefox.

    Fixes a regression introduced by a7476b1 where multiple source-adds are sent from Firefox client resulting in Jicofo force muting the sources.
    jallamsetty1 committed Jan 8, 2025
    Copy the full SHA
    496b64a View commit details

Commits on Jan 9, 2025

  1. Copy the full SHA
    dc8b557 View commit details
  2. Copy the full SHA
    7de7d9a View commit details
Showing with 6,196 additions and 4,907 deletions.
  1. +0 −27 .github/ISSUE_TEMPLATE/bug-report.md
  2. +75 −0 .github/ISSUE_TEMPLATE/bug.yml
  3. +59 −0 .github/ISSUE_TEMPLATE/enhancement.yml
  4. +43 −0 .github/PULL_REQUEST_TEMPLATE.md
  5. +81 −40 JitsiConference.js
  6. +3 −29 JitsiConferenceEventManager.js
  7. +6 −1 JitsiConnection.js
  8. +4 −0 JitsiConnectionErrors.spec.ts
  9. +6 −0 JitsiConnectionErrors.ts
  10. +19 −14 JitsiMeetJS.ts
  11. +4 −4 JitsiParticipant.js
  12. +3 −1 JitsiTrackError.js
  13. +3 −0 JitsiTrackErrors.spec.ts
  14. +7 −0 JitsiTrackErrors.ts
  15. +4 −4 JitsiTranscriptionStatus.spec.ts
  16. +2 −2 JitsiTranscriptionStatus.ts
  17. +13 −1 authenticateAndUpgradeRole.js
  18. +13 −0 modules/RTC/MockClasses.js
  19. +6 −10 modules/RTC/RTC.js
  20. +0 −8 modules/RTC/RTCUtils.js
  21. +40 −12 modules/RTC/ScreenObtainer.js
  22. +420 −241 modules/RTC/TPCUtils.js
  23. +37 −74 modules/RTC/TPCUtils.spec.js
  24. +451 −743 modules/RTC/TraceablePeerConnection.js
  25. +18 −7 modules/browser/BrowserCapabilities.js
  26. +17 −80 modules/connectivity/IceFailedHandling.spec.js
  27. +29 −39 modules/connectivity/{IceFailedHandling.js → IceFailedHandling.ts}
  28. +6 −3 modules/e2ee/E2EEncryption.js
  29. +2 −3 modules/proxyconnection/ProxyConnectionPC.js
  30. +1 −1 modules/proxyconnection/ProxyConnectionService.js
  31. +2 −3 modules/qualitycontrol/CodecSelection.js
  32. +93 −20 modules/qualitycontrol/CodecSelection.spec.js
  33. +2 −2 modules/qualitycontrol/QualityController.spec.js
  34. +34 −12 modules/qualitycontrol/QualityController.ts
  35. +4 −3 modules/qualitycontrol/ReceiveVideoController.js
  36. +1 −0 modules/recording/recordingConstants.js
  37. +78 −167 modules/sdp/LocalSdpMunger.js
  38. +51 −10 modules/sdp/LocalSdpMunger.spec.js
  39. +4 −3 modules/sdp/RtxModifier.js
  40. +853 −660 modules/sdp/SDP.js
  41. +1,082 −103 modules/sdp/SDP.spec.js
  42. +96 −192 modules/sdp/SDPDiffer.js
  43. +293 −35 modules/sdp/SDPDiffer.spec.js
  44. +18 −2 modules/sdp/SDPUtil.js
  45. +114 −1 modules/sdp/SampleSdpStrings.js
  46. +7 −7 modules/sdp/SdpSimulcast.ts
  47. +6 −38 modules/sdp/SdpTransformUtil.js
  48. +30 −0 modules/statistics/PreCallTest.ts
  49. +0 −1 modules/statistics/RTPStatsCollector.js
  50. +60 −32 modules/xmpp/ChatRoom.js
  51. +22 −9 modules/xmpp/JingleHelperFunctions.js
  52. +1,500 −1,852 modules/xmpp/JingleSessionPC.js
  53. +166 −6 modules/xmpp/JingleSessionPC.spec.js
  54. +9 −1 modules/xmpp/ResumeTask.js
  55. +0 −4 modules/xmpp/ResumeTask.spec.js
  56. +2 −3 modules/xmpp/RoomMetadata.ts
  57. +8 −1 modules/xmpp/XmppConnection.js
  58. +44 −18 modules/xmpp/moderator.js
  59. +2 −39 modules/xmpp/strophe.jingle.js
  60. +34 −9 modules/xmpp/xmpp.js
  61. +82 −254 package-lock.json
  62. +1 −2 package.json
  63. +6 −1 service/RTC/MediaType.ts
  64. +0 −4 service/RTC/RTCEvents.spec.ts
  65. +0 −9 service/RTC/RTCEvents.ts
  66. +11 −0 service/RTC/StandardVideoQualitySettings.ts
  67. +6 −1 service/RTC/VideoType.ts
  68. +2 −0 service/connectivity/Constants.ts
  69. +5 −6 service/statistics/AnalyticsEvents.spec.ts
  70. +16 −11 service/statistics/AnalyticsEvents.ts
  71. +0 −2 service/xmpp/XMPPEvents.spec.ts
  72. +0 −12 service/xmpp/XMPPEvents.ts
  73. +74 −0 service/xmpp/XMPPExtensioProtocols.ts
  74. +0 −1 types/hand-crafted/JitsiConference.d.ts
  75. +1 −1 types/hand-crafted/JitsiMeetJS.d.ts
  76. +2 −2 types/hand-crafted/JitsiTranscriptionStatus.d.ts
  77. +0 −1 types/hand-crafted/modules/RTC/RTC.d.ts
  78. +0 −1 types/hand-crafted/modules/RTC/RTCUtils.d.ts
  79. +0 −1 types/hand-crafted/modules/RTC/TPCUtils.d.ts
  80. +1 −1 types/hand-crafted/modules/RTC/TraceablePeerConnection.d.ts
  81. +0 −1 types/hand-crafted/modules/sdp/SDP.d.ts
  82. +0 −3 types/hand-crafted/modules/sdp/SdpTransformUtil.d.ts
  83. +0 −1 types/hand-crafted/modules/xmpp/ChatRoom.d.ts
  84. +0 −8 types/hand-crafted/modules/xmpp/JingleSessionPC.d.ts
  85. +0 −1 types/hand-crafted/service/RTC/RTCEvents.d.ts
  86. +0 −2 types/hand-crafted/service/statistics/AnalyticsEvents.d.ts
  87. +0 −2 types/hand-crafted/service/xmpp/XMPPEvents.d.ts
  88. +2 −2 webpack-shared-config.js
27 changes: 0 additions & 27 deletions .github/ISSUE_TEMPLATE/bug-report.md

This file was deleted.

75 changes: 75 additions & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: 🐛 Bug Report
description: Report bugs to fix and improve.
title: "[BUG] <description>"
labels: ["bug"]


body:
- type: textarea
id: description
attributes:
label: Describe the bug.
description: A clear and concise description of what the bug is.
validations:
required: true

- type: textarea
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true


- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots/video of the bug you faced.
validations:
required: false

- type: textarea
id: reproduce
attributes:
label: How to Reproduce
description: Attach all steps/share a github gist that can help anyone reproduce the bug.
value: |
1. I first did this
2. I then did this
3. And so on . . .
validations:
required: true

- type: dropdown
id: browser
attributes:
label: "🥦 Browser"
description: "What browser are you using?"
options:
- Google Chrome
- Brave
- Microsoft Edge
- Mozilla Firefox
- Safari
- Opera
- Other
validations:
required: false

- type: checkboxes
id: no-duplicate-issues
attributes:
label: "👀 Have you checked for similar open issues?"
options:
- label: "I checked and didn't find similar issue"
required: true

- type: checkboxes
id: check-Community-Forum
attributes:
label: "🏢 Have you checked the Community Forum?"
options:
- label: "please make sure you check https://community.jitsi.org"
required: true
59 changes: 59 additions & 0 deletions .github/ISSUE_TEMPLATE/enhancement.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: 💡 Feature request
description: Suggest an idea for improving lib-jitsi-meet
labels: ["feature-request"]

body:
- type: markdown
attributes:
value: |
Thank you for suggesting an idea to improve lib-jitsi-meet!
**Note**: The decision to implement features lies with the Jitsi team, and not all requests will be accepted.
- type: textarea
id: problem
attributes:
label: What problem are you trying to solve?
description: Explain the issue or limitation that this feature would address.
validations:
required: true

- type: textarea
id: solution
attributes:
label: What solution would you like to see?
description: Describe the behavior or feature you'd like to have.
validations:
required: true

- type: textarea
id: alternative
attributes:
label: Is there an alternative?
description: Describe any alternative solutions or features you have considered.
validations:
required: false

- type: textarea
id: implementation
attributes:
label: How could this be implemented?
description: Suggest how this feature could be designed or integrated.
validations:
required: false

- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable.
validations:
required: false

- type: checkboxes
id: check-Community-Forum
attributes:
label: "🏢 Have you checked the Community Forum?"
options:
- label: "please make sure you check https://community.jitsi.org"
required: true
43 changes: 43 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!--- Provide a general summary of your changes in the Title above -->

## Description

<!--- Describe your changes in detail -->

## Related Issue

<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->

## How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

## Screenshots or GIF (In case of UI changes):

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
121 changes: 81 additions & 40 deletions JitsiConference.js
Original file line number Diff line number Diff line change
@@ -54,18 +54,21 @@ import RTCEvents from './service/RTC/RTCEvents';
import { SignalingEvents } from './service/RTC/SignalingEvents';
import { getMediaTypeFromSourceName, getSourceNameForJitsiTrack } from './service/RTC/SignalingLayer';
import { VideoType } from './service/RTC/VideoType';
import { MAX_CONNECTION_RETRIES } from './service/connectivity/Constants';
import {
ACTION_JINGLE_RESTART,
ACTION_JINGLE_SI_RECEIVED,
ACTION_JINGLE_SI_TIMEOUT,
ACTION_JINGLE_TERMINATE,
ACTION_JVB_ICE_FAILED,
ACTION_P2P_DECLINED,
ACTION_P2P_ESTABLISHED,
ACTION_P2P_FAILED,
ACTION_P2P_SWITCH_TO_JVB,
ICE_ESTABLISHMENT_DURATION_DIFF,
createConferenceEvent,
createJingleEvent,
createJvbIceFailedEvent,
createP2PEvent
} from './service/statistics/AnalyticsEvents';
import { XMPPEvents } from './service/xmpp/XMPPEvents';
@@ -79,6 +82,11 @@ const logger = getLogger(__filename);
*/
const JINGLE_SI_TIMEOUT = 5000;

/**
* Default source language for transcribing the local participant.
*/
const DEFAULT_TRANSCRIPTION_LANGUAGE = 'en-US';

/**
* Checks if a given string is a valid video codec mime type.
*
@@ -106,8 +114,6 @@ function _getCodecMimeType(codec) {
* @param {number} [options.config.avgRtpStatsN=15] how many samples are to be
* collected by {@link AvgRTPStatsReporter}, before arithmetic mean is
* calculated and submitted to the analytics module.
* @param {boolean} [options.config.enableIceRestart=false] - enables the ICE
* restart logic.
* @param {boolean} [options.config.p2p.enabled] when set to <tt>true</tt>
* the peer to peer mode will be enabled. It means that when there are only 2
* participants in the conference an attempt to make direct connection will be
@@ -444,8 +450,8 @@ JitsiConference.prototype._init = function(options = {}) {
enableAdaptiveMode: config.videoQuality?.enableAdaptiveMode,
lastNRampupTime: config.testing?.lastNRampupTime ?? 60000,
jvb: {
preferenceOrder: browser.isMobileDevice() && config.videoQuality?.mobileCodecPreferenceOrder
? config.videoQuality.mobileCodecPreferenceOrder
preferenceOrder: browser.isMobileDevice()
? config.videoQuality?.mobileCodecPreferenceOrder
: config.videoQuality?.codecPreferenceOrder,
disabledCodec: _getCodecMimeType(config.videoQuality?.disabledCodec),
preferredCodec: _getCodecMimeType(config.videoQuality?.preferredCodec),
@@ -454,8 +460,8 @@ JitsiConference.prototype._init = function(options = {}) {
: _getCodecMimeType(config.videoQuality?.screenshareCodec)
},
p2p: {
preferenceOrder: browser.isMobileDevice() && config.p2p?.mobileCodecPreferenceOrder
? config.p2p.mobileCodecPreferenceOrder
preferenceOrder: browser.isMobileDevice()
? config.p2p?.mobileCodecPreferenceOrder
: config.p2p?.codecPreferenceOrder,
disabledCodec: _getCodecMimeType(config.p2p?.disabledCodec),
preferredCodec: _getCodecMimeType(config.p2p?.preferredCodec),
@@ -557,6 +563,8 @@ JitsiConference.prototype._init = function(options = {}) {
// creates dominant speaker detection that works only in p2p mode
this.p2pDominantSpeakerDetection = new P2PDominantSpeakerDetection(this);

// TODO: Drop this after the change to use the region from the http requests
// to prosody is propagated to majority of deployments
if (config && config.deploymentInfo && config.deploymentInfo.userRegion) {
this.setLocalParticipantProperty(
'region', config.deploymentInfo.userRegion);
@@ -569,8 +577,10 @@ JitsiConference.prototype._init = function(options = {}) {
// In case the language config is undefined or has the default value that the transcriber uses
// (in our case Jigasi uses 'en-US'), don't set the participant property in order to avoid
// needlessly polluting the presence stanza.
if (config && config.transcriptionLanguage && config.transcriptionLanguage !== 'en-US') {
this.setLocalParticipantProperty('transcription_language', config.transcriptionLanguage);
const transcriptionLanguage = config?.transcriptionLanguage ?? DEFAULT_TRANSCRIPTION_LANGUAGE;

if (transcriptionLanguage !== DEFAULT_TRANSCRIPTION_LANGUAGE) {
this.setTranscriptionLanguage(transcriptionLanguage);
}
};

@@ -671,7 +681,14 @@ JitsiConference.prototype.leave = async function(reason) {

// Leave the conference. If this.room == null we are calling second time leave().
if (!this.room) {
throw new Error('You have already left the conference');
return;
}

// let's check is this breakout
if (reason === 'switch_room' && this.getBreakoutRooms()?.isBreakoutRoom()) {
const mJid = this.getBreakoutRooms().getMainRoomJid();

this.xmpp.connection._breakoutMovingToMain = mJid;
}

const room = this.room;
@@ -1107,8 +1124,8 @@ JitsiConference.prototype.addTrack = function(track) {

// Currently, only adding multiple video streams of different video types is supported.
// TODO - remove this limitation once issues with jitsi-meet trying to add multiple camera streams is fixed.
if (mediaType === MediaType.VIDEO
&& !localTracks.find(t => t.getVideoType() === track.getVideoType())) {
if (this.options.config.testing?.allowMultipleTracks
|| (mediaType === MediaType.VIDEO && !localTracks.find(t => t.getVideoType() === track.getVideoType()))) {
const sourceName = getSourceNameForJitsiTrack(
this.myUserId(),
mediaType,
@@ -1123,7 +1140,7 @@ JitsiConference.prototype.addTrack = function(track) {
return Promise.all(addTrackPromises)
.then(() => {
this._setupNewTrack(track);
this._sendBridgeVideoTypeMessage(track);
mediaType === MediaType.VIDEO && this._sendBridgeVideoTypeMessage(track);
this._updateRoomPresence(this.getActiveMediaSession());

if (this.isMutedByFocus || this.isVideoMutedByFocus) {
@@ -2663,7 +2680,20 @@ JitsiConference.prototype.setLocalParticipantProperty = function(name, value) {
*/
JitsiConference.prototype.removeLocalParticipantProperty = function(name) {
this.removeCommand(`jitsi_participant_${name}`);
this.room.sendPresence();
if (this.room) {
this.room.sendPresence();
}
};

/**
* Sets the transcription language.
* NB: Unlike _init_ here we don't check for the default value since we want to allow
* the value to be reset.
*
* @param lang the new transcription language to be used.
*/
JitsiConference.prototype.setTranscriptionLanguage = function(lang) {
this.setLocalParticipantProperty('transcription_language', lang);
};

/**
@@ -2890,7 +2920,7 @@ JitsiConference.prototype._onIceConnectionFailed = function(session) {
reason: 'connectivity-error',
reasonDescription: 'ICE FAILED'
});
} else if (session && this.jvbJingleSession === session) {
} else if (session && this.jvbJingleSession === session && this._iceRestarts < MAX_CONNECTION_RETRIES) {
// Use an exponential backoff timer for ICE restarts.
const jitterDelay = getJitterDelay(this._iceRestarts, 1000 /* min. delay */);

@@ -2900,6 +2930,16 @@ JitsiConference.prototype._onIceConnectionFailed = function(session) {
this._delayedIceFailed.start(session);
this._iceRestarts++;
}, jitterDelay);
} else if (this.jvbJingleSession === session) {
logger.warn('ICE failed, force reloading the conference after failed attempts to re-establish ICE');
Statistics.sendAnalyticsAndLog(
createJvbIceFailedEvent(
ACTION_JVB_ICE_FAILED,
{
participantId: this.myUserId(),
userRegion: this.options.config.deploymentInfo?.userRegion
}));
this.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, JitsiConferenceErrors.ICE_FAILED);
}
};

@@ -3354,40 +3394,41 @@ JitsiConference.prototype._maybeStartOrStopP2P = function(userLeftEvent) {
// Start peer to peer session
if (!this.p2pJingleSession && shouldBeInP2P) {
const peer = peerCount && peers[0];


const myId = this.myUserId();
const peersId = peer.getId();
const jid = peer.getJid();

if (myId > peersId) {
logger.debug(
'I\'m the bigger peersId - '
+ 'the other peer should start P2P', myId, peersId);

return;
} else if (myId === peersId) {
logger.error('The same IDs ? ', myId, peersId);
// Force initiator or responder mode for testing if option is passed to config.
if (this.options.config.testing?.forceInitiator) {
logger.debug(`Forcing P2P initiator, will start P2P with: ${jid}`);
this._startP2PSession(jid);
} else if (this.options.config.testing?.forceResponder) {
logger.debug(`Forcing P2P responder, will wait for the other peer ${jid} to start P2P`);
} else {
if (myId > peersId) {
logger.debug('I\'m the bigger peersId - the other peer should start P2P', myId, peersId);

return;
}
return;
} else if (myId === peersId) {
logger.error('The same IDs ? ', myId, peersId);

const jid = peer.getJid();
return;
}

if (userLeftEvent) {
if (this.deferredStartP2PTask) {
logger.error('Deferred start P2P task\'s been set already!');
if (userLeftEvent) {
if (this.deferredStartP2PTask) {
logger.error('Deferred start P2P task\'s been set already!');

return;
return;
}
logger.info(`Will start P2P with: ${jid} after ${this.backToP2PDelay} seconds...`);
this.deferredStartP2PTask = setTimeout(
this._startP2PSession.bind(this, jid),
this.backToP2PDelay * 1000);
} else {
logger.info(`Will start P2P with: ${jid}`);
this._startP2PSession(jid);
}
logger.info(
`Will start P2P with: ${jid} after ${
this.backToP2PDelay} seconds...`);
this.deferredStartP2PTask = setTimeout(
this._startP2PSession.bind(this, jid),
this.backToP2PDelay * 1000);
} else {
logger.info(`Will start P2P with: ${jid}`);
this._startP2PSession(jid);
}
} else if (this.p2pJingleSession && !shouldBeInP2P) {
logger.info(`Will stop P2P with: ${this.p2pJingleSession.remoteJid}`);
32 changes: 3 additions & 29 deletions JitsiConferenceEventManager.js
Original file line number Diff line number Diff line change
@@ -42,19 +42,6 @@ JitsiConferenceEventManager.prototype.setupChatRoomListeners = function() {
this.chatRoomForwarder = new EventEmitterForwarder(chatRoom,
this.conference.eventEmitter);

chatRoom.addListener(XMPPEvents.ICE_RESTARTING, jingleSession => {
if (!jingleSession.isP2P) {
// If using DataChannel as bridge channel, it must be closed
// before ICE restart, otherwise Chrome will not trigger "opened"
// event for the channel established with the new bridge.
// TODO: This may be bypassed when using a WebSocket as bridge
// channel.
conference.rtc.closeBridgeChannel();
}

// else: there are no DataChannels in P2P session (at least for now)
});

chatRoom.addListener(XMPPEvents.PARTICIPANT_FEATURES_CHANGED, (from, features) => {
const participant = conference.getParticipantById(Strophe.getResourceFromJid(from));

@@ -64,17 +51,6 @@ JitsiConferenceEventManager.prototype.setupChatRoomListeners = function() {
}
});

chatRoom.addListener(
XMPPEvents.ICE_RESTART_SUCCESS,
(jingleSession, offerIq) => {
// The JVB data chanel needs to be reopened in case the conference
// has been moved to a new bridge.
!jingleSession.isP2P
&& conference._setBridgeChannel(
offerIq, jingleSession.peerconnection);
});


chatRoom.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS,
actor => {
// TODO: Add a way to differentiate between commands which caused
@@ -299,16 +275,14 @@ JitsiConferenceEventManager.prototype.setupChatRoomListeners = function() {
this.chatRoomForwarder.forward(XMPPEvents.PHONE_NUMBER_CHANGED,
JitsiConferenceEvents.PHONE_NUMBER_CHANGED);

chatRoom.setParticipantPropertyListener((node, from) => {
const participant = conference.getParticipantById(from);
chatRoom.setParticipantPropertyListener((id, prop, value) => {
const participant = conference.getParticipantById(id);

if (!participant) {
return;
}

participant.setProperty(
node.tagName.substring('jitsi_participant_'.length),
node.value);
participant.setProperty(prop, value);
});

chatRoom.addListener(XMPPEvents.KICKED,
7 changes: 6 additions & 1 deletion JitsiConnection.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getLogger } from '@jitsi/logger';

import JitsiConference from './JitsiConference';
import * as JitsiConnectionEvents from './JitsiConnectionEvents';
import FeatureFlags from './modules/flags/FeatureFlags';
@@ -8,6 +10,8 @@ import {
createConnectionFailedEvent
} from './service/statistics/AnalyticsEvents';

const logger = getLogger(__filename);

/**
* Creates a new connection object for the Jitsi Meet server side video
* conferencing service. Provides access to the JitsiConference interface.
@@ -67,7 +71,8 @@ JitsiConnection.prototype.connect = function(options = {}) {
this.xmpp.moderator.sendConferenceRequest(this.xmpp.getRoomJid(options.name))
.then(() => {
this.xmpp.connect(options.id, options.password);
});
})
.catch(e => logger.trace('sendConferenceRequest rejected', e));
} else {
this.xmpp.connect(options.id, options.password);
}
4 changes: 4 additions & 0 deletions JitsiConnectionErrors.spec.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import * as exported from "./JitsiConnectionErrors";

describe( "/JitsiConnectionErrors members", () => {
const {
CONFERENCE_REQUEST_FAILED,
CONNECTION_DROPPED_ERROR,
NOT_LIVE_ERROR,
OTHER_ERROR,
@@ -14,13 +15,16 @@ describe( "/JitsiConnectionErrors members", () => {
} = exported;

it( "known members", () => {
expect( CONFERENCE_REQUEST_FAILED ).toBe( 'connection.conferenceRequestFailed' );
expect( CONNECTION_DROPPED_ERROR ).toBe( 'connection.droppedError' );
expect( NOT_LIVE_ERROR ).toBe( 'connection.notLiveError' );
expect( OTHER_ERROR ).toBe( 'connection.otherError' );
expect( PASSWORD_REQUIRED ).toBe( 'connection.passwordRequired' );
expect( SERVER_ERROR ).toBe( 'connection.serverError' );

expect( JitsiConnectionErrors ).toBeDefined();

expect( JitsiConnectionErrors.CONFERENCE_REQUEST_FAILED ).toBe( 'connection.conferenceRequestFailed' );
expect( JitsiConnectionErrors.CONNECTION_DROPPED_ERROR ).toBe( 'connection.droppedError' );
expect( JitsiConnectionErrors.NOT_LIVE_ERROR ).toBe( 'connection.notLiveError' );
expect( JitsiConnectionErrors.OTHER_ERROR ).toBe( 'connection.otherError' );
6 changes: 6 additions & 0 deletions JitsiConnectionErrors.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,11 @@
*/

export enum JitsiConnectionErrors {
/**
* When the conference-request to jicofo fails.
*/
CONFERENCE_REQUEST_FAILED = "connection.conferenceRequestFailed",

/**
* Indicates that the connection was dropped with an error which was most likely
* caused by some networking issues. The dropped term in this context means that
@@ -39,6 +44,7 @@ export enum JitsiConnectionErrors {
};

// exported for backward compatibility
export const CONFERENCE_REQUEST_FAILED = JitsiConnectionErrors.CONFERENCE_REQUEST_FAILED;
export const CONNECTION_DROPPED_ERROR = JitsiConnectionErrors.CONNECTION_DROPPED_ERROR;
export const NOT_LIVE_ERROR = JitsiConnectionErrors.NOT_LIVE_ERROR;
export const OTHER_ERROR = JitsiConnectionErrors.OTHER_ERROR;
33 changes: 19 additions & 14 deletions JitsiMeetJS.ts
Original file line number Diff line number Diff line change
@@ -35,9 +35,13 @@ import * as E2ePingEvents from './service/e2eping/E2ePingEvents';
import { createGetUserMediaEvent } from './service/statistics/AnalyticsEvents';
import * as RTCStatsEvents from './modules/RTCStats/RTCStatsEvents';
import { VideoType } from './service/RTC/VideoType';
import runPreCallTest, { IceServer, PreCallResult } from './modules/statistics/PreCallTest';

const logger = Logger.getLogger(__filename);

// Settin the default log levels to info early so that we avoid overriding a log level set externally.
Logger.setLogLevel(Logger.levels.INFO);

/**
* Indicates whether GUM has been executed or not.
*/
@@ -67,13 +71,15 @@ function getAnalyticsAttributesFromOptions(options) {
interface ICreateLocalTrackOptions {
cameraDeviceId?: string;
devices?: any[];
firePermissionPromptIsShownEvent?: boolean;
fireSlowPromiseEvent?: boolean;
micDeviceId?: string;
resolution?: string;
}

type desktopSharingSourceType = 'screen' | 'window';

interface IJitsiMeetJSOptions {
desktopSharingSources?: Array<desktopSharingSourceType>;
enableAnalyticsLogging?: boolean;
enableWindowOnErrorHandler?: boolean;
externalStorage?: Storage;
@@ -136,8 +142,6 @@ export default {
mediaDevices: JitsiMediaDevices as unknown,
analytics: Statistics.analytics as unknown,
init(options: IJitsiMeetJSOptions = {}) {
Logger.setLogLevel(Logger.levels.INFO);

// @ts-ignore
logger.info(`This appears to be ${browser.getName()}, ver: ${browser.getVersion()}`);

@@ -271,8 +275,6 @@ export default {
* which should be created. should be created or some additional
* configurations about resolution for example.
* @param {Array} options.effects optional effects array for the track
* @param {boolean} options.firePermissionPromptIsShownEvent - if event
* JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN should be fired
* @param {Array} options.devices the devices that will be requested
* @param {string} options.resolution resolution constraints
* @param {string} options.cameraDeviceId
@@ -283,13 +285,6 @@ export default {
* JitsiConferenceError if rejected.
*/
createLocalTracks(options: ICreateLocalTrackOptions = {}) {
const { firePermissionPromptIsShownEvent, ...restOptions } = options;

if (firePermissionPromptIsShownEvent && !RTC.arePermissionsGrantedForAvailableDevices()) {
// @ts-ignore
JitsiMediaDevices.emit(JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN, browser.getName());
}

let isFirstGUM = false;
let startTS = window.performance.now();

@@ -304,7 +299,7 @@ export default {
}
window.connectionTimes['obtainPermissions.start'] = startTS;

return RTC.obtainAudioAndVideoPermissions(restOptions)
return RTC.obtainAudioAndVideoPermissions(options)
.then(tracks => {
let endTS = window.performance.now();

@@ -317,7 +312,7 @@ export default {
Statistics.sendAnalytics(
createGetUserMediaEvent(
'success',
getAnalyticsAttributesFromOptions(restOptions)));
getAnalyticsAttributesFromOptions(options)));

if (this.isCollectingLocalStats()) {
for (let i = 0; i < tracks.length; i++) {
@@ -476,6 +471,16 @@ export default {
NetworkInfo.updateNetworkInfo({ isOnline });
},

/**
* Run a pre-call test to check the network conditions.
*
* @param {IceServer} iceServers - The ICE servers to use for the test,
* @returns {Promise<PreCallResult | any>} - A Promise that resolves with the test results or rejects with an error message.
*/
runPreCallTest(iceServers) {
return runPreCallTest(iceServers);
},

/**
* Represents a hub/namespace for utility functionality which may be of
* interest to lib-jitsi-meet clients.
8 changes: 4 additions & 4 deletions JitsiParticipant.js
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ export default class JitsiParticipant {
this._status = status;
this._hidden = hidden;
this._statsID = statsID;
this._properties = {};
this._properties = new Map();
this._identity = identity;
this._isReplacing = isReplacing;
this._isReplaced = isReplaced;
@@ -171,7 +171,7 @@ export default class JitsiParticipant {
* Gets the value of a property of this participant.
*/
getProperty(name) {
return this._properties[name];
return this._properties.get(name);
}

/**
@@ -347,10 +347,10 @@ export default class JitsiParticipant {
* @value the value to set.
*/
setProperty(name, value) {
const oldValue = this._properties[name];
const oldValue = this._properties.get(name);

if (value !== oldValue) {
this._properties[name] = value;
this._properties.set(name, value);
this._conference.eventEmitter.emit(
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
this,
4 changes: 3 additions & 1 deletion JitsiTrackError.js
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@ TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.SCREENSHARING_USER_CANCELED]
= 'User canceled screen sharing prompt';
TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR]
= 'Unknown error from screensharing';
TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.SCREENSHARING_NOT_SUPPORTED_ERROR]
= 'Not supported';
TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.ELECTRON_DESKTOP_PICKER_ERROR]
= 'Unkown error from desktop picker';
TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.ELECTRON_DESKTOP_PICKER_NOT_FOUND]
@@ -21,7 +23,7 @@ TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.NOT_FOUND]
TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.CONSTRAINT_FAILED]
= 'Constraint could not be satisfied: ';
TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.TIMEOUT]
= 'Could not start media source. Timeout occured!';
= 'Could not start media source. Timeout occurred!';
TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.TRACK_IS_DISPOSED]
= 'Track has been already disposed';
TRACK_ERROR_TO_MESSAGE_MAP[JitsiTrackErrors.TRACK_NO_STREAM_FOUND]
3 changes: 3 additions & 0 deletions JitsiTrackErrors.spec.ts
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ describe( "/JitsiTrackErrors members", () => {
NOT_FOUND,
PERMISSION_DENIED,
SCREENSHARING_GENERIC_ERROR,
SCREENSHARING_NOT_SUPPORTED_ERROR,
SCREENSHARING_USER_CANCELED,
TIMEOUT,
TRACK_IS_DISPOSED,
@@ -30,6 +31,7 @@ describe( "/JitsiTrackErrors members", () => {
expect( NOT_FOUND ).toBe( 'gum.not_found' );
expect( PERMISSION_DENIED ).toBe( 'gum.permission_denied' );
expect( SCREENSHARING_GENERIC_ERROR ).toBe( 'gum.screensharing_generic_error' );
expect( SCREENSHARING_NOT_SUPPORTED_ERROR ).toBe( 'gdm.screen_sharing_not_supported' );
expect( SCREENSHARING_USER_CANCELED ).toBe( 'gum.screensharing_user_canceled' );
expect( TIMEOUT ).toBe( 'gum.timeout' );
expect( TRACK_IS_DISPOSED ).toBe( 'track.track_is_disposed' );
@@ -47,6 +49,7 @@ describe( "/JitsiTrackErrors members", () => {
expect( JitsiTrackErrors.NOT_FOUND ).toBe( 'gum.not_found' );
expect( JitsiTrackErrors.PERMISSION_DENIED ).toBe( 'gum.permission_denied' );
expect( JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR ).toBe( 'gum.screensharing_generic_error' );
expect( JitsiTrackErrors.SCREENSHARING_NOT_SUPPORTED_ERROR ).toBe( 'gdm.screen_sharing_not_supported' );
expect( JitsiTrackErrors.SCREENSHARING_USER_CANCELED ).toBe( 'gum.screensharing_user_canceled' );
expect( JitsiTrackErrors.TIMEOUT ).toBe( 'gum.timeout' );
expect( JitsiTrackErrors.TRACK_IS_DISPOSED ).toBe( 'track.track_is_disposed' );
7 changes: 7 additions & 0 deletions JitsiTrackErrors.ts
Original file line number Diff line number Diff line change
@@ -42,6 +42,12 @@ export enum JitsiTrackErrors {
*/
SCREENSHARING_GENERIC_ERROR = 'gum.screensharing_generic_error',

/**
* Error in getDisplayMedia when not supported. Can happen in Electron if no
* permission handler was set.
*/
SCREENSHARING_NOT_SUPPORTED_ERROR = 'gdm.screen_sharing_not_supported',

/**
* An error which indicates that user canceled screen sharing window
* selection dialog.
@@ -89,6 +95,7 @@ export const GENERAL = JitsiTrackErrors.GENERAL;
export const NOT_FOUND = JitsiTrackErrors.NOT_FOUND;
export const PERMISSION_DENIED = JitsiTrackErrors.PERMISSION_DENIED;
export const SCREENSHARING_GENERIC_ERROR = JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR;
export const SCREENSHARING_NOT_SUPPORTED_ERROR = JitsiTrackErrors.SCREENSHARING_NOT_SUPPORTED_ERROR;
export const SCREENSHARING_USER_CANCELED = JitsiTrackErrors.SCREENSHARING_USER_CANCELED;
export const TIMEOUT = JitsiTrackErrors.TIMEOUT;
export const TRACK_IS_DISPOSED = JitsiTrackErrors.TRACK_IS_DISPOSED;
8 changes: 4 additions & 4 deletions JitsiTranscriptionStatus.spec.ts
Original file line number Diff line number Diff line change
@@ -11,13 +11,13 @@ describe( "/JitsiTranscriptionStatus members", () => {
} = exported;

it( "known members", () => {
expect( ON ).toBe( 'on' );
expect( OFF ).toBe( 'off' );
expect( ON ).toBe( 'ON' );
expect( OFF ).toBe( 'OFF' );

expect( JitsiTranscriptionStatus ).toBeDefined();

expect( JitsiTranscriptionStatus.ON ).toBe( 'on' );
expect( JitsiTranscriptionStatus.OFF ).toBe( 'off' );
expect( JitsiTranscriptionStatus.ON ).toBe( 'ON' );
expect( JitsiTranscriptionStatus.OFF ).toBe( 'OFF' );
} );

it( "unknown members", () => {
4 changes: 2 additions & 2 deletions JitsiTranscriptionStatus.ts
Original file line number Diff line number Diff line change
@@ -2,12 +2,12 @@ export enum JitsiTranscriptionStatus {
/**
* The transcription is on.
*/
ON = 'on',
ON = 'ON',

/**
* The transcription is off.
*/
OFF = 'off'
OFF = 'OFF'
}

// exported for backward compatibility
14 changes: 13 additions & 1 deletion authenticateAndUpgradeRole.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { getLogger } from '@jitsi/logger';

import {
CONNECTION_DISCONNECTED,
CONNECTION_ESTABLISHED,
CONNECTION_FAILED
} from './JitsiConnectionEvents';
import XMPP from './modules/xmpp/xmpp';

const logger = getLogger(__filename);

/**
* @typedef {Object} UpgradeRoleError
*
@@ -111,7 +115,15 @@ export default function authenticateAndUpgradeRole({
// we execute this logic in JitsiConference where we bind the current conference as `this`
// At this point we should have the new session ID
// stored in the settings. Send a new conference IQ.
this.room.xmpp.moderator.sendConferenceRequest(this.room.roomjid).finally(resolve);
this.room.xmpp.moderator.sendConferenceRequest(this.room.roomjid)
.catch(e => logger.trace('sendConferenceRequest rejected', e))
.finally(() => {
// we need to reset it because of breakout rooms which will
// reuse connection but will invite jicofo
this.room.xmpp.moderator.conferenceRequestSent = false;

resolve();
});
})
.catch(({ error, message }) => {
xmpp.disconnect();
13 changes: 13 additions & 0 deletions modules/RTC/MockClasses.js
Original file line number Diff line number Diff line change
@@ -183,6 +183,13 @@ export class MockPeerConnection {
return Array.from(codecs);
}

/**
* {@link TraceablePeerConnection.getDesiredMediaDirection}.
*/
getDesiredMediaDirection() {
return 'sendrecv';
}

/**
* {@link TraceablePeerConnection.isSpatialScalabilityOn}.
*
@@ -231,6 +238,12 @@ export class MockPeerConnection {
return false;
}

/**
* {@link TraceablePeerConnection.updateRemoteSources}.
*/
updateRemoteSources() {
}

/**
* {@link TraceablePeerConnection.usesUnifiedPlan}.
*/
16 changes: 6 additions & 10 deletions modules/RTC/RTC.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getLogger } from '@jitsi/logger';
import { cloneDeep } from 'lodash-es';
import { cloneDeep, isEqual } from 'lodash-es';

import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
import { MediaType } from '../../service/RTC/MediaType';
@@ -301,7 +301,11 @@ export default class RTC extends Listenable {
* @param {*} constraints
*/
setReceiverVideoConstraints(constraints) {
this._receiverVideoConstraints = constraints;
if (isEqual(this._receiverVideoConstraints, constraints)) {
return;
}

this._receiverVideoConstraints = cloneDeep(constraints);

if (this._channel && this._channel.isOpen()) {
this._channel.sendReceiverVideoConstraintsMessage(constraints);
@@ -627,14 +631,6 @@ export default class RTC extends Listenable {
return RTCUtils.getCurrentlyAvailableMediaDevices();
}

/**
* Returns whether available devices have permissions granted
* @returns {Boolean}
*/
static arePermissionsGrantedForAvailableDevices() {
return RTCUtils.arePermissionsGrantedForAvailableDevices();
}

/**
* Returns event data for device to be reported to stats.
* @returns {MediaDeviceInfo} device.
8 changes: 0 additions & 8 deletions modules/RTC/RTCUtils.js
Original file line number Diff line number Diff line change
@@ -827,14 +827,6 @@ class RTCUtils extends Listenable {
return availableDevices;
}

/**
* Returns whether available devices have permissions granted
* @returns {Boolean}
*/
arePermissionsGrantedForAvailableDevices() {
return availableDevices.some(device => Boolean(device.label));
}

/**
* Returns event data for device to be reported to stats.
* @returns {MediaDeviceInfo} device.
52 changes: 40 additions & 12 deletions modules/RTC/ScreenObtainer.js
Original file line number Diff line number Diff line change
@@ -36,6 +36,8 @@ const ScreenObtainer = {
if (!this.obtainStream) {
logger.info('Desktop sharing disabled');
}

this._electronSkipDisplayMedia = false;
},

/**
@@ -46,11 +48,13 @@ const ScreenObtainer = {
* @private
*/
_createObtainStreamMethod() {
const supportsGetDisplayMedia = browser.supportsGetDisplayMedia();

if (browser.isElectron()) {
return this.obtainScreenOnElectron;
} else if (browser.isReactNative() && browser.supportsGetDisplayMedia()) {
} else if (browser.isReactNative() && supportsGetDisplayMedia) {
return this.obtainScreenFromGetDisplayMediaRN;
} else if (browser.supportsGetDisplayMedia()) {
} else if (supportsGetDisplayMedia) {
return this.obtainScreenFromGetDisplayMedia;
}
logger.log('Screen sharing not supported on ', browser.getName());
@@ -92,7 +96,24 @@ const ScreenObtainer = {
* @param {Object} options - Optional parameters.
*/
obtainScreenOnElectron(onSuccess, onFailure, options = {}) {
if (window.JitsiMeetScreenObtainer && window.JitsiMeetScreenObtainer.openDesktopPicker) {
if (!this._electronSkipDisplayMedia) {
// Fall-back to the old API in case of not supported error. This can happen if
// an old Electron SDK is used with a new Jitsi Meet + lib-jitsi-meet version.
this.obtainScreenFromGetDisplayMedia(onSuccess, err => {
if (err.name === JitsiTrackErrors.SCREENSHARING_NOT_SUPPORTED_ERROR) {
// Make sure we don't recurse infinitely.
this._electronSkipDisplayMedia = true;
this.obtainScreenOnElectron(onSuccess, onFailure);
} else {
onFailure(err);
}
});

return;
}

// TODO: legacy flow, remove after the Electron SDK supporting gDM has been out for a while.
if (typeof window.JitsiMeetScreenObtainer?.openDesktopPicker === 'function') {
const { desktopSharingFrameRate, desktopSharingResolution, desktopSharingSources } = this.options;

window.JitsiMeetScreenObtainer.openDesktopPicker(
@@ -286,22 +307,29 @@ const ScreenObtainer = {
})
.catch(error => {
const errorDetails = {
errorName: error && error.name,
errorMsg: error && error.message,
errorStack: error && error.stack
errorCode: error.code,
errorName: error.name,
errorMsg: error.message,
errorStack: error.stack
};

logger.error('getDisplayMedia error', JSON.stringify(constraints), JSON.stringify(errorDetails));
logger.warn('getDisplayMedia error', JSON.stringify(constraints), JSON.stringify(errorDetails));

if (errorDetails.errorMsg && errorDetails.errorMsg.indexOf('denied by system') !== -1) {
if (errorDetails.errorCode === DOMException.NOT_SUPPORTED_ERR) {
// This error is thrown when an Electron client has not set a permissions handler.
errorCallback(new JitsiTrackError(JitsiTrackErrors.SCREENSHARING_NOT_SUPPORTED_ERROR));
} else if (errorDetails.errorMsg?.indexOf('denied by system') !== -1) {
// On Chrome this is the only thing different between error returned when user cancels
// and when no permission was given on the OS level.
errorCallback(new JitsiTrackError(JitsiTrackErrors.PERMISSION_DENIED));

return;
} else if (errorDetails.errorMsg === 'NotReadableError') {
// This can happen under some weird conditions:
// - https://issues.chromium.org/issues/369103607
// - https://issues.chromium.org/issues/353555347
errorCallback(new JitsiTrackError(JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR));
} else {
errorCallback(new JitsiTrackError(JitsiTrackErrors.SCREENSHARING_USER_CANCELED));
}

errorCallback(new JitsiTrackError(JitsiTrackErrors.SCREENSHARING_USER_CANCELED));
});
},

661 changes: 420 additions & 241 deletions modules/RTC/TPCUtils.js

Large diffs are not rendered by default.

111 changes: 37 additions & 74 deletions modules/RTC/TPCUtils.spec.js

Large diffs are not rendered by default.

1,194 changes: 451 additions & 743 deletions modules/RTC/TraceablePeerConnection.js

Large diffs are not rendered by default.

25 changes: 18 additions & 7 deletions modules/browser/BrowserCapabilities.js
Original file line number Diff line number Diff line change
@@ -71,6 +71,12 @@ export default class BrowserCapabilities extends BrowserDetection {
* @returns {boolean} true if the browser is supported, false otherwise.
*/
isSupported() {
// First check for WebRTC APIs because some "security" extensions are dumb.
if (typeof RTCPeerConnection === 'undefined'
|| !navigator?.mediaDevices?.enumerateDevices || !navigator?.mediaDevices?.getUserMedia) {
return false;
}

if (this.isSafari() && this._getSafariVersion() < MIN_REQUIRED_SAFARI_VERSION) {
return false;
}
@@ -146,7 +152,11 @@ export default class BrowserCapabilities extends BrowserDetection {

// this is not working on Safari because of the following bug
// https://bugs.webkit.org/show_bug.cgi?id=215567
&& !this.isWebKitBased();
&& !this.isWebKitBased()

// Calling this API on Firefox is causing freezes when the local endpoint is the answerer.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1917800
&& !this.isFirefox();
}

/**
@@ -167,7 +177,7 @@ export default class BrowserCapabilities extends BrowserDetection {
* @returns {boolean}
*/
supportsDDExtHeaders() {
return !this.isFirefox();
return !(this.isFirefox() && this.isVersionLessThan('136'));
}

/**
@@ -223,7 +233,8 @@ export default class BrowserCapabilities extends BrowserDetection {
* @returns {boolean}
*/
supportsScalabilityModeAPI() {
return this.isChromiumBased() && this.isEngineVersionGreaterThan(112);
return (this.isChromiumBased() && this.isEngineVersionGreaterThan(112))
|| (this.isFirefox() && this.isVersionGreaterThan(135));
}

/**
@@ -236,12 +247,12 @@ export default class BrowserCapabilities extends BrowserDetection {
}

/**
* Returns true if VP9 is supported by the client on the browser. VP9 is currently disabled on Firefox and Safari
* because of issues with rendering. Please check https://bugzilla.mozilla.org/show_bug.cgi?id=1492500,
* https://bugs.webkit.org/show_bug.cgi?id=231071 and https://bugs.webkit.org/show_bug.cgi?id=231074 for details.
* Returns true if VP9 is supported by the client on the browser. VP9 is currently disabled on Safari
* and older versions of Firefox because of issues. Please check https://bugs.webkit.org/show_bug.cgi?id=231074 for
* details.
*/
supportsVP9() {
return this.isChromiumBased() || this.isReactNative();
return !(this.isWebKitBased() || (this.isFirefox() && this.isVersionLessThan('136')));
}

/**
97 changes: 17 additions & 80 deletions modules/connectivity/IceFailedHandling.spec.js
Original file line number Diff line number Diff line change
@@ -36,9 +36,7 @@ describe('IceFailedHandling', () => {
// eslint-disable-next-line no-empty-function
emit: () => { }
};
mockConference.room = {
supportsRestartByTerminate: () => false
};
mockConference.room = {};
mockConference.xmpp = {
ping: () => Promise.resolve()
};
@@ -49,83 +47,11 @@ describe('IceFailedHandling', () => {
jasmine.clock().uninstall();
});

describe('when ICE restarts are disabled', () => {
beforeEach(() => {
mockConference.options.config.enableIceRestart = false;
});
it('emits ICE failed with 2 seconds delay after XMPP ping comes through', () => {
iceFailedHandling.start();

return nextTick() // tick for ping
.then(() => {
expect(emitEventSpy).not.toHaveBeenCalled();

return nextTick(2500); // tick for the 2 sec ice timeout
})
.then(() => {
expect(emitEventSpy).toHaveBeenCalled();
});
});
it('cancel method cancels the ICE failed event', () => {
iceFailedHandling.start();

return nextTick(1000) // tick for ping
.then(() => {
expect(emitEventSpy).not.toHaveBeenCalled();
iceFailedHandling.cancel();

return nextTick(2500); // tick for ice timeout
})
.then(() => {
expect(emitEventSpy).not.toHaveBeenCalled();
});
});
});
describe('when ICE restart are enabled', () => {
let sendIceFailedSpy;

beforeEach(() => {
mockConference.options.config.enableIceRestart = true;
mockConference.jvbJingleSession = {
getIceConnectionState: () => 'failed',
// eslint-disable-next-line no-empty-function
sendIceFailedNotification: () => { }
};
sendIceFailedSpy = spyOn(mockConference.jvbJingleSession, 'sendIceFailedNotification');
});
it('send ICE failed notification to Jicofo', () => {
iceFailedHandling.start();

return nextTick() // tick for ping
.then(() => nextTick(2500)) // tick for ice timeout
.then(() => {
expect(sendIceFailedSpy).toHaveBeenCalled();
});
});
it('not send ICE failed notification to Jicofo if canceled', () => {
iceFailedHandling.start();

// first it send ping which is async - need next tick
return nextTick(1000)
.then(() => {
expect(sendIceFailedSpy).not.toHaveBeenCalled();
iceFailedHandling.cancel();

return nextTick(3000); // tick for ice timeout
})
.then(() => {
expect(sendIceFailedSpy).not.toHaveBeenCalled();
});
});
});
describe('if Jingle session restarts are supported', () => {
let sendSessionTerminateSpy;

beforeEach(() => {
mockConference.options.config.enableIceRestart = undefined;
mockConference.room = {
supportsRestartByTerminate: () => true
};
mockConference.room = {};
mockConference.jvbJingleSession = {
getIceConnectionState: () => 'failed',
// eslint-disable-next-line no-empty-function
@@ -148,15 +74,26 @@ describe('IceFailedHandling', () => {
});
});
});
it('cancel method cancels the call to terminate session', () => {
iceFailedHandling.start();

return nextTick(1000) // tick for ping
.then(() => {
expect(sendSessionTerminateSpy).not.toHaveBeenCalled();
iceFailedHandling.cancel();

return nextTick(2500); // tick for ice timeout
})
.then(() => {
expect(sendSessionTerminateSpy).not.toHaveBeenCalled();
});
});
});
describe('when forced reloads are enabled', () => {
beforeEach(() => {
mockConference.options.config.enableIceRestart = undefined;
mockConference.options.config.enableForcedReload = true;

mockConference.room = {
supportsRestartByTerminate: () => true
};
mockConference.room = {};
});

it('emits conference restarted when force reloads are enabled', () => {
Original file line number Diff line number Diff line change
@@ -2,26 +2,28 @@ import { getLogger } from '@jitsi/logger';

import * as JitsiConferenceErrors from '../../JitsiConferenceErrors';
import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
import JitsiConference from '../../JitsiConference';

const logger = getLogger(__filename);

/**
* This class deals with shenanigans around JVB media session's ICE failed status handling.
*
* If ICE restarts are NOT explicitly enabled by the {@code enableIceRestart} config option, then the conference will
* delay emitting the {@JitsiConferenceErrors.ICE_FAILED} event by 15 seconds. If the network info module reports
* the internet offline status then the time will start counting after the internet comes back online.
*
* If ICE restart are enabled, then a delayed ICE failed notification to Jicofo will be sent, only if the ICE connection
* does not recover soon after or before the XMPP connection is restored (if it was ever broken). If ICE fails while
* the XMPP connection is not broken then the notifications will be sent after 2 seconds delay.
* If ICE connection is not re-established within 2 secs after the internet comes back online, the client will initiate
* a session restart via 'session-terminate'. This results in Jicofo re-inviting the participant into the conference by
* recreating the jvb media session so that there is minimla disruption to the user by default. However, if the
* 'enableForcedReload' option is set in config.js, the conference will be forcefully reloaded.
*/
export default class IceFailedHandling {
private _conference: JitsiConference;
private _canceled: boolean = false;
private _iceFailedTimeout?: number;

/**
* Creates new {@code DelayedIceFailed} task.
* @param {JitsiConference} conference
*/
constructor(conference) {
constructor(conference:JitsiConference) {
this._conference = conference;
}

@@ -31,28 +33,20 @@ export default class IceFailedHandling {
* @private
* @returns {void}
*/
_actOnIceFailed() {
_actOnIceFailed(): void {
if (!this._conference.room) {
return;
}

const { enableForcedReload, enableIceRestart } = this._conference.options.config;
const explicitlyDisabled = typeof enableIceRestart !== 'undefined' && !enableIceRestart;
const supportsRestartByTerminate = this._conference.room.supportsRestartByTerminate();
const useTerminateForRestart = supportsRestartByTerminate && !enableIceRestart;

logger.info('ICE failed,'
+ ` enableForcedReload: ${enableForcedReload},`
+ ` enableIceRestart: ${enableIceRestart},`
+ ` supports restart by terminate: ${supportsRestartByTerminate}`);
const { enableForcedReload } = this._conference.options.config;

if (explicitlyDisabled || (!enableIceRestart && !supportsRestartByTerminate) || enableForcedReload) {
logger.info('ICE failed, but ICE restarts are disabled');
const reason = enableForcedReload
? JitsiConferenceErrors.CONFERENCE_RESTARTED
: JitsiConferenceErrors.ICE_FAILED;
logger.info(`ICE failed, enableForcedReload: ${enableForcedReload}`);

this._conference.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_FAILED, reason);
if (enableForcedReload) {
logger.info('ICE failed, force reloading the conference');
this._conference.eventEmitter.emit(
JitsiConferenceEvents.CONFERENCE_FAILED,
JitsiConferenceErrors.CONFERENCE_RESTARTED);

return;
}
@@ -65,26 +59,21 @@ export default class IceFailedHandling {
} else if (jvbConnIceState === 'connected') {
logger.info('ICE connection restored - not sending ICE failed');
} else {
logger.info('Sending ICE failed - the connection did not recover, '
+ `ICE state: ${jvbConnIceState}, `
+ `use 'session-terminate': ${useTerminateForRestart}`);
if (useTerminateForRestart) {
this._conference._stopJvbSession({
reason: 'connectivity-error',
reasonDescription: 'ICE FAILED',
requestRestart: true,
sendSessionTerminate: true
});
} else {
this._conference.jvbJingleSession.sendIceFailedNotification();
}
logger.info(`Sending ICE failed - the connection did not recover, ICE state: ${jvbConnIceState}`);
this._conference._stopJvbSession({
reason: 'connectivity-error',
reasonDescription: 'ICE FAILED',
requestRestart: true,
sendSessionTerminate: true
});
}
}

/**
* Starts the task.
* @returns {void}
*/
start() {
start(): void {
// Using xmpp.ping allows to handle both XMPP being disconnected and internet offline cases. The ping function
// uses sendIQ2 method which is resilient to XMPP connection disconnected state and will patiently wait until it
// gets reconnected.
@@ -110,8 +99,9 @@ export default class IceFailedHandling {

/**
* Cancels the task.
* @returns {void}
*/
cancel() {
cancel(): void {
this._canceled = true;
window.clearTimeout(this._iceFailedTimeout);
}
9 changes: 6 additions & 3 deletions modules/e2ee/E2EEncryption.js
Original file line number Diff line number Diff line change
@@ -37,9 +37,12 @@ export class E2EEncryption {
return false;
}

return !(config.testing && config.testing.disableE2EE)
&& (browser.supportsInsertableStreams()
|| (config.enableEncodedTransformSupport && browser.supportsEncodedTransform()));
if (e2ee.disabled || config.testing?.disableE2EE) {
return false;
}

return browser.supportsInsertableStreams()
|| (config.enableEncodedTransformSupport && browser.supportsEncodedTransform());
}

/**
5 changes: 2 additions & 3 deletions modules/proxyconnection/ProxyConnectionPC.js
Original file line number Diff line number Diff line change
@@ -225,8 +225,7 @@ export default class ProxyConnectionPC {
connectionTimes: [],
eventEmitter: { emit: emitter },
removeEventListener: () => { /* no op */ },
removePresenceListener: () => { /* no-op */ },
supportsRestartByTerminate: () => false
removePresenceListener: () => { /* no-op */ }
};

/**
@@ -286,7 +285,7 @@ export default class ProxyConnectionPC {
* Invoked when a connection related issue has been encountered.
*
* @param {string} errorType - The constant indicating the type of the error
* that occured.
* that occurred.
* @param {string} details - Optional additional data about the error.
* @private
* @returns {void}
2 changes: 1 addition & 1 deletion modules/proxyconnection/ProxyConnectionService.js
Original file line number Diff line number Diff line change
@@ -218,7 +218,7 @@ export default class ProxyConnectionService {
* attempted or started, and to which an iq with error details should be
* sent.
* @param {string} errorType - The constant indicating the type of the error
* that occured.
* that occurred.
* @param {string} details - Optional additional data about the error.
* @private
* @returns {void}
5 changes: 2 additions & 3 deletions modules/qualitycontrol/CodecSelection.js
Original file line number Diff line number Diff line change
@@ -72,11 +72,10 @@ export class CodecSelection {
}

// Push VP9 to the end of the list so that the client continues to decode VP9 even if its not
// preferable to encode VP9 (because of browser bugs on the encoding side or added complexity on mobile
// devices). Currently, VP9 encode is supported on Chrome and on Safari (only for p2p).
// preferable to encode VP9 (because of browser bugs on the encoding side or other reasons).
const isVp9EncodeSupported = browser.supportsVP9() || (browser.isWebKitBased() && connectionType === 'p2p');

if (!isVp9EncodeSupported || this.conference.isE2EEEnabled()) {
if (!isVp9EncodeSupported) {
const index = selectedOrder.findIndex(codec => codec === CodecMimeType.VP9);

if (index !== -1) {
113 changes: 93 additions & 20 deletions modules/qualitycontrol/CodecSelection.spec.js
Original file line number Diff line number Diff line change
@@ -50,43 +50,57 @@ describe('Codec Selection', () => {
};

qualityController = new QualityController(conference, options);
jasmine.clock().install();
spyOn(jingleSession, 'setVideoCodecs');
});

it('and remote endpoints use the new codec selection logic', () => {
afterEach(() => {
jasmine.clock().uninstall();
});

it('and remote endpoints use the new codec selection logic', async () => {
// Add a second user joining the call.
participant1 = new MockParticipant('remote-1');
conference.addParticipant(participant1, [ 'vp9', 'vp8' ]);

await nextTick(1000);

expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'vp8' ], 'vp9');

// Add a third user joining the call with a subset of codecs.
participant2 = new MockParticipant('remote-2');
conference.addParticipant(participant2, [ 'vp8' ]);

expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8' ], 'vp9');

// Make p2 leave the call
// Make p2 leave the call.
conference.removeParticipant(participant2);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(3);

await nextTick(1000);

expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(2);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'vp8' ], 'vp9');
});

it('and remote endpoints use the old codec selection logic (RN)', () => {
it('and remote endpoints use the old codec selection logic (RN)', async () => {
// Add a second user joining the call.
participant1 = new MockParticipant('remote-1');
conference.addParticipant(participant1, null, 'vp8');

await nextTick(1000);

expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8' ], 'vp9');

// Add a third user (newer) to the call.
participant2 = new MockParticipant('remote-2');
conference.addParticipant(participant2, [ 'vp9', 'vp8' ]);

expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8' ], 'vp9');

// Make p1 leave the call
conference.removeParticipant(participant1);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(3);

await nextTick(1000);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(2);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'vp8' ], 'vp9');
});
});

@@ -102,50 +116,61 @@ describe('Codec Selection', () => {

qualityController = new QualityController(conference, options);
spyOn(jingleSession, 'setVideoCodecs');
jasmine.clock().install();
});

it('and remote endpoints use the new codec selection logic', () => {
afterEach(() => {
jasmine.clock().uninstall();
});

it('and remote endpoints use the new codec selection logic', async () => {
// Add a second user joining the call.
participant1 = new MockParticipant('remote-1');
conference.addParticipant(participant1, [ 'vp9', 'vp8', 'h264' ]);

await nextTick(1000);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'vp8' ], undefined);

// Add a third user joining the call with a subset of codecs.
participant2 = new MockParticipant('remote-2');
conference.addParticipant(participant2, [ 'vp8' ]);

expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8' ], undefined);

// Make p2 leave the call
conference.removeParticipant(participant2);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(3);

await nextTick(1000);

expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(2);
});

it('and remote endpoint prefers a codec that is locally disabled', () => {
it('and remote endpoint prefers a codec that is locally disabled', async () => {
// Add a second user joining the call the prefers H.264 and VP8.
participant1 = new MockParticipant('remote-1');
conference.addParticipant(participant1, [ 'h264', 'vp8' ]);

await nextTick(1200);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8' ], undefined);
});

it('and remote endpoints use the old codec selection logic (RN)', () => {
it('and remote endpoints use the old codec selection logic (RN)', async () => {
// Add a second user joining the call.
participant1 = new MockParticipant('remote-1');
conference.addParticipant(participant1, null, 'vp8');

await nextTick(1000);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8' ], undefined);

// Add a third user (newer) to the call.
participant2 = new MockParticipant('remote-2');
conference.addParticipant(participant2, [ 'vp9', 'vp8', 'h264' ]);

expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8' ], undefined);

// Make p1 leave the call
conference.removeParticipant(participant1);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(3);

jasmine.clock().tick(1000);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(2);
});
});

@@ -171,11 +196,14 @@ describe('Codec Selection', () => {

participant1 = new MockParticipant('remote-1');
conference.addParticipant(participant1, [ 'av1', 'vp9', 'vp8' ]);

await nextTick(1000);

expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'av1', 'vp9', 'vp8' ], undefined);

participant2 = new MockParticipant('remote-2');
conference.addParticipant(participant2, [ 'av1', 'vp9', 'vp8' ]);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'av1', 'vp9', 'vp8' ], undefined);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);

qualityController.codecController.changeCodecPreferenceOrder(localTrack, 'av1');

@@ -187,6 +215,7 @@ describe('Codec Selection', () => {

// Expect the local endpoint to continue sending VP9.
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'av1', 'vp8' ], undefined);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(3);
});

it('and does not change codec if the current codec is already the lowest complexity codec', async () => {
@@ -196,6 +225,8 @@ describe('Codec Selection', () => {

participant1 = new MockParticipant('remote-1');
conference.addParticipant(participant1, [ 'av1', 'vp9', 'vp8' ]);

await nextTick(1000);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8', 'vp9', 'av1' ], undefined);

participant2 = new MockParticipant('remote-2');
@@ -239,11 +270,14 @@ describe('Codec Selection', () => {

participant1 = new MockParticipant('remote-1');
conference.addParticipant(participant1, [ 'av1', 'vp9', 'vp8' ]);

await nextTick(1000);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'av1', 'vp9', 'vp8' ], undefined);

participant2 = new MockParticipant('remote-2');
conference.addParticipant(participant2, [ 'av1', 'vp9', 'vp8' ]);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'av1', 'vp9', 'vp8' ], undefined);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);

const sourceStats = {
avgEncodeTime: 12,
@@ -269,6 +303,8 @@ describe('Codec Selection', () => {
participant3 = new MockParticipant('remote-3');
conference.addParticipant(participant3, [ 'av1', 'vp9', 'vp8' ]);

await nextTick(1000);

// Expect the local endpoint to continue sending VP9.
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'av1', 'vp8' ], undefined);

@@ -328,4 +364,41 @@ describe('Codec Selection', () => {
expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(0);
});
});

describe('When multiple joins and leaves happen in a quick burst', () => {
beforeEach(() => {
options = {
jvb: {
preferenceOrder: [ 'AV1', 'VP9', 'VP8' ],
screenshareCodec: 'VP9'
},
p2p: {}
};
jasmine.clock().install();
qualityController = new QualityController(conference, options);
spyOn(jingleSession, 'setVideoCodecs');
});

afterEach(() => {
jasmine.clock().uninstall();
});

it('should call setVideoCodecs only once within the same tick', async () => {
participant1 = new MockParticipant('remote-1');
conference.addParticipant(participant1, [ 'vp9', 'vp8' ]);

// Add a third user joining the call with a subset of codecs.
participant2 = new MockParticipant('remote-2');
conference.addParticipant(participant2, [ 'vp8' ]);

// Make p1 and p2 leave the call.
conference.removeParticipant(participant2);
conference.removeParticipant(participant1);

await nextTick(1000);

expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'av1', 'vp9', 'vp8' ], 'vp9');
});
});
});
4 changes: 2 additions & 2 deletions modules/qualitycontrol/QualityController.spec.js
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ describe('QualityController', () => {
p2p: {}
};
localTrack = new MockLocalTrack('1', 720, 'camera');
qualityController = new QualityController(conference, options, true);
qualityController = new QualityController(conference, options);
sourceStats = {
avgEncodeTime: 12,
codec: 'VP8',
@@ -165,7 +165,7 @@ describe('QualityController', () => {
p2p: {}
};
localTrack = new MockLocalTrack('1', 720, 'camera');
qualityController = new QualityController(conference, options, true);
qualityController = new QualityController(conference, options);
sourceStats = {
avgEncodeTime: 12,
codec: 'VP8',
46 changes: 34 additions & 12 deletions modules/qualitycontrol/QualityController.ts
Original file line number Diff line number Diff line change
@@ -145,12 +145,13 @@ export class QualityController {
this._conference.on(
JitsiConferenceEvents.CONFERENCE_VISITOR_CODECS_CHANGED,
(codecList: CodecMimeType[]) => this._codecController.updateVisitorCodecs(codecList));
this._conference.on(
JitsiConferenceEvents.USER_JOINED,
() => this._codecController.selectPreferredCodec(this._conference.jvbJingleSession));
this._conference.on(
JitsiConferenceEvents.USER_LEFT,
() => this._codecController.selectPreferredCodec(this._conference.jvbJingleSession));

// Debounce the calls to codec selection when there is a burst of joins and leaves.
const debouncedSelectCodec = this._debounce(
() => this._codecController.selectPreferredCodec(this._conference.jvbJingleSession),
1000);
this._conference.on(JitsiConferenceEvents.USER_JOINED, debouncedSelectCodec.bind(this));
this._conference.on(JitsiConferenceEvents.USER_LEFT, debouncedSelectCodec.bind(this));
this._conference.rtc.on(
RTCEvents.SENDER_VIDEO_CONSTRAINTS_CHANGED,
(videoConstraints: IVideoConstraints) => this._sendVideoController.onSenderConstraintsReceived(videoConstraints));
@@ -159,6 +160,26 @@ export class QualityController {
(tpc: TraceablePeerConnection, stats: Map<number, IOutboundRtpStats>) => this._processOutboundRtpStats(tpc, stats));
}

/**
* Creates a debounced function that delays the execution of the provided function until after the specified delay
* has elapsed. Unlike typical debounce implementations, the timer does not reset when the function is called again
* within the delay period.
*
* @param {Function} func - The function to be debounced.
* @param {number} delay - The delay in milliseconds.
* @returns {Function} - The debounced function.
*/
_debounce(func: Function, delay: number) {
return function (...args) {
if (!this._timer) {
this._timer = setTimeout(() => {
this._timer = null;
func.apply(this, args);
}, delay);
}
};
}

/**
* Adjusts the lastN value so that fewer remote video sources are received from the bridge in an attempt to improve
* encode resolution of the outbound video streams based on cpuLimited parameter passed. If cpuLimited is false,
@@ -280,8 +301,12 @@ export class QualityController {
if (!this._enableAdaptiveMode) {
return;
}

const { encodeResolution, localTrack, qualityLimitationReason, tpc } = sourceStats;

// Older browser versions might not report the resolution in the stats.
if (Number.isNaN(encodeResolution)) {
return;
}
const trackId = localTrack.rtcId;

if (encodeResolution === tpc.calculateExpectedSendResolution(localTrack)) {
@@ -319,10 +344,7 @@ export class QualityController {
// 2. Switch to a lower lastN value, cutting the receive videos by half in every iteration until
// MIN_LAST_N value is reached.
// 3. Lower the receive resolution of individual streams up to 180p.
if (qualityLimitationReason === QualityLimitationReason.CPU
|| (encodeResolution < tpc.calculateExpectedSendResolution(localTrack)
&& qualityLimitationReason !== QualityLimitationReason.BANDWIDTH)) {

if (qualityLimitationReason === QualityLimitationReason.CPU) {
if (this._lastNRampupTimeout) {
window.clearTimeout(this._lastNRampupTimeout);
this._lastNRampupTimeout = undefined;
@@ -364,7 +386,7 @@ export class QualityController {
const track = tpc.getTrackBySSRC(ssrc);
const trackId = track.rtcId;
let existingStats = statsPerTrack.get(trackId);
const encodeResolution = Math.min(resolution.height, resolution.width);
const encodeResolution = Math.min(resolution?.height, resolution?.width);
const ssrcStats = {
encodeResolution,
encodeTime,
7 changes: 4 additions & 3 deletions modules/qualitycontrol/ReceiveVideoController.js
Original file line number Diff line number Diff line change
@@ -74,14 +74,15 @@ export default class ReceiveVideoController {
/**
* Updates the source based constraints based on the maxHeight set.
*
* @param {number} maxFrameHeight - the height to be requested for remote sources.
* @returns {void}
*/
_updateIndividualConstraints() {
_updateIndividualConstraints(maxFrameHeight) {
const individualConstraints = this._receiverVideoConstraints.constraints;

if (individualConstraints && Object.keys(individualConstraints).length) {
for (const value of Object.values(individualConstraints)) {
value.maxHeight = Math.min(value.maxHeight, this._maxFrameHeight);
value.maxHeight = maxFrameHeight ?? Math.min(value.maxHeight, this._maxFrameHeight);
}
} else {
this._receiverVideoConstraints.defaultConstraints = { 'maxHeight': this._maxFrameHeight };
@@ -184,7 +185,7 @@ export default class ReceiveVideoController {
if (session.isP2P) {
session.setReceiverVideoConstraint(this._getDefaultSourceReceiverConstraints(session, maxFrameHeight));
} else {
this._updateIndividualConstraints();
this._updateIndividualConstraints(maxFrameHeight);
this._rtc.setReceiverVideoConstraints(this._receiverVideoConstraints);
}
}
1 change: 1 addition & 0 deletions modules/recording/recordingConstants.js
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ export default {
error: {
BUSY: 'busy',
ERROR: 'error',
POLICY_VIOLATION: 'policy-violation',
RESOURCE_CONSTRAINT: 'resource-constraint',
UNEXPECTED_REQUEST: 'unexpected-request',
SERVICE_UNAVAILABLE: 'service-unavailable'
245 changes: 78 additions & 167 deletions modules/sdp/LocalSdpMunger.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import { getLogger } from '@jitsi/logger';
import { isEqual } from 'lodash-es';

import { MediaDirection } from '../../service/RTC/MediaDirection';
import { MediaType } from '../../service/RTC/MediaType';
import { getSourceNameForJitsiTrack } from '../../service/RTC/SignalingLayer';
import browser from '../browser';

import { SdpTransformWrap } from './SdpTransformUtil';

const logger = getLogger(__filename);

/**
* Fakes local SDP exposed to {@link JingleSessionPC} through the local
* description getter. Modifies the SDP, so that it will contain muted local
* video tracks description, even though their underlying {MediaStreamTrack}s
* are no longer in the WebRTC peerconnection. That prevents from SSRC updates
* being sent to Jicofo/remote peer and prevents sRD/sLD cycle on the remote
* side.
* Fakes local SDP exposed to {@link JingleSessionPC} through the local description getter. Modifies the SDP, so that
* the stream identifiers are unique across all of the local PeerConnections and that the source names and video types
* are injected so that Jicofo can use them to identify the sources.
*/
export default class LocalSdpMunger {

@@ -28,77 +22,83 @@ export default class LocalSdpMunger {
constructor(tpc, localEndpointId) {
this.tpc = tpc;
this.localEndpointId = localEndpointId;
this.audioSourcesToMsidMap = new Map();
this.videoSourcesToMsidMap = new Map();
}

/**
* Returns a string that can be set as the MSID attribute for a source.
*
* @param {string} mediaType - Media type of the source.
* @param {string} trackId - Id of the MediaStreamTrack associated with the source.
* @param {string} streamId - Id of the MediaStream associated with the source.
* @returns {string|null}
*/
_generateMsidAttribute(mediaType, trackId, streamId) {
if (!(mediaType && trackId)) {
logger.error(`Unable to munge local MSID - track id=${trackId} or media type=${mediaType} is missing`);

return null;
}
const pcId = this.tpc.id;

return `${streamId}-${pcId} ${trackId}-${pcId}`;
}

/**
* Updates or adds a 'msid' attribute in the format '<endpoint_id>-<mediaType>-<trackIndex>-<tpcId>'
* example - d8ff91-video-0-1
* All other attributes like 'cname', 'label' and 'mslabel' are removed since these are not processed by Jicofo.
* Updates or adds a 'msid' attribute for the local sources in the SDP. Also adds 'sourceName' and 'videoType'
* (if applicable) attributes. All other source attributes like 'cname', 'label' and 'mslabel' are removed since
* these are not processed by Jicofo.
*
* @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
* modified in place.
* @returns {void}
* @private
*/
_transformMediaIdentifiers(mediaSection) {
const mediaType = mediaSection.mLine?.type;
const mediaDirection = mediaSection.mLine?.direction;
const msidLine = mediaSection.mLine?.msid;
const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
const streamId = `${this.localEndpointId}-${mediaType}`;
let trackId = msidLine ? msidLine.split(' ')[1] : `${this.localEndpointId}-${mediaSection.mLine.mid}`;

// Always overwrite msid since we want the msid to be in this format even if the browser generates one.
for (const source of sources) {
const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid');

if (msid) {
trackId = msid.value.split(' ')[1];
_transformMediaIdentifiers(mediaSection, ssrcMap) {
const mediaType = mediaSection.mLine.type;
const mediaDirection = mediaSection.mLine.direction;
const sources = [ ...new Set(mediaSection.mLine.ssrcs?.map(s => s.id)) ];
let trackId = mediaSection.mLine.msid?.split(' ')[1];
let sourceName;

if (ssrcMap.size) {
const sortedSources = sources.slice().sort();

for (const [ id, trackSsrcs ] of ssrcMap.entries()) {
if (isEqual(sortedSources, [ ...trackSsrcs.ssrcs ].sort())) {
sourceName = id;
}
}
this._updateSourcesToMsidMap(mediaType, streamId, trackId);
const storedStreamId = mediaType === MediaType.VIDEO
? this.videoSourcesToMsidMap.get(trackId)
: this.audioSourcesToMsidMap.get(trackId);

const generatedMsid = this._generateMsidAttribute(mediaType, trackId, storedStreamId);

// Update the msid if the 'msid' attribute exists.
if (msid) {
msid.value = generatedMsid;

// Generate the 'msid' attribute if there is a local source.
} else if (mediaDirection === MediaDirection.SENDONLY || mediaDirection === MediaDirection.SENDRECV) {
mediaSection.ssrcs.push({
id: source,
attribute: 'msid',
value: generatedMsid
});
for (const source of sources) {
if ((mediaDirection === MediaDirection.SENDONLY || mediaDirection === MediaDirection.SENDRECV)
&& sourceName) {
const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid');

if (msid) {
trackId = msid.value.split(' ')[1];
}
const generatedMsid = `${ssrcMap.get(sourceName).msid}-${this.tpc.id} ${trackId}-${this.tpc.id}`;
const existingMsid = mediaSection.ssrcs
.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid');

// Always overwrite msid since we want the msid to be in this format even if the browser generates
// one. '<endpoint_id>-<mediaType>-<trackIndex>-<tpcId>' example - d8ff91-video-0-1
if (existingMsid) {
existingMsid.value = generatedMsid;
} else {
mediaSection.ssrcs.push({
id: source,
attribute: 'msid',
value: generatedMsid
});
}

// Inject source names as a=ssrc:3124985624 name:endpointA-v0
mediaSection.ssrcs.push({
id: source,
attribute: 'name',
value: sourceName
});

const videoType = this.tpc.getLocalVideoTracks()
.find(track => track.getSourceName() === sourceName)
?.getVideoType();

if (mediaType === MediaType.VIDEO && videoType) {
// Inject videoType as a=ssrc:1234 videoType:desktop.
mediaSection.ssrcs.push({
id: source,
attribute: 'videoType',
value: videoType
});
}
}
}
}

// Ignore the 'cname', 'label' and 'mslabel' attributes and only have the 'msid' attribute.
mediaSection.ssrcs = mediaSection.ssrcs.filter(ssrc => ssrc.attribute === 'msid');
// Ignore the 'cname', 'label' and 'mslabel' attributes.
mediaSection.ssrcs = mediaSection.ssrcs
.filter(ssrc => ssrc.attribute === 'msid' || ssrc.attribute === 'name' || ssrc.attribute === 'videoType');

// On FF when the user has started muted create answer will generate a recv only SSRC. We don't want to signal
// this SSRC in order to reduce the load of the xmpp server for large calls. Therefore the SSRC needs to be
@@ -122,43 +122,16 @@ export default class LocalSdpMunger {
}

/**
* Updates the MSID map.
* This transformation will make sure that stream identifiers are unique across all of the local PeerConnections
* even if the same stream is used by multiple instances at the same time. It also injects 'sourceName' and
* 'videoType' attribute.
*
* @param {string} mediaType The media type.
* @param {string} streamId The stream id.
* @param {string} trackId The track id.
* @returns {void}
*/
_updateSourcesToMsidMap(mediaType, streamId, trackId) {
if (mediaType === MediaType.VIDEO) {
if (!this.videoSourcesToMsidMap.has(trackId)) {
const generatedStreamId = `${streamId}-${this.videoSourcesToMsidMap.size}`;

this.videoSourcesToMsidMap.set(trackId, generatedStreamId);
}
} else if (!this.audioSourcesToMsidMap.has(trackId)) {
const generatedStreamId = `${streamId}-${this.audioSourcesToMsidMap.size}`;

this.audioSourcesToMsidMap.set(trackId, generatedStreamId);
}
}

/**
* This transformation will make sure that stream identifiers are unique
* across all of the local PeerConnections even if the same stream is used
* by multiple instances at the same time.
* Each PeerConnection assigns different SSRCs to the same local
* MediaStream, but the MSID remains the same as it's used to identify
* the stream by the WebRTC backend. The transformation will append
* {@link TraceablePeerConnection#id} at the end of each stream's identifier
* ("cname", "msid", "label" and "mslabel").
*
* @param {RTCSessionDescription} sessionDesc - The local session
* description (this instance remains unchanged).
* @param {RTCSessionDescription} sessionDesc - The local session description (this instance remains unchanged).
* @param {Map<string, TPCSSRCInfo>} ssrcMap - The SSRC and source map for the local tracks.
* @return {RTCSessionDescription} - Transformed local session description
* (a modified copy of the one given as the input).
*/
transformStreamIdentifiers(sessionDesc) {
transformStreamIdentifiers(sessionDesc, ssrcMap) {
if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) {
return sessionDesc;
}
@@ -167,80 +140,18 @@ export default class LocalSdpMunger {
const audioMLine = transformer.selectMedia(MediaType.AUDIO)?.[0];

if (audioMLine) {
this._transformMediaIdentifiers(audioMLine);
this._injectSourceNames(audioMLine);
this._transformMediaIdentifiers(audioMLine, ssrcMap);
}

const videoMlines = transformer.selectMedia(MediaType.VIDEO);

for (const videoMLine of videoMlines) {
this._transformMediaIdentifiers(videoMLine);
this._injectSourceNames(videoMLine);
this._transformMediaIdentifiers(videoMLine, ssrcMap);
}

// Reset the local tracks based maps for msid after every transformation since Chrome 122 is generating
// a new set of SSRCs for the same source when the direction of transceiver changes because of a remote
// source getting added on the p2p connection.
this.audioSourcesToMsidMap.clear();
this.videoSourcesToMsidMap.clear();

return new RTCSessionDescription({
return {
type: sessionDesc.type,
sdp: transformer.toRawSDP()
});
}

/**
* Injects source names. Source names are need to for multiple streams per endpoint support. The final plan is to
* use the "mid" attribute for source names, but because the SDP to Jingle conversion still operates in the Plan-B
* semantics (one source name per media), a custom "name" attribute is injected into SSRC lines..
*
* @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
* modified in place.
* @returns {void}
* @private
*/
_injectSourceNames(mediaSection) {
const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
const mediaType = mediaSection.mLine?.type;

if (!mediaType) {
throw new Error('_transformMediaIdentifiers - no media type in mediaSection');
}

for (const source of sources) {
const nameExists = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'name');
const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid').value;
const streamId = msid.split(' ')[0];

// Example stream id: d8ff91-video-8-1
// In the example above 8 is the track index
const trackIndexParts = streamId.split('-');
const trackIndex = trackIndexParts[trackIndexParts.length - 2];
const sourceName = getSourceNameForJitsiTrack(this.localEndpointId, mediaType, trackIndex);

if (!nameExists) {
// Inject source names as a=ssrc:3124985624 name:endpointA-v0
mediaSection.ssrcs.push({
id: source,
attribute: 'name',
value: sourceName
});
}

if (mediaType === MediaType.VIDEO) {
const videoType = this.tpc.getLocalVideoTracks().find(track => track.getSourceName() === sourceName)
?.getVideoType();

if (videoType) {
// Inject videoType as a=ssrc:1234 videoType:desktop.
mediaSection.ssrcs.push({
id: source,
attribute: 'videoType',
value: videoType
});
}
}
}
};
}
}
61 changes: 51 additions & 10 deletions modules/sdp/LocalSdpMunger.spec.js
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@
import * as transform from 'sdp-transform';

import { MockPeerConnection } from '../RTC/MockClasses';
import FeatureFlags from '../flags/FeatureFlags';

import LocalSdpMunger from './LocalSdpMunger';
import { default as SampleSdpStrings } from './SampleSdpStrings.js';
@@ -26,7 +25,6 @@ describe('TransformSdpsForUnifiedPlan', () => {
const localEndpointId = 'sRdpsdg';

beforeEach(() => {
FeatureFlags.init({ });
localSdpMunger = new LocalSdpMunger(tpc, localEndpointId);
});
describe('StripSsrcs', () => {
@@ -38,7 +36,7 @@ describe('TransformSdpsForUnifiedPlan', () => {
type: 'offer',
sdp: sdpStr
});
const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc);
const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, {});
const newSdp = transform.parse(transformedDesc.sdp);
const audioSsrcs = getSsrcLines(newSdp, 'audio');
const videoSsrcs = getSsrcLines(newSdp, 'video');
@@ -56,7 +54,20 @@ describe('TransformSdpsForUnifiedPlan', () => {
type: 'offer',
sdp: sdpStr
});
const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc);
const ssrcMap = new Map();

ssrcMap.set('sRdpsdg-v0', {
ssrcs: [ 1757014965, 1479742055, 1089111804 ],
msid: 'sRdpsdg-video-0',
groups: [ {
semantics: 'SIM',
ssrcs: [ 1757014965, 1479742055, 1089111804 ] } ]
});
ssrcMap.set('sRdpsdg-a0', {
ssrcs: [ 124723944 ],
msid: 'sRdpsdg-audio-0'
});
const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap);
const newSdp = transform.parse(transformedDesc.sdp);

audioSsrcs = getSsrcLines(newSdp, 'audio');
@@ -79,16 +90,27 @@ describe('TransformSdpsForUnifiedPlan', () => {
type: 'offer',
sdp: sdpStr
});
const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc);
const ssrcMap = new Map();

ssrcMap.set('sRdpsdg-v0', {
ssrcs: [ 984899560 ],
msid: 'sRdpsdg-video-0'
});
ssrcMap.set('sRdpsdg-a0', {
ssrcs: [ 124723944 ],
msid: 'sRdpsdg-audio-0'
});
const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap);
const newSdp = transform.parse(transformedDesc.sdp);

const videoSsrcs = getSsrcLines(newSdp, 'video');

for (const ssrcLine of videoSsrcs) {
if (ssrcLine.attribute === 'msid') {
const msid = ssrcLine.value.split(' ')[0];
const msid = ssrcLine.value;

expect(msid).toBe(`${localEndpointId}-video-0-${tpc.id}`);
expect(msid)
.toBe(`${localEndpointId}-video-0-${tpc.id} bdbd2c0a-7959-4578-8db5-9a6a1aec4ecf-${tpc.id}`);
}
}
});
@@ -102,7 +124,17 @@ describe('TransformSdpsForUnifiedPlan', () => {
type: 'offer',
sdp: sdpStr
});
const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc);
const ssrcMap = new Map();

ssrcMap.set('sRdpsdg-v0', {
ssrcs: [ 984899560 ],
msid: 'sRdpsdg-video-0'
});
ssrcMap.set('sRdpsdg-a0', {
ssrcs: [ 124723944 ],
msid: 'sRdpsdg-audio-0'
});
const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap);
const newSdp = transform.parse(transformedDesc.sdp);
const videoSsrcs = getSsrcLines(newSdp, 'video');
const msidExists = videoSsrcs.find(s => s.attribute === 'msid');
@@ -124,7 +156,17 @@ describe('Transform msids for source-name signaling', () => {
type: 'offer',
sdp: sdpStr
});
const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc);
const ssrcMap = new Map();

ssrcMap.set('sRdpsdg-v0', {
ssrcs: [ 1757014965, 984899560, 1479742055, 855213044, 1089111804, 2963867077 ],
msid: 'sRdpsdg-video-0'
});
ssrcMap.set('sRdpsdg-a0', {
ssrcs: [ 124723944 ],
msid: 'sRdpsdg-audio-0'
});
const transformedDesc = localSdpMunger.transformStreamIdentifiers(desc, ssrcMap);
const newSdp = transform.parse(transformedDesc.sdp);

audioMsidLine = getSsrcLines(newSdp, 'audio').find(ssrc => ssrc.attribute === 'msid')?.value;
@@ -134,7 +176,6 @@ describe('Transform msids for source-name signaling', () => {
};

it('should transform', () => {
FeatureFlags.init({ });
transformStreamIdentifiers();

expect(audioMsid).toBe('sRdpsdg-audio-0-1');
7 changes: 4 additions & 3 deletions modules/sdp/RtxModifier.js
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import { getLogger } from '@jitsi/logger';

import { MediaDirection } from '../../service/RTC/MediaDirection';
import { MediaType } from '../../service/RTC/MediaType';
import { SSRC_GROUP_SEMANTICS } from '../../service/RTC/StandardVideoQualitySettings';

import SDPUtil from './SDPUtil';
import { SdpTransformWrap, parseSecondarySSRC } from './SdpTransformUtil';
@@ -48,7 +49,7 @@ function updateAssociatedRtxStream(mLine, primarySsrcInfo, rtxSsrc) {
value: primarySsrcMsid
});
mLine.addSSRCGroup({
semantics: 'FID',
semantics: SSRC_GROUP_SEMANTICS.FID,
ssrcs: `${primarySsrc} ${rtxSsrc}`
});
}
@@ -188,10 +189,10 @@ export default class RtxModifier {
if (videoMLine.direction !== MediaDirection.RECVONLY
&& videoMLine.getSSRCCount()
&& videoMLine.containsAnySSRCGroups()) {
const fidGroups = videoMLine.findGroups('FID');
const fidGroups = videoMLine.findGroups(SSRC_GROUP_SEMANTICS.FID);

// Remove the fid groups from the mline
videoMLine.removeGroupsBySemantics('FID');
videoMLine.removeGroupsBySemantics(SSRC_GROUP_SEMANTICS.FID);

// Get the rtx ssrcs and remove them from the mline
for (const fidGroup of fidGroups) {
Loading