@stream-io/video-client 0.3.28 → 0.3.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +6 -4
  3. package/dist/index.browser.es.js +382 -118
  4. package/dist/index.browser.es.js.map +1 -1
  5. package/dist/index.cjs.js +382 -116
  6. package/dist/index.cjs.js.map +1 -1
  7. package/dist/index.es.js +382 -118
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/Call.d.ts +14 -10
  10. package/dist/src/devices/CameraManager.d.ts +0 -1
  11. package/dist/src/devices/InputMediaDeviceManager.d.ts +18 -15
  12. package/dist/src/devices/InputMediaDeviceManagerState.d.ts +22 -6
  13. package/dist/src/devices/MicrophoneManager.d.ts +0 -1
  14. package/dist/src/devices/ScreenShareManager.d.ts +39 -0
  15. package/dist/src/devices/ScreenShareState.d.ts +36 -0
  16. package/dist/src/devices/__tests__/ScreenShareManager.test.d.ts +1 -0
  17. package/dist/src/devices/__tests__/mocks.d.ts +3 -7
  18. package/dist/src/devices/index.d.ts +2 -0
  19. package/dist/src/helpers/DynascaleManager.d.ts +3 -2
  20. package/dist/src/helpers/__tests__/hq-audio-sdp.d.ts +1 -0
  21. package/dist/src/helpers/sdp-munging.d.ts +8 -0
  22. package/dist/src/rtc/Publisher.d.ts +7 -4
  23. package/dist/src/rtc/helpers/tracks.d.ts +2 -1
  24. package/dist/src/rtc/videoLayers.d.ts +2 -1
  25. package/dist/src/types.d.ts +20 -0
  26. package/dist/version.d.ts +1 -1
  27. package/package.json +1 -1
  28. package/src/Call.ts +56 -12
  29. package/src/devices/CameraManager.ts +3 -4
  30. package/src/devices/InputMediaDeviceManager.ts +60 -45
  31. package/src/devices/InputMediaDeviceManagerState.ts +34 -14
  32. package/src/devices/MicrophoneManager.ts +3 -4
  33. package/src/devices/ScreenShareManager.ts +85 -0
  34. package/src/devices/ScreenShareState.ts +63 -0
  35. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +16 -1
  36. package/src/devices/__tests__/ScreenShareManager.test.ts +119 -0
  37. package/src/devices/__tests__/mocks.ts +38 -1
  38. package/src/devices/devices.ts +10 -1
  39. package/src/devices/index.ts +2 -0
  40. package/src/helpers/DynascaleManager.ts +18 -3
  41. package/src/helpers/__tests__/DynascaleManager.test.ts +36 -1
  42. package/src/helpers/__tests__/hq-audio-sdp.ts +332 -0
  43. package/src/helpers/__tests__/sdp-munging.test.ts +13 -1
  44. package/src/helpers/sdp-munging.ts +49 -0
  45. package/src/rtc/Publisher.ts +87 -48
  46. package/src/rtc/Subscriber.ts +4 -1
  47. package/src/rtc/helpers/tracks.ts +16 -6
  48. package/src/rtc/videoLayers.ts +4 -2
  49. package/src/store/CallState.ts +3 -2
  50. package/src/store/__tests__/CallState.test.ts +1 -1
  51. package/src/types.ts +27 -0
@@ -1,6 +1,7 @@
1
1
  import { vi } from 'vitest';
2
2
  import { CallingState, CallState } from '../../store';
3
3
  import { OwnCapability } from '../../gen/coordinator';
4
+ import { Call } from '../../Call';
4
5
 
