@stream-io/video-client 1.11.3 → 1.11.4

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.
@@ -2,6 +2,10 @@
2
2
  * Returns an SDP with DTX enabled or disabled.
3
3
  */
4
4
  export declare const toggleDtx: (sdp: string, enable: boolean) => string;
5
+ /**
6
+ * Returns and SDP with all the codecs except the given codec removed.
7
+ */
8
+ export declare const preserveCodec: (sdp: string, mid: string, codec: RTCRtpCodec) => string;
5
9
  /**
6
10
  * Enables high-quality audio through SDP munging for the given trackMid.
7
11
  *
@@ -124,6 +124,7 @@ export declare class Publisher {
124
124
  * @param options the optional offer options to use.
125
125
  */
126
126
  private negotiate;
127
+ private removeUnpreferredCodecs;
127
128
  private enableHighQualityAudio;
128
129
  /**
129
130
  * Returns a list of tracks that are currently being published.
@@ -129,6 +129,12 @@ export type PublishOptions = {
129
129
  * Use with caution.
130
130
  */
131
131
  forceCodec?: PreferredCodec;
132
+ /**
133
+ * When using a preferred codec, force the use of a single codec.
134
+ * Enabling this, it will remove all other supported codecs from the SDP.
135
+ * Defaults to false.
136
+ */
137
+ forceSingleCodec?: boolean;
132
138
  /**
133
139
  * The preferred scalability to use when publishing the video stream.
134
140
  * Applicable only for SVC codecs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.11.3",
3
+ "version": "1.11.4",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -1,5 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { enableHighQualityAudio, toggleDtx } from '../sdp-munging';
2
+ import {
3
+ enableHighQualityAudio,
4
+ preserveCodec,
5
+ toggleDtx,
6
+ } from '../sdp-munging';
3
7
  import { initialSdp as HQAudioSDP } from './hq-audio-sdp';
4
8
 
5
9
  describe('sdp-munging', () => {
@@ -21,4 +25,167 @@ a=maxptime:40`;
21
25
  expect(sdpWithHighQualityAudio).toContain('maxaveragebitrate=510000');
22
26
  expect(sdpWithHighQualityAudio).toContain('stereo=1');
23
27
  });
28
+
29
+ it('preserves the preferred codec', () => {
30
+ const sdp = `v=0
31
+ o=- 8608371809202407637 2 IN IP4 127.0.0.1
32
+ s=-
33
+ t=0 0
34
+ a=extmap-allow-mixed
35
+ a=msid-semantic: WMS 52fafc21-b8bb-4f4f-8072-86a29cb6590e
36
+ a=group:BUNDLE 0
37
+ m=video 9 UDP/TLS/RTP/SAVPF 98 100 99 101
38
+ c=IN IP4 0.0.0.0
39
+ a=rtpmap:98 VP9/90000
40
+ a=rtpmap:99 rtx/90000
41
+ a=rtpmap:100 VP9/90000
42
+ a=rtpmap:101 rtx/90000
43
+ a=fmtp:98 profile-id=0
44
+ a=fmtp:99 apt=98
45
+ a=fmtp:100 profile-id=2
46
+ a=fmtp:101 apt=100
47
+ a=rtcp:9 IN IP4 0.0.0.0
48
+ a=rtcp-fb:98 goog-remb
49
+ a=rtcp-fb:98 transport-cc
50
+ a=rtcp-fb:98 ccm fir
51
+ a=rtcp-fb:98 nack
52
+ a=rtcp-fb:98 nack pli
53
+ a=rtcp-fb:100 goog-remb
54
+ a=rtcp-fb:100 transport-cc
55
+ a=rtcp-fb:100 ccm fir
56
+ a=rtcp-fb:100 nack
57
+ a=rtcp-fb:100 nack pli
58
+ a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
59
+ a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
60
+ a=extmap:3 urn:3gpp:video-orientation
61
+ a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
62
+ a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
63
+ a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
64
+ a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
65
+ a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
66
+ a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
67
+ a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
68
+ a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
69
+ a=extmap:12 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension
70
+ a=extmap:14 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00
71
+ a=setup:actpass
72
+ a=mid:0
73
+ a=msid:52fafc21-b8bb-4f4f-8072-86a29cb6590e 1bd1c5c2-d3cc-4490-ac0c-70b187242232
74
+ a=sendonly
75
+ a=ice-ufrag:LvRk
76
+ a=ice-pwd:IpBRr2Rrg9TkOgayjYqALhPY
77
+ a=fingerprint:sha-256 18:DE:8F:ED:E6:A2:0C:99:A8:25:AB:C9:F8:3D:91:4C:3E:9F:B4:1F:22:87:A7:3C:85:8F:F3:51:09:A7:E3:FA
78
+ a=ice-options:trickle
79
+ a=ssrc:3192778601 cname:yYSN5R+RG2j3luO7
80
+ a=ssrc:3192778601 msid:52fafc21-b8bb-4f4f-8072-86a29cb6590e 1bd1c5c2-d3cc-4490-ac0c-70b187242232
81
+ a=ssrc:283365205 cname:yYSN5R+RG2j3luO7
82
+ a=ssrc:283365205 msid:52fafc21-b8bb-4f4f-8072-86a29cb6590e 1bd1c5c2-d3cc-4490-ac0c-70b187242232
83
+ a=ssrc-group:FID 3192778601 283365205
84
+ a=rtcp-mux
85
+ a=rtcp-rsize`;
86
+ const target = preserveCodec(sdp, '0', {
87
+ mimeType: 'video/VP9',
88
+ clockRate: 90000,
89
+ sdpFmtpLine: 'profile-id=0',
90
+ });
91
+ expect(target).toContain('VP9');
92
+ expect(target).not.toContain('profile-id=2');
93
+ });
94
+
95
+ it('handles ios munging', () => {
96
+ const sdp = `v=0
97
+ o=- 525780719364332676 2 IN IP4 127.0.0.1
98
+ s=-
99
+ t=0 0
100
+ a=group:BUNDLE 0
101
+ a=extmap-allow-mixed
102
+ a=msid-semantic: WMS BF3AFE62-88F8-4189-99D7-7CAE159205E3
103
+ m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 103 35 36 104 105 106
104
+ c=IN IP4 0.0.0.0
105
+ a=rtcp:9 IN IP4 0.0.0.0
106
+ a=ice-ufrag:SAkq
107
+ a=ice-pwd:FYHHro0VWRO8CjI/M1VG5vRw
108
+ a=ice-options:trickle renomination
109
+ a=fingerprint:sha-256 03:5B:16:0E:E1:7B:FE:4F:9A:5C:AC:CF:08:21:4B:49:CE:53:79:E6:97:AE:4E:73:F8:43:34:C3:11:F7:6D:E7
110
+ a=setup:actpass
111
+ a=mid:0
112
+ a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
113
+ a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
114
+ a=extmap:3 urn:3gpp:video-orientation
115
+ a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
116
+ a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
117
+ a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
118
+ a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
119
+ a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
120
+ a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
121
+ a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
122
+ a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
123
+ a=extmap:12 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension
124
+ a=extmap:14 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00
125
+ a=sendonly
126
+ a=msid:BF3AFE62-88F8-4189-99D7-7CAE159205E3 6013DC02-A0A5-43A9-9D41-9D4A89648A42
127
+ a=rtcp-mux
128
+ a=rtcp-rsize
129
+ a=rtpmap:96 H264/90000
130
+ a=rtcp-fb:96 goog-remb
131
+ a=rtcp-fb:96 transport-cc
132
+ a=rtcp-fb:96 ccm fir
133
+ a=rtcp-fb:96 nack
134
+ a=rtcp-fb:96 nack pli
135
+ a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c29
136
+ a=rtpmap:97 rtx/90000
137
+ a=fmtp:97 apt=96
138
+ a=rtpmap:98 H264/90000
139
+ a=rtcp-fb:98 goog-remb
140
+ a=rtcp-fb:98 transport-cc
141
+ a=rtcp-fb:98 ccm fir
142
+ a=rtcp-fb:98 nack
143
+ a=rtcp-fb:98 nack pli
144
+ a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e029
145
+ a=rtpmap:99 rtx/90000
146
+ a=fmtp:99 apt=98
147
+ a=rtpmap:100 VP8/90000
148
+ a=rtcp-fb:100 goog-remb
149
+ a=rtcp-fb:100 transport-cc
150
+ a=rtcp-fb:100 ccm fir
151
+ a=rtcp-fb:100 nack
152
+ a=rtcp-fb:100 nack pli
153
+ a=rtpmap:101 rtx/90000
154
+ a=fmtp:101 apt=100
155
+ a=rtpmap:127 VP9/90000
156
+ a=rtcp-fb:127 goog-remb
157
+ a=rtcp-fb:127 transport-cc
158
+ a=rtcp-fb:127 ccm fir
159
+ a=rtcp-fb:127 nack
160
+ a=rtcp-fb:127 nack pli
161
+ a=rtpmap:103 rtx/90000
162
+ a=fmtp:103 apt=127
163
+ a=rtpmap:35 AV1/90000
164
+ a=rtcp-fb:35 goog-remb
165
+ a=rtcp-fb:35 transport-cc
166
+ a=rtcp-fb:35 ccm fir
167
+ a=rtcp-fb:35 nack
168
+ a=rtcp-fb:35 nack pli
169
+ a=rtpmap:36 rtx/90000
170
+ a=fmtp:36 apt=35
171
+ a=rtpmap:104 red/90000
172
+ a=rtpmap:105 rtx/90000
173
+ a=fmtp:105 apt=104
174
+ a=rtpmap:106 ulpfec/90000
175
+ a=rid:q send
176
+ a=rid:h send
177
+ a=rid:f send
178
+ a=simulcast:send q;h;f`;
179
+ const target = preserveCodec(sdp, '0', {
180
+ mimeType: 'video/H264',
181
+ clockRate: 90000,
182
+ sdpFmtpLine:
183
+ 'profile-level-id=42e029;packetization-mode=1;level-asymmetry-allowed=1',
184
+ });
185
+ expect(target).toContain('H264');
186
+ expect(target).toContain('profile-level-id=42e029');
187
+ expect(target).not.toContain('profile-level-id=640c29');
188
+ expect(target).not.toContain('VP9');
189
+ expect(target).not.toContain('AV1');
190
+ });
24
191
  });
@@ -129,6 +129,61 @@ export const toggleDtx = (sdp: string, enable: boolean): string => {
129
129
  return sdp.replace(opusFmtp.original, newFmtp);
130
130
  };
131
131
 
132
+ /**
133
+ * Returns and SDP with all the codecs except the given codec removed.
134
+ */
135
+ export const preserveCodec = (
136
+ sdp: string,
137
+ mid: string,
138
+ codec: RTCRtpCodec,
139
+ ): string => {
140
+ const [kind, codecName] = codec.mimeType.toLowerCase().split('/');
141
+
142
+ const toSet = (fmtpLine: string) =>
143
+ new Set(fmtpLine.split(';').map((f) => f.trim().toLowerCase()));
144
+
145
+ const equal = (a: Set<string>, b: Set<string>) => {
146
+ if (a.size !== b.size) return false;
147
+ for (const item of a) if (!b.has(item)) return false;
148
+ return true;
149
+ };
150
+
151
+ const codecFmtp = toSet(codec.sdpFmtpLine || '');
152
+ const parsedSdp = SDP.parse(sdp);
153
+ for (const media of parsedSdp.media) {
154
+ if (media.type !== kind || String(media.mid) !== mid) continue;
155
+
156
+ // find the payload id of the desired codec
157
+ const payloads = new Set<number>();
158
+ for (const rtp of media.rtp) {
159
+ if (
160
+ rtp.codec.toLowerCase() === codecName &&
161
+ media.fmtp.some(
162
+ (f) => f.payload === rtp.payload && equal(toSet(f.config), codecFmtp),
163
+ )
164
+ ) {
165
+ payloads.add(rtp.payload);
166
+ }
167
+ }
168
+
169
+ // find the corresponding rtx codec by matching apt=<preserved-codec-payload>
170
+ for (const fmtp of media.fmtp) {
171
+ const match = fmtp.config.match(/(apt)=(\d+)/);
172
+ if (!match) continue;
173
+ const [, , preservedCodecPayload] = match;
174
+ if (payloads.has(Number(preservedCodecPayload))) {
175
+ payloads.add(fmtp.payload);
176
+ }
177
+ }
178
+
179
+ media.rtp = media.rtp.filter((r) => payloads.has(r.payload));
180
+ media.fmtp = media.fmtp.filter((f) => payloads.has(f.payload));
181
+ media.rtcpFb = media.rtcpFb?.filter((f) => payloads.has(f.payload));
182
+ media.payloads = Array.from(payloads).join(' ');
183
+ }
184
+ return SDP.write(parsedSdp);
185
+ };
186
+
132
187
  /**
133
188
  * Enables high-quality audio through SDP munging for the given trackMid.
134
189
  *
@@ -20,6 +20,7 @@ import { PublishOptions } from '../types';
20
20
  import {
21
21
  enableHighQualityAudio,
22
22
  extractMid,
23
+ preserveCodec,
23
24
  toggleDtx,
24
25
  } from '../helpers/sdp-munging';
25
26
  import { Logger } from '../coordinator/connection/types';
@@ -530,6 +531,12 @@ export class Publisher {
530
531
  if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
531
532
  offer.sdp = this.enableHighQualityAudio(offer.sdp);
532
533
  }
534
+ if (this.isPublishing(TrackType.VIDEO)) {
535
+ // Hotfix for platforms that don't respect the ordered codec list
536
+ // (Firefox, Android, Linux, etc...).
537
+ // We remove all the codecs from the SDP except the one we want to use.
538
+ offer.sdp = this.removeUnpreferredCodecs(offer.sdp, TrackType.VIDEO);
539
+ }
533
540
  }
534
541
 
535
542
  const trackInfos = this.getAnnouncedTracks(offer.sdp);
@@ -564,6 +571,23 @@ export class Publisher {
564
571
  );
565
572
  };
566
573
 
574
+ private removeUnpreferredCodecs(sdp: string, trackType: TrackType): string {
575
+ const opts = this.publishOptsForTrack.get(trackType);
576
+ if (!opts || !opts.forceSingleCodec) return sdp;
577
+
578
+ const codec = opts.forceCodec || opts.preferredCodec;
579
+ const orderedCodecs = this.getCodecPreferences(trackType, codec);
580
+ if (!orderedCodecs || orderedCodecs.length === 0) return sdp;
581
+
582
+ const transceiver = this.transceiverCache.get(trackType);
583
+ if (!transceiver) return sdp;
584
+
585
+ const index = this.transceiverInitOrder.indexOf(trackType);
586
+ const mid = extractMid(transceiver, index, sdp);
587
+ const [codecToPreserve] = orderedCodecs;
588
+ return preserveCodec(sdp, mid, codecToPreserve);
589
+ }
590
+
567
591
  private enableHighQualityAudio = (sdp: string) => {
568
592
  const transceiver = this.transceiverCache.get(TrackType.SCREEN_SHARE_AUDIO);
569
593
  if (!transceiver) return sdp;
package/src/rtc/codecs.ts CHANGED
@@ -50,7 +50,7 @@ export const getPreferredCodecs = (
50
50
  }
51
51
 
52
52
  const sdpFmtpLine = codec.sdpFmtpLine;
53
- if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=42e01f')) {
53
+ if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=42')) {
54
54
  // this is not the baseline h264 codec, prioritize it lower
55
55
  partiallyPreferred.push(codec);
56
56
  continue;
package/src/types.ts CHANGED
@@ -167,6 +167,12 @@ export type PublishOptions = {
167
167
  * Use with caution.
168
168
  */
169
169
  forceCodec?: PreferredCodec;
170
+ /**
171
+ * When using a preferred codec, force the use of a single codec.
172
+ * Enabling this, it will remove all other supported codecs from the SDP.
173
+ * Defaults to false.
174
+ */
175
+ forceSingleCodec?: boolean;
170
176
  /**
171
177
  * The preferred scalability to use when publishing the video stream.
172
178
  * Applicable only for SVC codecs.