@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.
- package/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +45 -27
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +45 -27
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +45 -27
- package/dist/index.es.js.map +1 -1
- package/dist/src/rtc/codecs.d.ts +1 -1
- package/package.json +1 -1
- package/src/devices/InputMediaDeviceManager.ts +1 -0
- package/src/devices/devices.ts +16 -22
- package/src/helpers/__tests__/sdp-munging.test.ts +92 -0
- package/src/helpers/sdp-munging.ts +10 -6
- package/src/rtc/Publisher.ts +10 -3
- package/src/rtc/__tests__/codecs.test.ts +6 -6
- package/src/rtc/codecs.ts +20 -9
package/dist/src/rtc/codecs.d.ts
CHANGED
|
@@ -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
|
|
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
package/src/devices/devices.ts
CHANGED
|
@@ -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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
}
|
package/src/rtc/Publisher.ts
CHANGED
|
@@ -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
|
|
462
|
-
codecPreferencesSource
|
|
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
|
-
|
|
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
|
|
18
|
-
codecPreferencesSource: 'sender' | '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
|
-
|
|
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')
|
|
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
|
*
|