5
6
  export const mockVideoDevices = [
6
7
  {
@@ -63,7 +64,7 @@ export const mockAudioDevices = [
63
64
  },
64
65
  ] as MediaDeviceInfo[];
65
66
 
66
- export const mockCall = () => {
67
+ export const mockCall = (): Partial<Call> => {
67
68
  const callState = new CallState();
68
69
  callState.setCallingState(CallingState.JOINED);
69
70
  callState.setOwnCapabilities([
@@ -74,6 +75,7 @@ export const mockCall = () => {
74
75
  state: callState,
75
76
  publishVideoStream: vi.fn(),
76
77
  publishAudioStream: vi.fn(),
78
+ publishScreenShareStream: vi.fn(),
77
79
  stopPublish: vi.fn(),
78
80
  };
79
81
  };
@@ -90,6 +92,7 @@ export const mockAudioStream = () => {
90
92
  },
91
93
  };
92
94
  return {
95
+ getTracks: () => [track],
93
96
  getAudioTracks: () => [track],
94
97
  } as MediaStream;
95
98
  };
@@ -108,6 +111,40 @@ export const mockVideoStream = () => {
108
111
  },
109
112
  };
110
113
  return {
114
+ getTracks: () => [track],
111
115
  getVideoTracks: () => [track],
112
116
  } as MediaStream;
113
117
  };
