@stream-io/video-client 1.40.3 → 1.41.0
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 +11 -0
- package/dist/index.browser.es.js +164 -29
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +171 -28
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +164 -29
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +4 -4
- package/dist/src/devices/SpeakerManager.d.ts +0 -2
- package/dist/src/gen/coordinator/index.d.ts +269 -0
- package/dist/src/rtc/helpers/sdp.d.ts +8 -0
- package/dist/src/store/CallState.d.ts +21 -2
- package/dist/src/types.d.ts +29 -1
- package/package.json +1 -1
- package/src/Call.ts +26 -5
- package/src/devices/SpeakerManager.ts +0 -2
- package/src/devices/__tests__/MicrophoneManager.test.ts +34 -23
- package/src/devices/__tests__/mocks.ts +14 -12
- package/src/events/__tests__/call.test.ts +4 -5
- package/src/gen/coordinator/index.ts +293 -0
- package/src/rtc/Publisher.ts +1 -1
- package/src/rtc/helpers/__tests__/sdp.startBitrate.test.ts +105 -0
- package/src/rtc/helpers/sdp.ts +30 -12
- package/src/store/CallState.ts +72 -9
- package/src/store/__tests__/CallState.test.ts +462 -106
- package/src/types.ts +42 -0
|
@@ -109,4 +109,109 @@ a=fmtp:103 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001
|
|
|
109
109
|
'a=fmtp:103 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f;x-google-start-bitrate=1050',
|
|
110
110
|
);
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
it('clamps to 300kbps when factor is 0', () => {
|
|
114
|
+
const offerSdp = `v=0
|
|
115
|
+
o=- 123 2 IN IP4 127.0.0.1
|
|
116
|
+
s=-
|
|
117
|
+
t=0 0
|
|
118
|
+
m=video 9 UDP/TLS/RTP/SAVPF 98
|
|
119
|
+
c=IN IP4 0.0.0.0
|
|
120
|
+
a=mid:0
|
|
121
|
+
a=rtpmap:98 VP9/90000
|
|
122
|
+
a=fmtp:98 profile-id=0
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
const result = setStartBitrate(offerSdp, 1500, 0, '0'); // 0 -> clamped to 300
|
|
126
|
+
expect(result).toContain(
|
|
127
|
+
'a=fmtp:98 profile-id=0;x-google-start-bitrate=300',
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('uses max bitrate when factor is 1', () => {
|
|
132
|
+
const offerSdp = `v=0
|
|
133
|
+
o=- 123 2 IN IP4 127.0.0.1
|
|
134
|
+
s=-
|
|
135
|
+
t=0 0
|
|
136
|
+
m=video 9 UDP/TLS/RTP/SAVPF 98
|
|
137
|
+
c=IN IP4 0.0.0.0
|
|
138
|
+
a=mid:0
|
|
139
|
+
a=rtpmap:98 VP9/90000
|
|
140
|
+
a=fmtp:98 profile-id=0
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const result = setStartBitrate(offerSdp, 1500, 1, '0'); // should be 1500
|
|
144
|
+
expect(result).toContain(
|
|
145
|
+
'a=fmtp:98 profile-id=0;x-google-start-bitrate=1500',
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('clamps to max bitrate when factor exceeds 1', () => {
|
|
150
|
+
const offerSdp = `v=0
|
|
151
|
+
o=- 123 2 IN IP4 127.0.0.1
|
|
152
|
+
s=-
|
|
153
|
+
t=0 0
|
|
154
|
+
m=video 9 UDP/TLS/RTP/SAVPF 98
|
|
155
|
+
c=IN IP4 0.0.0.0
|
|
156
|
+
a=mid:0
|
|
157
|
+
a=rtpmap:98 VP9/90000
|
|
158
|
+
a=fmtp:98 profile-id=0
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
const result = setStartBitrate(offerSdp, 1500, 1.5, '0'); // 2250 -> clamped to 1500
|
|
162
|
+
expect(result).toContain(
|
|
163
|
+
'a=fmtp:98 profile-id=0;x-google-start-bitrate=1500',
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('creates fmtp line when none exists for target codec', () => {
|
|
168
|
+
const offerSdp = `v=0
|
|
169
|
+
o=- 123 2 IN IP4 127.0.0.1
|
|
170
|
+
s=-
|
|
171
|
+
t=0 0
|
|
172
|
+
m=video 9 UDP/TLS/RTP/SAVPF 98
|
|
173
|
+
c=IN IP4 0.0.0.0
|
|
174
|
+
a=mid:0
|
|
175
|
+
a=rtpmap:98 VP9/90000
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
const result = setStartBitrate(offerSdp, 1500, 0.5, '0'); // 750kbps
|
|
179
|
+
expect(result).toContain('a=fmtp:98 x-google-start-bitrate=750');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('creates fmtp lines for multiple codecs without existing fmtp', () => {
|
|
183
|
+
const offerSdp = `v=0
|
|
184
|
+
o=- 123 2 IN IP4 127.0.0.1
|
|
185
|
+
s=-
|
|
186
|
+
t=0 0
|
|
187
|
+
m=video 9 UDP/TLS/RTP/SAVPF 98 103
|
|
188
|
+
c=IN IP4 0.0.0.0
|
|
189
|
+
a=mid:0
|
|
190
|
+
a=rtpmap:98 VP9/90000
|
|
191
|
+
a=rtpmap:103 H264/90000
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
const result = setStartBitrate(offerSdp, 1500, 0.5, '0'); // 750kbps
|
|
195
|
+
expect(result).toContain('a=fmtp:98 x-google-start-bitrate=750');
|
|
196
|
+
expect(result).toContain('a=fmtp:103 x-google-start-bitrate=750');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('never exceeds maxBitrateKbps even when it is below 300', () => {
|
|
200
|
+
const offerSdp = `v=0
|
|
201
|
+
o=- 123 2 IN IP4 127.0.0.1
|
|
202
|
+
s=-
|
|
203
|
+
t=0 0
|
|
204
|
+
m=video 9 UDP/TLS/RTP/SAVPF 98
|
|
205
|
+
c=IN IP4 0.0.0.0
|
|
206
|
+
a=mid:0
|
|
207
|
+
a=rtpmap:98 VP9/90000
|
|
208
|
+
a=fmtp:98 profile-id=0
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
// maxBitrateKbps (200) is less than minimum (300), should clamp to maxBitrateKbps
|
|
212
|
+
const result = setStartBitrate(offerSdp, 200, 1, '0');
|
|
213
|
+
expect(result).toContain(
|
|
214
|
+
'a=fmtp:98 profile-id=0;x-google-start-bitrate=200',
|
|
215
|
+
);
|
|
216
|
+
});
|
|
112
217
|
});
|
package/src/rtc/helpers/sdp.ts
CHANGED
|
@@ -29,11 +29,13 @@ export const extractMid = (
|
|
|
29
29
|
return String(transceiverInitIndex);
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
* Sets the start bitrate for the VP9 and
|
|
32
|
+
/**
|
|
33
|
+
* Sets the start bitrate for the VP9, H264, and AV1 codecs in the SDP.
|
|
34
34
|
*
|
|
35
35
|
* @param offerSdp the offer SDP to modify.
|
|
36
|
-
* @param
|
|
36
|
+
* @param maxBitrateKbps the maximum bitrate in kbps.
|
|
37
|
+
* @param startBitrateFactor the factor (0-1) to multiply with maxBitrateKbps to get the start bitrate.
|
|
38
|
+
* @param targetMid the media ID to target.
|
|
37
39
|
*/
|
|
38
40
|
export const setStartBitrate = (
|
|
39
41
|
offerSdp: string,
|
|
@@ -42,9 +44,10 @@ export const setStartBitrate = (
|
|
|
42
44
|
targetMid: string,
|
|
43
45
|
): string => {
|
|
44
46
|
// start bitrate should be between 300kbps and max-bitrate-kbps
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// Clamp to max first, then ensure minimum of 300 (but never exceed max)
|
|
48
|
+
const startBitrate = Math.min(
|
|
49
|
+
maxBitrateKbps,
|
|
50
|
+
Math.max(300, startBitrateFactor * maxBitrateKbps),
|
|
48
51
|
);
|
|
49
52
|
const parsedSdp = parse(offerSdp);
|
|
50
53
|
const targetCodecs = new Set(['av1', 'vp9', 'h264']);
|
|
@@ -56,13 +59,28 @@ export const setStartBitrate = (
|
|
|
56
59
|
for (const rtp of media.rtp) {
|
|
57
60
|
if (!targetCodecs.has(rtp.codec.toLowerCase())) continue;
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Find existing fmtp entry for this payload
|
|
63
|
+
// Guard against media.fmtp being undefined when SDP has no a=fmtp lines
|
|
64
|
+
const fmtpList = media.fmtp ?? (media.fmtp = []);
|
|
65
|
+
const existingFmtp = fmtpList.find(
|
|
66
|
+
(fmtp) => fmtp.payload === rtp.payload,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (existingFmtp) {
|
|
70
|
+
// Append to existing fmtp if not already present
|
|
71
|
+
// Guard against undefined or empty config from malformed SDP
|
|
72
|
+
const config = existingFmtp.config ?? '';
|
|
73
|
+
if (!config.includes('x-google-start-bitrate')) {
|
|
74
|
+
existingFmtp.config = config
|
|
75
|
+
? `${config};x-google-start-bitrate=${startBitrate}`
|
|
76
|
+
: `x-google-start-bitrate=${startBitrate}`;
|
|
65
77
|
}
|
|
78
|
+
} else {
|
|
79
|
+
// Create new fmtp entry if none exists
|
|
80
|
+
fmtpList.push({
|
|
81
|
+
payload: rtp.payload,
|
|
82
|
+
config: `x-google-start-bitrate=${startBitrate}`,
|
|
83
|
+
});
|
|
66
84
|
}
|
|
67
85
|
}
|
|
68
86
|
}
|
package/src/store/CallState.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { Patch } from './rxUtils';
|
|
|
12
12
|
import * as RxUtils from './rxUtils';
|
|
13
13
|
import { CallingState } from './CallingState';
|
|
14
14
|
import {
|
|
15
|
+
type CallRecordingType,
|
|
15
16
|
type ClosedCaptionsSettings,
|
|
16
17
|
type StreamVideoParticipant,
|
|
17
18
|
type StreamVideoParticipantPatch,
|
|
@@ -49,12 +50,13 @@ import {
|
|
|
49
50
|
import { Timestamp } from '../gen/google/protobuf/timestamp';
|
|
50
51
|
import { ReconnectDetails } from '../gen/video/sfu/event/events';
|
|
51
52
|
import {
|
|
53
|
+
CallGrants,
|
|
52
54
|
CallState as SfuCallState,
|
|
53
55
|
Pin,
|
|
54
56
|
TrackType,
|
|
55
|
-
CallGrants,
|
|
56
57
|
} from '../gen/video/sfu/models/models';
|
|
57
58
|
import { Comparator, defaultSortPreset } from '../sorting';
|
|
59
|
+
import { ensureExhausted } from '../helpers/ensureExhausted';
|
|
58
60
|
import { hasScreenShare } from '../helpers/participantUtils';
|
|
59
61
|
import { videoLoggerSystem } from '../logger';
|
|
60
62
|
|
|
@@ -96,6 +98,8 @@ export class CallState {
|
|
|
96
98
|
undefined,
|
|
97
99
|
);
|
|
98
100
|
private recordingSubject = new BehaviorSubject<boolean>(false);
|
|
101
|
+
private individualRecordingSubject = new BehaviorSubject<boolean>(false);
|
|
102
|
+
private rawRecordingSubject = new BehaviorSubject<boolean>(false);
|
|
99
103
|
private sessionSubject = new BehaviorSubject<CallSessionResponse | undefined>(
|
|
100
104
|
undefined,
|
|
101
105
|
);
|
|
@@ -277,6 +281,16 @@ export class CallState {
|
|
|
277
281
|
*/
|
|
278
282
|
recording$: Observable<boolean>;
|
|
279
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Will provide the recording state of this call.
|
|
286
|
+
*/
|
|
287
|
+
individualRecording$: Observable<boolean>;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Will provide the recording state of this call.
|
|
291
|
+
*/
|
|
292
|
+
rawRecording$: Observable<boolean>;
|
|
293
|
+
|
|
280
294
|
/**
|
|
281
295
|
* Will provide the session data of this call.
|
|
282
296
|
*/
|
|
@@ -462,6 +476,8 @@ export class CallState {
|
|
|
462
476
|
);
|
|
463
477
|
this.participantCount$ = duc(this.participantCountSubject);
|
|
464
478
|
this.recording$ = duc(this.recordingSubject);
|
|
479
|
+
this.individualRecording$ = duc(this.individualRecordingSubject);
|
|
480
|
+
this.rawRecording$ = duc(this.rawRecordingSubject);
|
|
465
481
|
this.transcribing$ = duc(this.transcribingSubject);
|
|
466
482
|
this.captioning$ = duc(this.captioningSubject);
|
|
467
483
|
|
|
@@ -530,12 +546,15 @@ export class CallState {
|
|
|
530
546
|
},
|
|
531
547
|
'call.permissions_updated': this.updateOwnCapabilities,
|
|
532
548
|
'call.reaction_new': this.updateParticipantReaction,
|
|
533
|
-
'call.recording_started': () =>
|
|
534
|
-
this.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
549
|
+
'call.recording_started': (e) => {
|
|
550
|
+
this.updateFromRecordingEvent(e.recording_type, true);
|
|
551
|
+
},
|
|
552
|
+
'call.recording_stopped': (e) => {
|
|
553
|
+
this.updateFromRecordingEvent(e.recording_type, false);
|
|
554
|
+
},
|
|
555
|
+
'call.recording_failed': (e) => {
|
|
556
|
+
this.updateFromRecordingEvent(e.recording_type, false);
|
|
557
|
+
},
|
|
539
558
|
'call.rejected': (e) => this.updateFromCallResponse(e.call),
|
|
540
559
|
'call.ring': (e) => this.updateFromCallResponse(e.call),
|
|
541
560
|
'call.missed': (e) => this.updateFromCallResponse(e.call),
|
|
@@ -919,12 +938,26 @@ export class CallState {
|
|
|
919
938
|
}
|
|
920
939
|
|
|
921
940
|
/**
|
|
922
|
-
* Will provide the recording state of this call.
|
|
941
|
+
* Will provide the composite recording state of this call.
|
|
923
942
|
*/
|
|
924
943
|
get recording() {
|
|
925
944
|
return this.getCurrentValue(this.recording$);
|
|
926
945
|
}
|
|
927
946
|
|
|
947
|
+
/**
|
|
948
|
+
* Will provide the individual recording state of this call.
|
|
949
|
+
*/
|
|
950
|
+
get individualRecording() {
|
|
951
|
+
return this.getCurrentValue(this.individualRecording$);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Will provide the raw recording state of this call.
|
|
956
|
+
*/
|
|
957
|
+
get rawRecording() {
|
|
958
|
+
return this.getCurrentValue(this.rawRecording$);
|
|
959
|
+
}
|
|
960
|
+
|
|
928
961
|
/**
|
|
929
962
|
* Will provide the session data of this call.
|
|
930
963
|
*/
|
|
@@ -1272,7 +1305,21 @@ export class CallState {
|
|
|
1272
1305
|
this.setCurrentValue(this.customSubject, call.custom);
|
|
1273
1306
|
this.setCurrentValue(this.egressSubject, call.egress);
|
|
1274
1307
|
this.setCurrentValue(this.ingressSubject, call.ingress);
|
|
1275
|
-
|
|
1308
|
+
const { individual_recording, composite_recording, raw_recording } =
|
|
1309
|
+
call.egress;
|
|
1310
|
+
this.setCurrentValue(
|
|
1311
|
+
this.recordingSubject,
|
|
1312
|
+
call.recording || composite_recording?.status === 'running',
|
|
1313
|
+
);
|
|
1314
|
+
this.setCurrentValue(
|
|
1315
|
+
this.individualRecordingSubject,
|
|
1316
|
+
individual_recording?.status === 'running',
|
|
1317
|
+
);
|
|
1318
|
+
this.setCurrentValue(
|
|
1319
|
+
this.rawRecordingSubject,
|
|
1320
|
+
raw_recording?.status === 'running',
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1276
1323
|
const s = this.setCurrentValue(this.sessionSubject, call.session);
|
|
1277
1324
|
this.updateParticipantCountFromSession(s);
|
|
1278
1325
|
this.setCurrentValue(this.settingsSubject, call.settings);
|
|
@@ -1363,6 +1410,22 @@ export class CallState {
|
|
|
1363
1410
|
}));
|
|
1364
1411
|
};
|
|
1365
1412
|
|
|
1413
|
+
private updateFromRecordingEvent = (
|
|
1414
|
+
type: CallRecordingType | undefined,
|
|
1415
|
+
running: boolean,
|
|
1416
|
+
) => {
|
|
1417
|
+
// handle the legacy format, where `type` is absent in the emitted events
|
|
1418
|
+
if (type === undefined || type === 'composite') {
|
|
1419
|
+
this.setCurrentValue(this.recordingSubject, running);
|
|
1420
|
+
} else if (type === 'individual') {
|
|
1421
|
+
this.setCurrentValue(this.individualRecordingSubject, running);
|
|
1422
|
+
} else if (type === 'raw') {
|
|
1423
|
+
this.setCurrentValue(this.rawRecordingSubject, running);
|
|
1424
|
+
} else {
|
|
1425
|
+
ensureExhausted(type, 'Unknown recording type');
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1366
1429
|
private updateParticipantCountFromSession = (
|
|
1367
1430
|
session: CallSessionResponse | undefined,
|
|
1368
1431
|
) => {
|