@stream-io/video-client 1.11.7 → 1.11.9

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.
@@ -7,7 +7,7 @@ import type { PreferredCodec } from '../types';
7
7
  * @param codecToRemove the codec to exclude from the list.
8
8
  * @param codecPreferencesSource the source of the codec preferences.
9
9
  */
10
- export declare const getPreferredCodecs: (kind: "audio" | "video", preferredCodec: string, codecToRemove?: string, codecPreferencesSource?: "sender" | "receiver") => RTCRtpCodec[] | undefined;
10
+ export declare const getPreferredCodecs: (kind: "audio" | "video", preferredCodec: string, codecToRemove: string | undefined, codecPreferencesSource: "sender" | "receiver") => RTCRtpCodec[] | undefined;
11
11
  /**
12
12
  * Returns a generic SDP for the given direction.
13
13
  * We use this SDP to send it as part of our JoinRequest so that the SFU
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.11.7",
3
+ "version": "1.11.9",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -215,6 +215,7 @@ export abstract class InputMediaDeviceManager<
215
215
  await this.applySettingsToStream();
216
216
  } catch (error) {
217
217
  this.state.setDevice(prevDeviceId);
218
+ await this.applySettingsToStream();
218
219
  throw error;
219
220
  }
220
221
  }
@@ -184,7 +184,7 @@ export const getAudioStream = async (
184
184
  const constraints: MediaStreamConstraints = {
185
185
  audio: {
186
186
  ...audioDeviceConstraints.audio,
187
- ...trackConstraints,
187
+ ...normalizeContraints(trackConstraints),
188
188
  },
189
189
  };
190
190
 
@@ -195,16 +195,6 @@ export const getAudioStream = async (
195
195
  });
196
196
  return await getStream(constraints);
197
197
  } catch (error) {
198
- if (error instanceof OverconstrainedError && trackConstraints?.deviceId) {
199
- const { deviceId, ...relaxedContraints } = trackConstraints;
200
- getLogger(['devices'])(
201
- 'warn',
202
- 'Failed to get audio stream, will try again with relaxed contraints',
203
- { error, constraints, relaxedContraints },
204
- );
205
- return getAudioStream(relaxedContraints);
206
- }
207
-
208
198
  getLogger(['devices'])('error', 'Failed to get audio stream', {
209
199
  error,
210
200
  constraints,
@@ -227,7 +217,7 @@ export const getVideoStream = async (
227
217
  const constraints: MediaStreamConstraints = {
228
218
  video: {
229
219
  ...videoDeviceConstraints.video,
230
- ...trackConstraints,
220
+ ...normalizeContraints(trackConstraints),
231
221
  },
232
222
  };
233
223
  try {
@@ -237,16 +227,6 @@ export const getVideoStream = async (
237
227
  });
238
228
  return await getStream(constraints);
239
229
  } catch (error) {
240
- if (error instanceof OverconstrainedError && trackConstraints?.deviceId) {
241
- const { deviceId, ...relaxedContraints } = trackConstraints;
242
- getLogger(['devices'])(
243
- 'warn',
244
- 'Failed to get video stream, will try again with relaxed contraints',
245
- { error, constraints, relaxedContraints },
246
- );
247
- return getVideoStream(relaxedContraints);
248
- }
249
-
250
230
  getLogger(['devices'])('error', 'Failed to get video stream', {
251
231
  error,
252
232
  constraints,
@@ -255,6 +235,20 @@ export const getVideoStream = async (
255
235
  }
256
236
  };
257
237
 
238
+ function normalizeContraints(constraints: MediaTrackConstraints | undefined) {
239
+ if (
240
+ constraints?.deviceId === 'default' ||
241
+ (typeof constraints?.deviceId === 'object' &&
242
+ 'exact' in constraints.deviceId &&
243
+ constraints.deviceId.exact === 'default')
244
+ ) {
245
+ const { deviceId, ...contraintsWithoutDeviceId } = constraints;
246
+ return contraintsWithoutDeviceId;
247
+ }
248
+
249
+ return constraints;
250
+ }
251
+
258
252
  /**
259
253
  * Prompts the user for a permission to share a screen.
260
254
  * If the user grants the permission, a screen sharing stream is returned. Throws otherwise.
@@ -188,4 +188,96 @@ a=simulcast:send q;h;f`;
188
188
  expect(target).not.toContain('VP9');
189
189
  expect(target).not.toContain('AV1');
190
190
  });
191
+
192
+ it('works with iOS RN vp8', () => {
193
+ const sdp = `v=0
194
+ o=- 2055959380019004946 2 IN IP4 127.0.0.1
195
+ s=-
196
+ t=0 0
197
+ a=group:BUNDLE 0
198
+ a=extmap-allow-mixed
199
+ a=msid-semantic: WMS FE2B3B06-61D7-4ACC-A4EF-76441C116E47
200
+ m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 103 35 36 104 105 106
201
+ c=IN IP4 0.0.0.0
202
+ a=rtcp:9 IN IP4 0.0.0.0
203
+ a=ice-ufrag:gCgh
204
+ a=ice-pwd:bz18EOLBL9+kSJfLiVOyU4RP
205
+ a=ice-options:trickle renomination
206
+ a=fingerprint:sha-256 6B:04:36:6D:E6:92:B5:68:DA:30:CF:53:46:14:49:5B:48:3E:B9:F7:06:B4:E8:85:B1:8C:B3:1C:EB:E8:F8:16
207
+ a=setup:actpass
208
+ a=mid:0
209
+ a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
210
+ a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
211
+ a=extmap:3 urn:3gpp:video-orientation
212
+ a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
213
+ a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
214
+ a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
215
+ a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
216
+ a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
217
+ a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
218
+ a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
219
+ a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
220
+ a=extmap:12 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension
221
+ a=extmap:14 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00
222
+ a=sendonly
223
+ a=msid:FE2B3B06-61D7-4ACC-A4EF-76441C116E47 93FCE555-1DA2-4721-901C-5D263E11DF23
224
+ a=rtcp-mux
225
+ a=rtcp-rsize
226
+ a=rtpmap:96 H264/90000
227
+ a=rtcp-fb:96 goog-remb
228
+ a=rtcp-fb:96 transport-cc
229
+ a=rtcp-fb:96 ccm fir
230
+ a=rtcp-fb:96 nack
231
+ a=rtcp-fb:96 nack pli
232
+ a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c29
233
+ a=rtpmap:97 rtx/90000
234
+ a=fmtp:97 apt=96
235
+ a=rtpmap:98 H264/90000
236
+ a=rtcp-fb:98 goog-remb
237
+ a=rtcp-fb:98 transport-cc
238
+ a=rtcp-fb:98 ccm fir
239
+ a=rtcp-fb:98 nack
240
+ a=rtcp-fb:98 nack pli
241
+ a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e029
242
+ a=rtpmap:99 rtx/90000
243
+ a=fmtp:99 apt=98
244
+ a=rtpmap:100 VP8/90000
245
+ a=rtcp-fb:100 goog-remb
246
+ a=rtcp-fb:100 transport-cc
247
+ a=rtcp-fb:100 ccm fir
248
+ a=rtcp-fb:100 nack
249
+ a=rtcp-fb:100 nack pli
250
+ a=rtpmap:101 rtx/90000
251
+ a=fmtp:101 apt=100
252
+ a=rtpmap:127 VP9/90000
253
+ a=rtcp-fb:127 goog-remb
254
+ a=rtcp-fb:127 transport-cc
255
+ a=rtcp-fb:127 ccm fir
256
+ a=rtcp-fb:127 nack
257
+ a=rtcp-fb:127 nack pli
258
+ a=rtpmap:103 rtx/90000
259
+ a=fmtp:103 apt=127
260
+ a=rtpmap:35 AV1/90000
261
+ a=rtcp-fb:35 goog-remb
262
+ a=rtcp-fb:35 transport-cc
263
+ a=rtcp-fb:35 ccm fir
264
+ a=rtcp-fb:35 nack
265
+ a=rtcp-fb:35 nack pli
266
+ a=rtpmap:36 rtx/90000
267
+ a=fmtp:36 apt=35
268
+ a=rtpmap:104 red/90000
269
+ a=rtpmap:105 rtx/90000
270
+ a=fmtp:105 apt=104
271
+ a=rtpmap:106 ulpfec/90000
272
+ a=rid:q send
273
+ a=rid:h send
274
+ a=rid:f send
275
+ a=simulcast:send q;h;f`;
276
+ const target = preserveCodec(sdp, '0', {
277
+ clockRate: 90000,
278
+ mimeType: 'video/VP8',
279
+ });
280
+ expect(target).toContain('VP8');
281
+ expect(target).not.toContain('VP9');
282
+ });
191
283
  });
@@ -156,12 +156,16 @@ export const preserveCodec = (
156
156
  // find the payload id of the desired codec
157
157
  const payloads = new Set<number>();
158
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
- ) {
159
+ if (rtp.codec.toLowerCase() !== codecName) continue;
160
+ const match =
161
+ // vp8 doesn't have any fmtp, we preserve it without any additional checks
162
+ codecName === 'vp8'
163
+ ? true
164
+ : media.fmtp.some(
165
+ (f) =>
166
+ f.payload === rtp.payload && equal(toSet(f.config), codecFmtp),
167
+ );
168
+ if (match) {
165
169
  payloads.add(rtp.payload);
166
170
  }
167
171
  }
@@ -29,6 +29,8 @@ import { Dispatcher } from './Dispatcher';
29
29
  import { VideoLayerSetting } from '../gen/video/sfu/event/events';
30
30
  import { TargetResolutionResponse } from '../gen/shims';
31
31
  import { withoutConcurrency } from '../helpers/concurrency';
32
+ import { isReactNative } from '../helpers/platforms';
33
+ import { isFirefox } from '../helpers/browsers';
32
34
 
33
35
  export type PublisherConstructorOpts = {
34
36
  sfuClient: StreamSfuClient;
@@ -256,6 +258,7 @@ export class Publisher {
256
258
  const codecPreferences = this.getCodecPreferences(
257
259
  trackType,
258
260
  trackType === TrackType.VIDEO ? codecInUse : undefined,
261
+ 'receiver',
259
262
  );
260
263
  if (!codecPreferences) return;
261
264
 
@@ -458,13 +461,14 @@ export class Publisher {
458
461
 
459
462
  private getCodecPreferences = (
460
463
  trackType: TrackType,
461
- preferredCodec?: string,
462
- codecPreferencesSource?: 'sender' | 'receiver',
464
+ preferredCodec: string | undefined,
465
+ codecPreferencesSource: 'sender' | 'receiver',
463
466
  ) => {
464
467
  if (trackType === TrackType.VIDEO) {
465
468
  return getPreferredCodecs(
466
469
  'video',
467
470
  preferredCodec || 'vp8',
471
+ undefined,
468
472
  codecPreferencesSource,
469
473
  );
470
474
  }
@@ -475,6 +479,7 @@ export class Publisher {
475
479
  'audio',
476
480
  preferredCodec ?? defaultAudioCodec,
477
481
  codecToRemove,
482
+ codecPreferencesSource,
478
483
  );
479
484
  }
480
485
  };
@@ -578,7 +583,9 @@ export class Publisher {
578
583
 
579
584
  private removeUnpreferredCodecs(sdp: string, trackType: TrackType): string {
580
585
  const opts = this.publishOptsForTrack.get(trackType);
581
- if (!opts || !opts.forceSingleCodec) return sdp;
586
+ const forceSingleCodec =
587
+ !!opts?.forceSingleCodec || isReactNative() || isFirefox();
588
+ if (!opts || !forceSingleCodec) return sdp;
582
589
 
583
590
  const codec = opts.forceCodec || getOptimalVideoCodec(opts.preferredCodec);
584
591
  const orderedCodecs = this.getCodecPreferences(trackType, codec, 'sender');
@@ -5,7 +5,7 @@ import './mocks/webrtc.mocks';
5
5
  describe('codecs', () => {
6
6
  it('should return preferred audio codec', () => {
7
7
  RTCRtpReceiver.getCapabilities = vi.fn().mockReturnValue(audioCodecs);
8
- const codecs = getPreferredCodecs('audio', 'red');
8
+ const codecs = getPreferredCodecs('audio', 'red', undefined, 'receiver');
9
9
  expect(codecs).toBeDefined();
10
10
  expect(codecs?.map((c) => c.mimeType)).toEqual([
11
11
  'audio/red',
@@ -20,7 +20,7 @@ describe('codecs', () => {
20
20
 
21
21
  it('should return preferred video codec', () => {
22
22
  RTCRtpReceiver.getCapabilities = vi.fn().mockReturnValue(videoCodecs);
23
- const codecs = getPreferredCodecs('video', 'vp8');
23
+ const codecs = getPreferredCodecs('video', 'vp8', undefined, 'receiver');
24
24
  expect(codecs).toBeDefined();
25
25
  // prettier-ignore
26
26
  expect(codecs?.map((c) => [c.mimeType, c.sdpFmtpLine])).toEqual([
@@ -40,12 +40,12 @@ describe('codecs', () => {
40
40
 
41
41
  it('should pick the baseline H264 codec', () => {
42
42
  RTCRtpReceiver.getCapabilities = vi.fn().mockReturnValue(videoCodecs);
43
- const codecs = getPreferredCodecs('video', 'h264');
43
+ const codecs = getPreferredCodecs('video', 'h264', undefined, 'receiver');
44
44
  expect(codecs).toBeDefined();
45
45
  // prettier-ignore
46
46
  expect(codecs?.map((c) => [c.mimeType, c.sdpFmtpLine])).toEqual([
47
- ['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f'],
48
47
  ['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f'],
48
+ ['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f'],
49
49
  ['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f'],
50
50
  ['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=640c1f'],
51
51
  ['video/rtx', undefined],
@@ -62,12 +62,12 @@ describe('codecs', () => {
62
62
  RTCRtpReceiver.getCapabilities = vi
63
63
  .fn()
64
64
  .mockReturnValue(videoCodecsFirefox);
65
- const codecs = getPreferredCodecs('video', 'h264');
65
+ const codecs = getPreferredCodecs('video', 'h264', undefined, 'receiver');
66
66
  expect(codecs).toBeDefined();
67
67
  // prettier-ignore
68
68
  expect(codecs?.map((c) => [c.mimeType, c.sdpFmtpLine])).toEqual([
69
- ['video/H264', 'profile-level-id=42e01f;level-asymmetry-allowed=1'],
70
69
  ['video/H264', 'profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1'],
70
+ ['video/H264', 'profile-level-id=42e01f;level-asymmetry-allowed=1'],
71
71
  ['video/VP8', 'max-fs=12288;max-fr=60'],
72
72
  ['video/rtx', undefined],
73
73
  ['video/VP9', 'max-fs=12288;max-fr=60'],
package/src/rtc/codecs.ts CHANGED
@@ -14,8 +14,8 @@ import type { PreferredCodec } from '../types';
14
14
  export const getPreferredCodecs = (
15
15
  kind: 'audio' | 'video',
16
16
  preferredCodec: string,
17
- codecToRemove?: string,
18
- codecPreferencesSource: 'sender' | 'receiver' = 'receiver',
17
+ codecToRemove: string | undefined,
18
+ codecPreferencesSource: 'sender' | 'receiver',
19
19
  ): RTCRtpCodec[] | undefined => {
20
20
  const source =
21
21
  codecPreferencesSource === 'receiver' ? RTCRtpReceiver : RTCRtpSender;
@@ -60,12 +60,7 @@ export const getPreferredCodecs = (
60
60
  continue;
61
61
  }
62
62
 
63
- // packetization-mode mode is optional; when not present it defaults to 0:
64
- // https://datatracker.ietf.org/doc/html/rfc6184#section-6.2
65
- if (
66
- sdpFmtpLine.includes('packetization-mode=0') ||
67
- !sdpFmtpLine.includes('packetization-mode')
68
- ) {
63
+ if (sdpFmtpLine.includes('packetization-mode=1')) {
69
64
  preferred.unshift(codec);
70
65
  } else {
71
66
  preferred.push(codec);
@@ -107,7 +102,9 @@ export const getOptimalVideoCodec = (
107
102
  if (isReactNative()) {
108
103
  const os = getOSInfo()?.name.toLowerCase();
109
104
  if (os === 'android') return preferredOr(preferredCodec, 'vp8');
110
- if (os === 'ios' || os === 'ipados') return 'h264';
105
+ if (os === 'ios' || os === 'ipados') {
106
+ return supportsH264Baseline() ? 'h264' : 'vp8';
107
+ }
111
108
  return preferredOr(preferredCodec, 'h264');
112
109
  }
113
110
  if (isSafari()) return 'h264';
@@ -139,6 +136,20 @@ const preferredOr = (
139
136
  : fallback;
140
137
  };
141
138
 
139
+ /**
140
+ * Returns whether the platform supports the H264 baseline codec.
141
+ */
142
+ const supportsH264Baseline = (): boolean => {
143
+ if (!('getCapabilities' in RTCRtpSender)) return false;
144
+ const capabilities = RTCRtpSender.getCapabilities('video');
145
+ if (!capabilities) return false;
146
+ return capabilities.codecs.some(
147
+ (c) =>
148
+ c.mimeType.toLowerCase() === 'video/h264' &&
149
+ c.sdpFmtpLine?.includes('profile-level-id=42e01f'),
150
+ );
151
+ };
152
+
142
153
  /**
143
154
  * Returns whether the codec is an SVC codec.
144
155
  *