118
+
119
+ export const mockScreenShareStream = (includeAudio: boolean = true) => {
120
+ const track = {
121
+ getSettings: () => ({
122
+ deviceId: 'screen',
123
+ }),
124
+ enabled: true,
125
+ readyState: 'live',
126
+ stop: () => {
127
+ track.readyState = 'ended';
128
+ },
129
+ };
130
+
131
+ const tracks = [track];
132
+ if (includeAudio) {
133
+ tracks.push({
134
+ getSettings: () => ({
135
+ deviceId: 'screen-audio',
136
+ }),
137
+ enabled: true,
138
+ readyState: 'live',
139
+ stop: () => {
140
+ track.readyState = 'ended';
141
+ },
142
+ });
143
+ }
144
+
145
+ return {
146
+ getTracks: () => tracks,
147
+ getVideoTracks: () => tracks,
148
+ getAudioTracks: () => tracks,
149
+ } as MediaStream;
150
+ };
@@ -199,7 +199,16 @@ export const getScreenShareStream = async (
199
199
  try {
200
200
  return await navigator.mediaDevices.getDisplayMedia({
201
201
  video: true,
202
- audio: false,
202
+ audio: {
203
+ channelCount: {
204
+ ideal: 2,
205
+ },
206
+ echoCancellation: false,
207
+ autoGainControl: false,
208
+ noiseSuppression: false,
209
+ },
210
+ // @ts-expect-error - not present in types yet
211
+ systemAudio: 'include',
203
212
  ...options,
204
213
  });
205
214
  } catch (e) {
@@ -5,5 +5,7 @@ export * from './CameraManager';
5
5
  export * from './CameraManagerState';
6
6
  export * from './MicrophoneManager';
7
7
  export * from './MicrophoneManagerState';
8
+ export * from './ScreenShareManager';
9
+ export * from './ScreenShareState';
8
10
  export * from './SpeakerManager';
9
11
  export * from './SpeakerState';
@@ -1,5 +1,6 @@
1
1
  import { Call } from '../Call';
2
2
  import {
3
+ AudioTrackType,
3
4
  DebounceType,
4
5
  StreamVideoLocalParticipant,
5
6
  StreamVideoParticipant,
@@ -324,9 +325,14 @@ export class DynascaleManager {
324
325
  *
325
326
  * @param audioElement the audio element to bind to.
326
327
  * @param sessionId the session id.
328
+ * @param trackType the kind of audio.
327
329
  * @returns a cleanup function that will unbind the audio element.
328
330
  */
329
- bindAudioElement = (audioElement: HTMLAudioElement, sessionId: string) => {
331
+ bindAudioElement = (
332
+ audioElement: HTMLAudioElement,
333
+ sessionId: string,
334
+ trackType: AudioTrackType,
335
+ ) => {
330
336
  const participant = this.call.state.findParticipantBySessionId(sessionId);
331
337
  if (!participant || participant.isLocalParticipant) return;
332
338
 
@@ -343,9 +349,18 @@ export class DynascaleManager {
343
349
  );
344
350
 
345
351
  const updateMediaStreamSubscription = participant$
346
- .pipe(distinctUntilKeyChanged('audioStream'))
352
+ .pipe(
353
+ distinctUntilKeyChanged(
354
+ trackType === 'screenShareAudioTrack'
355
+ ? 'screenShareAudioStream'
356
+ : 'audioStream',
357
+ ),
358
+ )
347
359
  .subscribe((p) => {
348
- const source = p.audioStream;
360
+ const source =
361
+ trackType === 'screenShareAudioTrack'
362
+ ? p.screenShareAudioStream
363
+ : p.audioStream;
349
364
  if (audioElement.srcObject === source) return;
350
365
 
351
366
  setTimeout(() => {
@@ -103,7 +103,7 @@ describe('DynascaleManager', () => {
103
103
  videoElement.clientHeight = 100;
104
104
  });
105
105
 
106
- it('should bind audio element', () => {
106
+ it('audio: should bind audio element', () => {
107
107
  vi.useFakeTimers();
108
108
  const audioElement = document.createElement('audio');
109
109
  const play = vi.spyOn(audioElement, 'play').mockResolvedValue();
@@ -128,6 +128,7 @@ describe('DynascaleManager', () => {
128
128
  const cleanup = dynascaleManager.bindAudioElement(
129
129
  audioElement,
130
130
  'session-id',
131
+ 'audioTrack',
131
132
  );
132
133
  expect(audioElement.autoplay).toBe(true);
133
134
 
@@ -172,6 +173,40 @@ describe('DynascaleManager', () => {
172
173
  cleanup?.();
173
174
  });
174
175
 
176
+ it('audio: should bind screenShare audio element', () => {
177
+ vi.useFakeTimers();
178
+ const audioElement = document.createElement('audio');
179
+ const play = vi.spyOn(audioElement, 'play').mockResolvedValue();
180
+
181
+ // @ts-ignore
182
+ call.state.updateOrAddParticipant('session-id', {
183
+ userId: 'user-id',
184
+ sessionId: 'session-id',
185
+ publishedTracks: [TrackType.SCREEN_SHARE_AUDIO],
186
+ });
187
+
188
+ const cleanup = dynascaleManager.bindAudioElement(
189
+ audioElement,
190
+ 'session-id',
191
+ 'screenShareAudioTrack',
192
+ );
193
+ expect(audioElement.autoplay).toBe(true);
194
+
195
+ const audioMediaStream = new MediaStream();
196
+ const screenShareAudioMediaStream = new MediaStream();
197
+ call.state.updateParticipant('session-id', {
198
+ audioStream: audioMediaStream,
199
+ screenShareAudioStream: screenShareAudioMediaStream,
200
+ });
201
+
202
+ vi.runAllTimers();
203
+
204
+ expect(play).toHaveBeenCalled();
205
+ expect(audioElement.srcObject).toBe(screenShareAudioMediaStream);
206
+
207
+ cleanup?.();
208
+ });
209
+
175
210
  it('video: should update subscription when track becomes available', () => {
176
211
  const updateSubscription = vi.spyOn(call, 'updateSubscriptionsPartial');
177
212
 
@@ -0,0 +1,332 @@
1
+ export const initialSdp = `
2
+ v=0
3
+ o=- 898697271686242868 5 IN IP4 127.0.0.1
4
+ s=-
5
+ t=0 0
6
+ a=group:BUNDLE 0 1 2 3
7
+ a=extmap-allow-mixed
8
+ a=msid-semantic: WMS e893e3ad-d9e8-4b56-998f-0d89213dd857
9
+ m=video 60017 UDP/TLS/RTP/SAVPF 96 97 102 103 104 105 106 107 108 109 127 125 39 40 45 46 98 99 100 101 112 113 116 117 118
10
+ c=IN IP4 79.125.240.146
11
+ a=rtcp:9 IN IP4 0.0.0.0
12
+ a=candidate:2824354870 1 udp 2122260223 192.168.1.102 60017 typ host generation 0 network-id 2
13
+ a=candidate:427386432 1 udp 2122194687 192.168.1.244 63221 typ host generation 0 network-id 1 network-cost 10
14
+ a=candidate:2841136656 1 udp 1686052607 79.125.240.146 60017 typ srflx raddr 192.168.1.102 rport 60017 generation 0 network-id 2
15
+ a=candidate:410588262 1 udp 1685987071 79.125.240.146 63221 typ srflx raddr 192.168.1.244 rport 63221 generation 0 network-id 1 network-cost 10
16
+ a=candidate:3600277166 1 tcp 1518280447 192.168.1.102 9 typ host tcptype active generation 0 network-id 2
17
+ a=candidate:1740014808 1 tcp 1518214911 192.168.1.244 9 typ host tcptype active generation 0 network-id 1 network-cost 10
18
+ a=ice-ufrag:GM64
19
+ a=ice-pwd:ANZFilRRlZJ3bg9AD40eRu7n
20
+ a=ice-options:trickle
21
+ a=fingerprint:sha-256 38:1F:02:E5:2A:49:9A:2A:D9:8E:B9:9B:4C:40:21:B7:F1:C4:27:8E:B5:68:D6:E0:91:08:D9:CB:2B:AC:B3:87
22
+ a=setup:actpass
23
+ a=mid:0
24
+ a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
25
+ a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
26
+ a=extmap:3 urn:3gpp:video-orientation
27
+ a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
28
+ a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
29
+ a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
30
+ a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
31
+ a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
32
+ a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
33
+ a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
34
+ a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
35
+ a=sendonly
36
+ a=msid:e893e3ad-d9e8-4b56-998f-0d89213dd857 44e5d53f-ce6a-4bf4-824a-335113d86e6e
37
+ a=rtcp-mux
38
+ a=rtcp-rsize
39
+ a=rtpmap:96 VP8/90000
40
+ a=rtcp-fb:96 goog-remb
41
+ a=rtcp-fb:96 transport-cc
42
+ a=rtcp-fb:96 ccm fir
43
+ a=rtcp-fb:96 nack
44
+ a=rtcp-fb:96 nack pli
45
+ a=rtpmap:97 rtx/90000
46
+ a=fmtp:97 apt=96
47
+ a=rtpmap:102 H264/90000
48
+ a=rtcp-fb:102 goog-remb
49
+ a=rtcp-fb:102 transport-cc
50
+ a=rtcp-fb:102 ccm fir
51
+ a=rtcp-fb:102 nack
52
+ a=rtcp-fb:102 nack pli
53
+ a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
54
+ a=rtpmap:103 rtx/90000
55
+ a=fmtp:103 apt=102
56
+ a=rtpmap:104 H264/90000
57
+ a=rtcp-fb:104 goog-remb
58
+ a=rtcp-fb:104 transport-cc
59
+ a=rtcp-fb:104 ccm fir
60
+ a=rtcp-fb:104 nack
61
+ a=rtcp-fb:104 nack pli
62
+ a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
63
+ a=rtpmap:105 rtx/90000
64
+ a=fmtp:105 apt=104
65
+ a=rtpmap:106 H264/90000
66
+ a=rtcp-fb:106 goog-remb
67
+ a=rtcp-fb:106 transport-cc
68
+ a=rtcp-fb:106 ccm fir
69
+ a=rtcp-fb:106 nack
70
+ a=rtcp-fb:106 nack pli
71
+ a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
72
+ a=rtpmap:107 rtx/90000
73
+ a=fmtp:107 apt=106
74
+ a=rtpmap:108 H264/90000
75
+ a=rtcp-fb:108 goog-remb
76
+ a=rtcp-fb:108 transport-cc
77
+ a=rtcp-fb:108 ccm fir
78
+ a=rtcp-fb:108 nack
79
+ a=rtcp-fb:108 nack pli
80
+ a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
81
+ a=rtpmap:109 rtx/90000
82
+ a=fmtp:109 apt=108
83
+ a=rtpmap:127 H264/90000
84
+ a=rtcp-fb:127 goog-remb
85
+ a=rtcp-fb:127 transport-cc
86
+ a=rtcp-fb:127 ccm fir
87
+ a=rtcp-fb:127 nack
88
+ a=rtcp-fb:127 nack pli
89
+ a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
90
+ a=rtpmap:125 rtx/90000
91
+ a=fmtp:125 apt=127
92
+ a=rtpmap:39 H264/90000
93
+ a=rtcp-fb:39 goog-remb
94
+ a=rtcp-fb:39 transport-cc
95
+ a=rtcp-fb:39 ccm fir
96
+ a=rtcp-fb:39 nack
97
+ a=rtcp-fb:39 nack pli
98
+ a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
99
+ a=rtpmap:40 rtx/90000
100
+ a=fmtp:40 apt=39
101
+ a=rtpmap:45 AV1/90000
102
+ a=rtcp-fb:45 goog-remb
103
+ a=rtcp-fb:45 transport-cc
104
+ a=rtcp-fb:45 ccm fir
105
+ a=rtcp-fb:45 nack
106
+ a=rtcp-fb:45 nack pli
107
+ a=rtpmap:46 rtx/90000
108
+ a=fmtp:46 apt=45
109
+ a=rtpmap:98 VP9/90000
110
+ a=rtcp-fb:98 goog-remb
111
+ a=rtcp-fb:98 transport-cc
112
+ a=rtcp-fb:98 ccm fir
113
+ a=rtcp-fb:98 nack
114
+ a=rtcp-fb:98 nack pli
115
+ a=fmtp:98 profile-id=0
116
+ a=rtpmap:99 rtx/90000
117
+ a=fmtp:99 apt=98
118
+ a=rtpmap:100 VP9/90000
119
+ a=rtcp-fb:100 goog-remb
120
+ a=rtcp-fb:100 transport-cc
121
+ a=rtcp-fb:100 ccm fir
122
+ a=rtcp-fb:100 nack
123
+ a=rtcp-fb:100 nack pli
124
+ a=fmtp:100 profile-id=2
125
+ a=rtpmap:101 rtx/90000
126
+ a=fmtp:101 apt=100
127
+ a=rtpmap:112 H264/90000
128
+ a=rtcp-fb:112 goog-remb
129
+ a=rtcp-fb:112 transport-cc
130
+ a=rtcp-fb:112 ccm fir
131
+ a=rtcp-fb:112 nack
132
+ a=rtcp-fb:112 nack pli
133
+ a=fmtp:112 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
134
+ a=rtpmap:113 rtx/90000
135
+ a=fmtp:113 apt=112
136
+ a=rtpmap:116 red/90000
137
+ a=rtpmap:117 rtx/90000
138
+ a=fmtp:117 apt=116
139
+ a=rtpmap:118 ulpfec/90000
140
+ a=rid:q send
141
+ a=rid:h send
142
+ a=rid:f send
143
+ a=simulcast:send q;h;f
144
+ m=audio 9 UDP/TLS/RTP/SAVPF 63 111 9 0 8 13 110 126
145
+ c=IN IP4 0.0.0.0
146
+ a=rtcp:9 IN IP4 0.0.0.0
147
+ a=ice-ufrag:GM64
148
+ a=ice-pwd:ANZFilRRlZJ3bg9AD40eRu7n
149
+ a=ice-options:trickle
150
+ a=fingerprint:sha-256 38:1F:02:E5:2A:49:9A:2A:D9:8E:B9:9B:4C:40:21:B7:F1:C4:27:8E:B5:68:D6:E0:91:08:D9:CB:2B:AC:B3:87
151
+ a=setup:actpass
152
+ a=mid:1
153
+ a=extmap:14 urn:ietf:params:rtp-hdrext:ssrc-audio-level
154
+ a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
155
+ a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
156
+ a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
157
+ a=sendonly
158
+ a=msid:- cb02416c-8035-4e80-8b7e-becd6b7f600f
159
+ a=rtcp-mux
160
+ a=rtpmap:63 red/48000/2
161
+ a=fmtp:63 111/111
162
+ a=rtpmap:111 opus/48000/2
163
+ a=rtcp-fb:111 transport-cc
164
+ a=fmtp:111 minptime=10;usedtx=1;useinbandfec=1
165
+ a=rtpmap:9 G722/8000
166
+ a=rtpmap:0 PCMU/8000
167
+ a=rtpmap:8 PCMA/8000
168
+ a=rtpmap:13 CN/8000
169
+ a=rtpmap:110 telephone-event/48000
170
+ a=rtpmap:126 telephone-event/8000
171
+ a=ssrc:743121750 cname:mbGW+aeWMLFhPbBC
172
+ a=ssrc:743121750 msid:- cb02416c-8035-4e80-8b7e-becd6b7f600f
173
+ m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 103 104 105 106 107 108 109 127 125 39 40 45 46 98 99 100 101 112 113 116 117 118
174
+ c=IN IP4 0.0.0.0
175
+ a=rtcp:9 IN IP4 0.0.0.0
176
+ a=ice-ufrag:GM64
177
+ a=ice-pwd:ANZFilRRlZJ3bg9AD40eRu7n
178
+ a=ice-options:trickle
179
+ a=fingerprint:sha-256 38:1F:02:E5:2A:49:9A:2A:D9:8E:B9:9B:4C:40:21:B7:F1:C4:27:8E:B5:68:D6:E0:91:08:D9:CB:2B:AC:B3:87
180
+ a=setup:actpass
181
+ a=mid:2
182
+ a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
183
+ a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
184
+ a=extmap:3 urn:3gpp:video-orientation
185
+ a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
186
+ a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
187
+ a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
188
+ a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
189
+ a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
190
+ a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
191
+ a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
192
+ a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
193
+ a=sendonly
194
+ a=msid:04f96a2a-d107-4a12-b545-cfe1dc2c4d37 4a1ae8a7-6c89-44d3-a9a1-02abce0012c7
195
+ a=rtcp-mux
196
+ a=rtcp-rsize
197
+ a=rtpmap:96 VP8/90000
198
+ a=rtcp-fb:96 goog-remb
199
+ a=rtcp-fb:96 transport-cc
200
+ a=rtcp-fb:96 ccm fir
201
+ a=rtcp-fb:96 nack
202
+ a=rtcp-fb:96 nack pli
203
+ a=rtpmap:97 rtx/90000
204
+ a=fmtp:97 apt=96
205
+ a=rtpmap:102 H264/90000
206
+ a=rtcp-fb:102 goog-remb
207
+ a=rtcp-fb:102 transport-cc
208
+ a=rtcp-fb:102 ccm fir
209
+ a=rtcp-fb:102 nack
210
+ a=rtcp-fb:102 nack pli
211
+ a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
212
+ a=rtpmap:103 rtx/90000
213
+ a=fmtp:103 apt=102
214
+ a=rtpmap:104 H264/90000
215
+ a=rtcp-fb:104 goog-remb
216
+ a=rtcp-fb:104 transport-cc
217
+ a=rtcp-fb:104 ccm fir
218
+ a=rtcp-fb:104 nack
219
+ a=rtcp-fb:104 nack pli
220
+ a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
221
+ a=rtpmap:105 rtx/90000
222
+ a=fmtp:105 apt=104
223
+ a=rtpmap:106 H264/90000
224
+ a=rtcp-fb:106 goog-remb
225
+ a=rtcp-fb:106 transport-cc
226
+ a=rtcp-fb:106 ccm fir
227
+ a=rtcp-fb:106 nack
228
+ a=rtcp-fb:106 nack pli
229
+ a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
230
+ a=rtpmap:107 rtx/90000
231
+ a=fmtp:107 apt=106
232
+ a=rtpmap:108 H264/90000
233
+ a=rtcp-fb:108 goog-remb
234
+ a=rtcp-fb:108 transport-cc
235
+ a=rtcp-fb:108 ccm fir
236
+ a=rtcp-fb:108 nack
237
+ a=rtcp-fb:108 nack pli
238
+ a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
239
+ a=rtpmap:109 rtx/90000
240
+ a=fmtp:109 apt=108
241
+ a=rtpmap:127 H264/90000
242
+ a=rtcp-fb:127 goog-remb
243
+ a=rtcp-fb:127 transport-cc
244
+ a=rtcp-fb:127 ccm fir
245
+ a=rtcp-fb:127 nack
246
+ a=rtcp-fb:127 nack pli
247
+ a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
248
+ a=rtpmap:125 rtx/90000
249
+ a=fmtp:125 apt=127
250
+ a=rtpmap:39 H264/90000
251
+ a=rtcp-fb:39 goog-remb
252
+ a=rtcp-fb:39 transport-cc
253
+ a=rtcp-fb:39 ccm fir
254
+ a=rtcp-fb:39 nack
255
+ a=rtcp-fb:39 nack pli
256
+ a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
257
+ a=rtpmap:40 rtx/90000
258
+ a=fmtp:40 apt=39
259
+ a=rtpmap:45 AV1/90000
260
+ a=rtcp-fb:45 goog-remb
261
+ a=rtcp-fb:45 transport-cc
262
+ a=rtcp-fb:45 ccm fir
263
+ a=rtcp-fb:45 nack
264
+ a=rtcp-fb:45 nack pli
265
+ a=rtpmap:46 rtx/90000
266
+ a=fmtp:46 apt=45
267
+ a=rtpmap:98 VP9/90000
268
+ a=rtcp-fb:98 goog-remb
269
+ a=rtcp-fb:98 transport-cc
270
+ a=rtcp-fb:98 ccm fir
271
+ a=rtcp-fb:98 nack
272
+ a=rtcp-fb:98 nack pli
273
+ a=fmtp:98 profile-id=0
274
+ a=rtpmap:99 rtx/90000
275
+ a=fmtp:99 apt=98
276
+ a=rtpmap:100 VP9/90000
277
+ a=rtcp-fb:100 goog-remb
278
+ a=rtcp-fb:100 transport-cc
279
+ a=rtcp-fb:100 ccm fir
280
+ a=rtcp-fb:100 nack
281
+ a=rtcp-fb:100 nack pli
282
+ a=fmtp:100 profile-id=2
283
+ a=rtpmap:101 rtx/90000
284
+ a=fmtp:101 apt=100
285
+ a=rtpmap:112 H264/90000
286
+ a=rtcp-fb:112 goog-remb
287
+ a=rtcp-fb:112 transport-cc
288
+ a=rtcp-fb:112 ccm fir
289
+ a=rtcp-fb:112 nack
290
+ a=rtcp-fb:112 nack pli
291
+ a=fmtp:112 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
292
+ a=rtpmap:113 rtx/90000
293
+ a=fmtp:113 apt=112
294
+ a=rtpmap:116 red/90000
295
+ a=rtpmap:117 rtx/90000
296
+ a=fmtp:117 apt=116
297
+ a=rtpmap:118 ulpfec/90000
298
+ a=ssrc-group:FID 4072017687 3466187747
299
+ a=ssrc:4072017687 cname:mbGW+aeWMLFhPbBC
300
+ a=ssrc:4072017687 msid:04f96a2a-d107-4a12-b545-cfe1dc2c4d37 4a1ae8a7-6c89-44d3-a9a1-02abce0012c7
301
+ a=ssrc:3466187747 cname:mbGW+aeWMLFhPbBC
302
+ a=ssrc:3466187747 msid:04f96a2a-d107-4a12-b545-cfe1dc2c4d37 4a1ae8a7-6c89-44d3-a9a1-02abce0012c7
303
+ m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126
304
+ c=IN IP4 0.0.0.0
305
+ a=rtcp:9 IN IP4 0.0.0.0
306
+ a=ice-ufrag:GM64
307
+ a=ice-pwd:ANZFilRRlZJ3bg9AD40eRu7n
308
+ a=ice-options:trickle
309
+ a=fingerprint:sha-256 38:1F:02:E5:2A:49:9A:2A:D9:8E:B9:9B:4C:40:21:B7:F1:C4:27:8E:B5:68:D6:E0:91:08:D9:CB:2B:AC:B3:87
310
+ a=setup:actpass
311
+ a=mid:3
312
+ a=extmap:14 urn:ietf:params:rtp-hdrext:ssrc-audio-level
313
+ a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
314
+ a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
315
+ a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
316
+ a=sendonly
317
+ a=msid:- 75b8c290-1f46-464c-8dc5-a4a3bced313a
318
+ a=rtcp-mux
319
+ a=rtpmap:111 opus/48000/2
320
+ a=rtcp-fb:111 transport-cc
321
+ a=fmtp:111 minptime=10;usedtx=1;useinbandfec=1
322
+ a=rtpmap:63 red/48000/2
323
+ a=fmtp:63 111/111
324
+ a=rtpmap:9 G722/8000
325
+ a=rtpmap:0 PCMU/8000
326
+ a=rtpmap:8 PCMA/8000
327
+ a=rtpmap:13 CN/8000
328
+ a=rtpmap:110 telephone-event/48000
329
+ a=rtpmap:126 telephone-event/8000
330
+ a=ssrc:1281279951 cname:mbGW+aeWMLFhPbBC
331
+ a=ssrc:1281279951 msid:- 75b8c290-1f46-464c-8dc5-a4a3bced313a
332
+ `.trim();
@@ -1,5 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { setPreferredCodec, removeCodec, toggleDtx } from '../sdp-munging';
2
+ import {
3
+ enableHighQualityAudio,
4
+ removeCodec,
5
+ setPreferredCodec,
6
+ toggleDtx,
7
+ } from '../sdp-munging';
8
+ import { initialSdp as HQAudioSDP } from './hq-audio-sdp';
3
9
 
4
10
  const sdpWithRed = `v=0
5
11
  o=- 3265541491372987511 2 IN IP4 127.0.0.1
@@ -275,4 +281,10 @@ a=maxptime:40`;
275
281
  const dtxDisabledSdp = toggleDtx(dtxEnabledSdp, false);
276
282
  expect(dtxDisabledSdp.search('usedtx=0') !== -1).toBeTruthy();
277
283
  });
284
+
285
+ it('enables HighQuality audio for Opus', () => {
286
+ const sdpWithHighQualityAudio = enableHighQualityAudio(HQAudioSDP, '3');
287
+ expect(sdpWithHighQualityAudio).toContain('maxaveragebitrate=510000');
288
+ expect(sdpWithHighQualityAudio).toContain('stereo=1');
289
+ });
278
290
  });
@@ -1,3 +1,5 @@
1
+ import * as SDP from 'sdp-transform';
2
+
1
3
  type Media = {
2
4
  original: string;
3
5
  mediaWithPorts: string;
@@ -227,3 +229,50 @@ export const toggleDtx = (sdp: string, enable: boolean): string => {
227
229
  }
228
230
  return sdp;
229
231
  };
232
+
233
+ /**
234
+ * Enables high-quality audio through SDP munging for the given trackMid.
235
+ *
236
+ * @param sdp the SDP to munge.
237
+ * @param trackMid the trackMid.
238
+ * @param maxBitrate the max bitrate to set.
239
+ */
240
+ export const enableHighQualityAudio = (
241
+ sdp: string,
242
+ trackMid: string,
243
+ maxBitrate: number = 510000,
244
+ ): string => {
245
+ maxBitrate = Math.max(Math.min(maxBitrate, 510000), 96000);
246
+
247
+ const parsedSdp = SDP.parse(sdp);
248
+ const audioMedia = parsedSdp.media.find(
249
+ (m) => m.type === 'audio' && String(m.mid) === trackMid,
250
+ );
251
+
252
+ if (!audioMedia) return sdp;
253
+
254
+ const opusRtp = audioMedia.rtp.find((r) => r.codec === 'opus');
255
+ if (!opusRtp) return sdp;
256
+
257
+ const opusFmtp = audioMedia.fmtp.find((f) => f.payload === opusRtp.payload);
258
+ if (!opusFmtp) return sdp;
259
+
260
+ // enable stereo, if not already enabled
261
+ if (opusFmtp.config.match(/stereo=(\d)/)) {
262
+ opusFmtp.config = opusFmtp.config.replace(/stereo=(\d)/, 'stereo=1');
263
+ } else {
264
+ opusFmtp.config = `${opusFmtp.config};stereo=1`;
265
+ }
266
+
267
+ // set maxaveragebitrate, to the given value
268
+ if (opusFmtp.config.match(/maxaveragebitrate=(\d*)/)) {
269
+ opusFmtp.config = opusFmtp.config.replace(
270
+ /maxaveragebitrate=(\d*)/,
271
+ `maxaveragebitrate=${maxBitrate}`,
272
+ );
273
+ } else {
274
+ opusFmtp.config = `${opusFmtp.config};maxaveragebitrate=${maxBitrate}`;
275
+ }
276
+
277
+ return SDP.write(parsedSdp);
278
+ };