@stream-io/video-client 1.36.0 → 1.37.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 +10 -0
- package/dist/index.browser.es.js +175 -113
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +175 -113
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +175 -113
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +6 -4
- package/dist/src/gen/coordinator/index.d.ts +726 -16
- package/dist/src/rtc/BasePeerConnection.d.ts +4 -2
- package/dist/src/rtc/Publisher.d.ts +3 -3
- package/dist/src/rtc/codecs.d.ts +3 -1
- package/dist/src/rtc/helpers/sdp.d.ts +8 -0
- package/dist/src/rtc/types.d.ts +4 -5
- package/dist/src/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/Call.ts +26 -26
- package/src/StreamVideoClient.ts +2 -9
- package/src/__tests__/StreamVideoClient.api.test.ts +5 -19
- package/src/__tests__/StreamVideoClient.ringing.test.ts +55 -6
- package/src/gen/coordinator/index.ts +708 -16
- package/src/rtc/BasePeerConnection.ts +7 -2
- package/src/rtc/Publisher.ts +13 -5
- package/src/rtc/Subscriber.ts +10 -1
- package/src/rtc/__tests__/Publisher.test.ts +12 -8
- package/src/rtc/codecs.ts +13 -2
- package/src/rtc/helpers/__tests__/sdp.codecs.test.ts +628 -0
- package/src/rtc/helpers/sdp.ts +82 -0
- package/src/rtc/types.ts +4 -4
- package/src/types.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
4
|
|
|
5
|
+
## [1.37.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.36.1...@stream-io/video-client-1.37.0) (2025-11-14)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- ring individual members ([#1755](https://github.com/GetStream/stream-video-js/issues/1755)) ([57564d6](https://github.com/GetStream/stream-video-js/commit/57564d63f21da7b95b582f74c88b24af7e77659c))
|
|
10
|
+
|
|
11
|
+
## [1.36.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.36.0...@stream-io/video-client-1.36.1) (2025-11-12)
|
|
12
|
+
|
|
13
|
+
- enforce the client to publish options on SDP level ([#1976](https://github.com/GetStream/stream-video-js/issues/1976)) ([1d93f72](https://github.com/GetStream/stream-video-js/commit/1d93f72cb4395aaf9b487eb66e0c3b6a8111aca4))
|
|
14
|
+
|
|
5
15
|
## [1.36.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.35.1...@stream-io/video-client-1.36.0) (2025-10-30)
|
|
6
16
|
|
|
7
17
|
### Features
|
package/dist/index.browser.es.js
CHANGED
|
@@ -6,9 +6,9 @@ export { AxiosError } from 'axios';
|
|
|
6
6
|
import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transport';
|
|
7
7
|
import * as scopedLogger from '@stream-io/logger';
|
|
8
8
|
export { LogLevelEnum } from '@stream-io/logger';
|
|
9
|
+
import { parse, write } from 'sdp-transform';
|
|
9
10
|
import { ReplaySubject, combineLatest, BehaviorSubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, of } from 'rxjs';
|
|
10
11
|
import { UAParser } from 'ua-parser-js';
|
|
11
|
-
import { parse, write } from 'sdp-transform';
|
|
12
12
|
import { WorkerTimer } from '@stream-io/worker-timer';
|
|
13
13
|
|
|
14
14
|
/* tslint:disable */
|
|
@@ -52,7 +52,6 @@ const FrameRecordingSettingsRequestQualityEnum = {
|
|
|
52
52
|
_720P: '720p',
|
|
53
53
|
_1080P: '1080p',
|
|
54
54
|
_1440P: '1440p',
|
|
55
|
-
_2160P: '2160p',
|
|
56
55
|
};
|
|
57
56
|
/**
|
|
58
57
|
* @export
|
|
@@ -148,13 +147,11 @@ const RTMPBroadcastRequestQualityEnum = {
|
|
|
148
147
|
_720P: '720p',
|
|
149
148
|
_1080P: '1080p',
|
|
150
149
|
_1440P: '1440p',
|
|
151
|
-
_2160P: '2160p',
|
|
152
150
|
PORTRAIT_360X640: 'portrait-360x640',
|
|
153
151
|
PORTRAIT_480X854: 'portrait-480x854',
|
|
154
152
|
PORTRAIT_720X1280: 'portrait-720x1280',
|
|
155
153
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
156
154
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
157
|
-
PORTRAIT_2160X3840: 'portrait-2160x3840',
|
|
158
155
|
};
|
|
159
156
|
/**
|
|
160
157
|
* @export
|
|
@@ -165,13 +162,11 @@ const RTMPSettingsRequestQualityEnum = {
|
|
|
165
162
|
_720P: '720p',
|
|
166
163
|
_1080P: '1080p',
|
|
167
164
|
_1440P: '1440p',
|
|
168
|
-
_2160P: '2160p',
|
|
169
165
|
PORTRAIT_360X640: 'portrait-360x640',
|
|
170
166
|
PORTRAIT_480X854: 'portrait-480x854',
|
|
171
167
|
PORTRAIT_720X1280: 'portrait-720x1280',
|
|
172
168
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
173
169
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
174
|
-
PORTRAIT_2160X3840: 'portrait-2160x3840',
|
|
175
170
|
};
|
|
176
171
|
/**
|
|
177
172
|
* @export
|
|
@@ -190,13 +185,11 @@ const RecordSettingsRequestQualityEnum = {
|
|
|
190
185
|
_720P: '720p',
|
|
191
186
|
_1080P: '1080p',
|
|
192
187
|
_1440P: '1440p',
|
|
193
|
-
_2160P: '2160p',
|
|
194
188
|
PORTRAIT_360X640: 'portrait-360x640',
|
|
195
189
|
PORTRAIT_480X854: 'portrait-480x854',
|
|
196
190
|
PORTRAIT_720X1280: 'portrait-720x1280',
|
|
197
191
|
PORTRAIT_1080X1920: 'portrait-1080x1920',
|
|
198
192
|
PORTRAIT_1440X2560: 'portrait-1440x2560',
|
|
199
|
-
PORTRAIT_2160X3840: 'portrait-2160x3840',
|
|
200
193
|
};
|
|
201
194
|
/**
|
|
202
195
|
* @export
|
|
@@ -3929,19 +3922,158 @@ const retryable = async (rpc, signal) => {
|
|
|
3929
3922
|
return result;
|
|
3930
3923
|
};
|
|
3931
3924
|
|
|
3925
|
+
/**
|
|
3926
|
+
* Extracts the mid from the transceiver or the SDP.
|
|
3927
|
+
*
|
|
3928
|
+
* @param transceiver the transceiver.
|
|
3929
|
+
* @param transceiverInitIndex the index of the transceiver in the transceiver's init array.
|
|
3930
|
+
* @param sdp the SDP.
|
|
3931
|
+
*/
|
|
3932
|
+
const extractMid = (transceiver, transceiverInitIndex, sdp) => {
|
|
3933
|
+
if (transceiver.mid)
|
|
3934
|
+
return transceiver.mid;
|
|
3935
|
+
if (!sdp)
|
|
3936
|
+
return String(transceiverInitIndex);
|
|
3937
|
+
const track = transceiver.sender.track;
|
|
3938
|
+
const parsedSdp = parse(sdp);
|
|
3939
|
+
const media = parsedSdp.media.find((m) => {
|
|
3940
|
+
return (m.type === track.kind &&
|
|
3941
|
+
// if `msid` is not present, we assume that the track is the first one
|
|
3942
|
+
(m.msid?.includes(track.id) ?? true));
|
|
3943
|
+
});
|
|
3944
|
+
if (typeof media?.mid !== 'undefined')
|
|
3945
|
+
return String(media.mid);
|
|
3946
|
+
if (transceiverInitIndex < 0)
|
|
3947
|
+
return '';
|
|
3948
|
+
return String(transceiverInitIndex);
|
|
3949
|
+
};
|
|
3950
|
+
/**
|
|
3951
|
+
* Enables stereo in the answer SDP based on the offered stereo in the offer SDP.
|
|
3952
|
+
*
|
|
3953
|
+
* @param offerSdp the offer SDP containing the stereo configuration.
|
|
3954
|
+
* @param answerSdp the answer SDP to be modified.
|
|
3955
|
+
*/
|
|
3956
|
+
const enableStereo = (offerSdp, answerSdp) => {
|
|
3957
|
+
const offeredStereoMids = new Set();
|
|
3958
|
+
const parsedOfferSdp = parse(offerSdp);
|
|
3959
|
+
for (const media of parsedOfferSdp.media) {
|
|
3960
|
+
if (media.type !== 'audio')
|
|
3961
|
+
continue;
|
|
3962
|
+
const opus = media.rtp.find((r) => r.codec === 'opus');
|
|
3963
|
+
if (!opus)
|
|
3964
|
+
continue;
|
|
3965
|
+
for (const fmtp of media.fmtp) {
|
|
3966
|
+
if (fmtp.payload === opus.payload && fmtp.config.includes('stereo=1')) {
|
|
3967
|
+
offeredStereoMids.add(media.mid);
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
// No stereo offered, return the original answerSdp
|
|
3972
|
+
if (offeredStereoMids.size === 0)
|
|
3973
|
+
return answerSdp;
|
|
3974
|
+
const parsedAnswerSdp = parse(answerSdp);
|
|
3975
|
+
for (const media of parsedAnswerSdp.media) {
|
|
3976
|
+
if (media.type !== 'audio' || !offeredStereoMids.has(media.mid))
|
|
3977
|
+
continue;
|
|
3978
|
+
const opus = media.rtp.find((r) => r.codec === 'opus');
|
|
3979
|
+
if (!opus)
|
|
3980
|
+
continue;
|
|
3981
|
+
for (const fmtp of media.fmtp) {
|
|
3982
|
+
if (fmtp.payload === opus.payload && !fmtp.config.includes('stereo=1')) {
|
|
3983
|
+
fmtp.config += ';stereo=1';
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
return write(parsedAnswerSdp);
|
|
3988
|
+
};
|
|
3989
|
+
/**
|
|
3990
|
+
* Removes all codecs from the SDP except the specified codec.
|
|
3991
|
+
*
|
|
3992
|
+
* @param sdp the SDP to modify.
|
|
3993
|
+
* @param codecMimeTypeToKeep the codec mime type to keep (video/h264 or audio/opus).
|
|
3994
|
+
* @param fmtpProfileToKeep the fmtp profile to keep (e.g. 'profile-level-id=42e01f' or multiple segments like 'profile-level-id=64001f;packetization-mode=1').
|
|
3995
|
+
*/
|
|
3996
|
+
const removeCodecsExcept = (sdp, codecMimeTypeToKeep, fmtpProfileToKeep) => {
|
|
3997
|
+
const [kind, codec] = toMimeType(codecMimeTypeToKeep).split('/');
|
|
3998
|
+
if (!kind || !codec)
|
|
3999
|
+
return sdp;
|
|
4000
|
+
const parsed = parse(sdp);
|
|
4001
|
+
for (const media of parsed.media) {
|
|
4002
|
+
if (media.type !== kind)
|
|
4003
|
+
continue;
|
|
4004
|
+
// Build a set of payloads to KEEP: all payloads whose rtp.codec matches codec
|
|
4005
|
+
let payloadsToKeep = new Set();
|
|
4006
|
+
for (const rtp of media.rtp) {
|
|
4007
|
+
if (rtp.codec.toLowerCase() !== codec)
|
|
4008
|
+
continue;
|
|
4009
|
+
payloadsToKeep.add(rtp.payload);
|
|
4010
|
+
}
|
|
4011
|
+
// If a specific fmtp profile is requested, only keep payloads whose fmtp config matches it
|
|
4012
|
+
if (fmtpProfileToKeep) {
|
|
4013
|
+
const filtered = new Set();
|
|
4014
|
+
const required = new Set(fmtpProfileToKeep.split(';'));
|
|
4015
|
+
for (const fmtp of media.fmtp) {
|
|
4016
|
+
if (payloadsToKeep.has(fmtp.payload) &&
|
|
4017
|
+
required.difference(new Set(fmtp.config.split(';'))).size === 0) {
|
|
4018
|
+
filtered.add(fmtp.payload);
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
payloadsToKeep = filtered;
|
|
4022
|
+
}
|
|
4023
|
+
// If no payloads to keep AND no fmtpProfile was specified, skip modifications (preserve SDP as-is)
|
|
4024
|
+
if (payloadsToKeep.size === 0 && !fmtpProfileToKeep)
|
|
4025
|
+
continue;
|
|
4026
|
+
// Keep RTX payloads that are associated with kept primary payloads via apt
|
|
4027
|
+
// RTX mappings look like: a=fmtp:<rtxPayload> apt=<primaryPayload>
|
|
4028
|
+
for (const fmtp of media.fmtp) {
|
|
4029
|
+
const matches = /\s*apt\s*=\s*(\d+)\s*/i.exec(fmtp.config);
|
|
4030
|
+
if (!matches)
|
|
4031
|
+
continue;
|
|
4032
|
+
const primaryPayloadApt = Number(matches[1]);
|
|
4033
|
+
if (!payloadsToKeep.has(primaryPayloadApt))
|
|
4034
|
+
continue;
|
|
4035
|
+
payloadsToKeep.add(fmtp.payload);
|
|
4036
|
+
}
|
|
4037
|
+
// Filter rtp, fmtp and rtcpFb entries
|
|
4038
|
+
media.rtp = media.rtp.filter((rtp) => payloadsToKeep.has(rtp.payload));
|
|
4039
|
+
media.fmtp = media.fmtp.filter((fmtp) => payloadsToKeep.has(fmtp.payload));
|
|
4040
|
+
media.rtcpFb = media.rtcpFb?.filter((fb) => typeof fb.payload === 'number' ? payloadsToKeep.has(fb.payload) : true);
|
|
4041
|
+
// Update the m= line payload list to only the kept payloads, preserving original order
|
|
4042
|
+
const payloads = [];
|
|
4043
|
+
for (const id of (media.payloads || '').split(/\s+/)) {
|
|
4044
|
+
const payload = Number(id);
|
|
4045
|
+
if (!payloadsToKeep.has(payload))
|
|
4046
|
+
continue;
|
|
4047
|
+
payloads.push(payload);
|
|
4048
|
+
}
|
|
4049
|
+
media.payloads = payloads.join(' ');
|
|
4050
|
+
}
|
|
4051
|
+
return write(parsed);
|
|
4052
|
+
};
|
|
4053
|
+
/**
|
|
4054
|
+
* Converts the given codec to a mime-type format when necessary.
|
|
4055
|
+
* e.g.: `vp9` -> `video/vp9`
|
|
4056
|
+
*/
|
|
4057
|
+
const toMimeType = (codec, kind = 'video') => codec.includes('/') ? codec : `${kind}/${codec}`;
|
|
4058
|
+
|
|
3932
4059
|
/**
|
|
3933
4060
|
* Returns a generic SDP for the given direction.
|
|
3934
4061
|
* We use this SDP to send it as part of our JoinRequest so that the SFU
|
|
3935
4062
|
* can use it to determine the client's codec capabilities.
|
|
3936
4063
|
*
|
|
3937
4064
|
* @param direction the direction of the transceiver.
|
|
4065
|
+
* @param codecToKeep the codec mime type to keep (video/h264 or audio/opus).
|
|
4066
|
+
* @param fmtpProfileToKeep optional fmtp profile to keep.
|
|
3938
4067
|
*/
|
|
3939
|
-
const getGenericSdp = async (direction) => {
|
|
4068
|
+
const getGenericSdp = async (direction, codecToKeep, fmtpProfileToKeep) => {
|
|
3940
4069
|
const tempPc = new RTCPeerConnection();
|
|
3941
4070
|
tempPc.addTransceiver('video', { direction });
|
|
3942
4071
|
tempPc.addTransceiver('audio', { direction });
|
|
3943
4072
|
const offer = await tempPc.createOffer();
|
|
3944
|
-
const sdp =
|
|
4073
|
+
const { sdp: baseSdp = '' } = offer;
|
|
4074
|
+
const sdp = codecToKeep
|
|
4075
|
+
? removeCodecsExcept(baseSdp, codecToKeep, fmtpProfileToKeep)
|
|
4076
|
+
: baseSdp;
|
|
3945
4077
|
tempPc.getTransceivers().forEach((t) => {
|
|
3946
4078
|
t.stop?.();
|
|
3947
4079
|
});
|
|
@@ -5837,7 +5969,7 @@ const getSdkVersion = (sdk) => {
|
|
|
5837
5969
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
5838
5970
|
};
|
|
5839
5971
|
|
|
5840
|
-
const version = "1.
|
|
5972
|
+
const version = "1.37.0";
|
|
5841
5973
|
const [major, minor, patch] = version.split('.');
|
|
5842
5974
|
let sdkInfo = {
|
|
5843
5975
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -6758,7 +6890,7 @@ class BasePeerConnection {
|
|
|
6758
6890
|
/**
|
|
6759
6891
|
* Constructs a new `BasePeerConnection` instance.
|
|
6760
6892
|
*/
|
|
6761
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, tag, enableTracing, iceRestartDelay = 2500, }) {
|
|
6893
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, }) {
|
|
6762
6894
|
this.isIceRestarting = false;
|
|
6763
6895
|
this.isDisposed = false;
|
|
6764
6896
|
this.trackIdToTrackType = new Map();
|
|
@@ -6787,7 +6919,7 @@ class BasePeerConnection {
|
|
|
6787
6919
|
e.error.code === ErrorCode.PARTICIPANT_SIGNAL_LOST
|
|
6788
6920
|
? WebsocketReconnectStrategy.FAST
|
|
6789
6921
|
: WebsocketReconnectStrategy.REJOIN;
|
|
6790
|
-
this.onReconnectionNeeded?.(strategy, reason);
|
|
6922
|
+
this.onReconnectionNeeded?.(strategy, reason, this.peerType);
|
|
6791
6923
|
});
|
|
6792
6924
|
};
|
|
6793
6925
|
/**
|
|
@@ -6903,7 +7035,7 @@ class BasePeerConnection {
|
|
|
6903
7035
|
}
|
|
6904
7036
|
// we can't recover from a failed connection state (contrary to ICE)
|
|
6905
7037
|
if (state === 'failed') {
|
|
6906
|
-
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, 'Connection failed');
|
|
7038
|
+
this.onReconnectionNeeded?.(WebsocketReconnectStrategy.REJOIN, 'Connection failed', this.peerType);
|
|
6907
7039
|
return;
|
|
6908
7040
|
}
|
|
6909
7041
|
this.handleConnectionStateUpdate(state);
|
|
@@ -6979,6 +7111,7 @@ class BasePeerConnection {
|
|
|
6979
7111
|
this.state = state;
|
|
6980
7112
|
this.dispatcher = dispatcher;
|
|
6981
7113
|
this.iceRestartDelay = iceRestartDelay;
|
|
7114
|
+
this.clientPublishOptions = clientPublishOptions;
|
|
6982
7115
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
6983
7116
|
this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
|
|
6984
7117
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
@@ -7313,71 +7446,6 @@ const withSimulcastConstraints = (width, height, optimalVideoLayers, useSingleLa
|
|
|
7313
7446
|
}));
|
|
7314
7447
|
};
|
|
7315
7448
|
|
|
7316
|
-
/**
|
|
7317
|
-
* Extracts the mid from the transceiver or the SDP.
|
|
7318
|
-
*
|
|
7319
|
-
* @param transceiver the transceiver.
|
|
7320
|
-
* @param transceiverInitIndex the index of the transceiver in the transceiver's init array.
|
|
7321
|
-
* @param sdp the SDP.
|
|
7322
|
-
*/
|
|
7323
|
-
const extractMid = (transceiver, transceiverInitIndex, sdp) => {
|
|
7324
|
-
if (transceiver.mid)
|
|
7325
|
-
return transceiver.mid;
|
|
7326
|
-
if (!sdp)
|
|
7327
|
-
return String(transceiverInitIndex);
|
|
7328
|
-
const track = transceiver.sender.track;
|
|
7329
|
-
const parsedSdp = parse(sdp);
|
|
7330
|
-
const media = parsedSdp.media.find((m) => {
|
|
7331
|
-
return (m.type === track.kind &&
|
|
7332
|
-
// if `msid` is not present, we assume that the track is the first one
|
|
7333
|
-
(m.msid?.includes(track.id) ?? true));
|
|
7334
|
-
});
|
|
7335
|
-
if (typeof media?.mid !== 'undefined')
|
|
7336
|
-
return String(media.mid);
|
|
7337
|
-
if (transceiverInitIndex < 0)
|
|
7338
|
-
return '';
|
|
7339
|
-
return String(transceiverInitIndex);
|
|
7340
|
-
};
|
|
7341
|
-
/**
|
|
7342
|
-
* Enables stereo in the answer SDP based on the offered stereo in the offer SDP.
|
|
7343
|
-
*
|
|
7344
|
-
* @param offerSdp the offer SDP containing the stereo configuration.
|
|
7345
|
-
* @param answerSdp the answer SDP to be modified.
|
|
7346
|
-
*/
|
|
7347
|
-
const enableStereo = (offerSdp, answerSdp) => {
|
|
7348
|
-
const offeredStereoMids = new Set();
|
|
7349
|
-
const parsedOfferSdp = parse(offerSdp);
|
|
7350
|
-
for (const media of parsedOfferSdp.media) {
|
|
7351
|
-
if (media.type !== 'audio')
|
|
7352
|
-
continue;
|
|
7353
|
-
const opus = media.rtp.find((r) => r.codec === 'opus');
|
|
7354
|
-
if (!opus)
|
|
7355
|
-
continue;
|
|
7356
|
-
for (const fmtp of media.fmtp) {
|
|
7357
|
-
if (fmtp.payload === opus.payload && fmtp.config.includes('stereo=1')) {
|
|
7358
|
-
offeredStereoMids.add(media.mid);
|
|
7359
|
-
}
|
|
7360
|
-
}
|
|
7361
|
-
}
|
|
7362
|
-
// No stereo offered, return the original answerSdp
|
|
7363
|
-
if (offeredStereoMids.size === 0)
|
|
7364
|
-
return answerSdp;
|
|
7365
|
-
const parsedAnswerSdp = parse(answerSdp);
|
|
7366
|
-
for (const media of parsedAnswerSdp.media) {
|
|
7367
|
-
if (media.type !== 'audio' || !offeredStereoMids.has(media.mid))
|
|
7368
|
-
continue;
|
|
7369
|
-
const opus = media.rtp.find((r) => r.codec === 'opus');
|
|
7370
|
-
if (!opus)
|
|
7371
|
-
continue;
|
|
7372
|
-
for (const fmtp of media.fmtp) {
|
|
7373
|
-
if (fmtp.payload === opus.payload && !fmtp.config.includes('stereo=1')) {
|
|
7374
|
-
fmtp.config += ';stereo=1';
|
|
7375
|
-
}
|
|
7376
|
-
}
|
|
7377
|
-
}
|
|
7378
|
-
return write(parsedAnswerSdp);
|
|
7379
|
-
};
|
|
7380
|
-
|
|
7381
7449
|
/**
|
|
7382
7450
|
* The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
|
|
7383
7451
|
*
|
|
@@ -7387,7 +7455,7 @@ class Publisher extends BasePeerConnection {
|
|
|
7387
7455
|
/**
|
|
7388
7456
|
* Constructs a new `Publisher` instance.
|
|
7389
7457
|
*/
|
|
7390
|
-
constructor(
|
|
7458
|
+
constructor(baseOptions, publishOptions) {
|
|
7391
7459
|
super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
|
|
7392
7460
|
this.transceiverCache = new TransceiverCache();
|
|
7393
7461
|
this.clonedTracks = new Set();
|
|
@@ -7646,7 +7714,11 @@ class Publisher extends BasePeerConnection {
|
|
|
7646
7714
|
try {
|
|
7647
7715
|
this.isIceRestarting = options?.iceRestart ?? false;
|
|
7648
7716
|
await this.pc.setLocalDescription(offer);
|
|
7649
|
-
const { sdp = '' } = offer;
|
|
7717
|
+
const { sdp: baseSdp = '' } = offer;
|
|
7718
|
+
const { dangerouslyForceCodec, fmtpLine } = this.clientPublishOptions || {};
|
|
7719
|
+
const sdp = dangerouslyForceCodec
|
|
7720
|
+
? removeCodecsExcept(baseSdp, dangerouslyForceCodec, fmtpLine)
|
|
7721
|
+
: baseSdp;
|
|
7650
7722
|
const { response } = await this.sfuClient.setPublisher({ sdp, tracks });
|
|
7651
7723
|
if (response.error)
|
|
7652
7724
|
throw new NegotiationError(response.error);
|
|
@@ -7876,6 +7948,10 @@ class Subscriber extends BasePeerConnection {
|
|
|
7876
7948
|
const answer = await this.pc.createAnswer();
|
|
7877
7949
|
if (answer.sdp) {
|
|
7878
7950
|
answer.sdp = enableStereo(subscriberOffer.sdp, answer.sdp);
|
|
7951
|
+
const { dangerouslyForceCodec, subscriberFmtpLine } = this.clientPublishOptions || {};
|
|
7952
|
+
if (dangerouslyForceCodec) {
|
|
7953
|
+
answer.sdp = removeCodecsExcept(answer.sdp, dangerouslyForceCodec, subscriberFmtpLine);
|
|
7954
|
+
}
|
|
7879
7955
|
}
|
|
7880
7956
|
await this.pc.setLocalDescription(answer);
|
|
7881
7957
|
await this.sfuClient.sendAnswer({
|
|
@@ -12022,6 +12098,7 @@ class Call {
|
|
|
12022
12098
|
* @param params.ring if set to true, a `call.ring` event will be sent to the call members.
|
|
12023
12099
|
* @param params.notify if set to true, a `call.notification` event will be sent to the call members.
|
|
12024
12100
|
* @param params.members_limit the total number of members to return as part of the response.
|
|
12101
|
+
* @param params.video if set to true, in a ringing scenario, mobile SDKs will show "incoming video call", audio only otherwise.
|
|
12025
12102
|
*/
|
|
12026
12103
|
this.get = async (params) => {
|
|
12027
12104
|
await this.setup();
|
|
@@ -12075,11 +12152,11 @@ class Call {
|
|
|
12075
12152
|
return this.streamClient.post(`${this.streamClientBasePath}/delete`, data);
|
|
12076
12153
|
};
|
|
12077
12154
|
/**
|
|
12078
|
-
*
|
|
12079
|
-
*
|
|
12155
|
+
* Sends a ring notification to the provided users who are not already in the call.
|
|
12156
|
+
* All users should be members of the call.
|
|
12080
12157
|
*/
|
|
12081
|
-
this.ring = async () => {
|
|
12082
|
-
return
|
|
12158
|
+
this.ring = async (data = {}) => {
|
|
12159
|
+
return this.streamClient.post(`${this.streamClientBasePath}/ring`, data);
|
|
12083
12160
|
};
|
|
12084
12161
|
/**
|
|
12085
12162
|
* A shortcut for {@link Call.get} with `notify` parameter set to `true`.
|
|
@@ -12216,9 +12293,10 @@ class Call {
|
|
|
12216
12293
|
// prepare a generic SDP and send it to the SFU.
|
|
12217
12294
|
// these are throw-away SDPs that the SFU will use to determine
|
|
12218
12295
|
// the capabilities of the client (codec support, etc.)
|
|
12296
|
+
const { dangerouslyForceCodec, fmtpLine, subscriberFmtpLine } = this.clientPublishOptions || {};
|
|
12219
12297
|
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
12220
|
-
getGenericSdp('recvonly'),
|
|
12221
|
-
getGenericSdp('sendonly'),
|
|
12298
|
+
getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
|
|
12299
|
+
getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
|
|
12222
12300
|
]);
|
|
12223
12301
|
const isReconnecting = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
|
|
12224
12302
|
const reconnectDetails = isReconnecting
|
|
@@ -12410,20 +12488,22 @@ class Call {
|
|
|
12410
12488
|
if (closePreviousInstances && this.subscriber) {
|
|
12411
12489
|
this.subscriber.dispose();
|
|
12412
12490
|
}
|
|
12413
|
-
|
|
12491
|
+
const basePeerConnectionOptions = {
|
|
12414
12492
|
sfuClient,
|
|
12415
12493
|
dispatcher: this.dispatcher,
|
|
12416
12494
|
state: this.state,
|
|
12417
12495
|
connectionConfig,
|
|
12418
12496
|
tag: sfuClient.tag,
|
|
12419
12497
|
enableTracing,
|
|
12420
|
-
|
|
12498
|
+
clientPublishOptions: this.clientPublishOptions,
|
|
12499
|
+
onReconnectionNeeded: (kind, reason, peerType) => {
|
|
12421
12500
|
this.reconnect(kind, reason).catch((err) => {
|
|
12422
|
-
const message = `[Reconnect] Error reconnecting after a
|
|
12501
|
+
const message = `[Reconnect] Error reconnecting, after a ${PeerType[peerType]} error: ${reason}`;
|
|
12423
12502
|
this.logger.warn(message, err);
|
|
12424
12503
|
});
|
|
12425
12504
|
},
|
|
12426
|
-
}
|
|
12505
|
+
};
|
|
12506
|
+
this.subscriber = new Subscriber(basePeerConnectionOptions);
|
|
12427
12507
|
// anonymous users can't publish anything hence, there is no need
|
|
12428
12508
|
// to create Publisher Peer Connection for them
|
|
12429
12509
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
@@ -12431,21 +12511,7 @@ class Call {
|
|
|
12431
12511
|
if (closePreviousInstances && this.publisher) {
|
|
12432
12512
|
this.publisher.dispose();
|
|
12433
12513
|
}
|
|
12434
|
-
this.publisher = new Publisher(
|
|
12435
|
-
sfuClient,
|
|
12436
|
-
dispatcher: this.dispatcher,
|
|
12437
|
-
state: this.state,
|
|
12438
|
-
connectionConfig,
|
|
12439
|
-
publishOptions,
|
|
12440
|
-
tag: sfuClient.tag,
|
|
12441
|
-
enableTracing,
|
|
12442
|
-
onReconnectionNeeded: (kind, reason) => {
|
|
12443
|
-
this.reconnect(kind, reason).catch((err) => {
|
|
12444
|
-
const message = `[Reconnect] Error reconnecting after a publisher error: ${reason}`;
|
|
12445
|
-
this.logger.warn(message, err);
|
|
12446
|
-
});
|
|
12447
|
-
},
|
|
12448
|
-
});
|
|
12514
|
+
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
|
|
12449
12515
|
}
|
|
12450
12516
|
this.statsReporter?.stop();
|
|
12451
12517
|
if (this.statsReportingIntervalInMs > 0) {
|
|
@@ -14807,7 +14873,7 @@ class StreamClient {
|
|
|
14807
14873
|
this.getUserAgent = () => {
|
|
14808
14874
|
if (!this.cachedUserAgent) {
|
|
14809
14875
|
const { clientAppIdentifier = {} } = this.options;
|
|
14810
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
14876
|
+
const { sdkName = 'js', sdkVersion = "1.37.0", ...extras } = clientAppIdentifier;
|
|
14811
14877
|
this.cachedUserAgent = [
|
|
14812
14878
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
14813
14879
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -15027,10 +15093,6 @@ class StreamVideoClient {
|
|
|
15027
15093
|
* @param e the event.
|
|
15028
15094
|
*/
|
|
15029
15095
|
this.initCallFromEvent = async (e) => {
|
|
15030
|
-
if (this.state.connectedUser?.id === e.call.created_by.id) {
|
|
15031
|
-
this.logger.debug(`Ignoring ${e.type} event sent by the current user`);
|
|
15032
|
-
return;
|
|
15033
|
-
}
|
|
15034
15096
|
try {
|
|
15035
15097
|
const concurrencyTag = getCallInitConcurrencyTag(e.call_cid);
|
|
15036
15098
|
await withoutConcurrency(concurrencyTag, async () => {
|
|
@@ -15344,12 +15406,12 @@ class StreamVideoClient {
|
|
|
15344
15406
|
this.shouldRejectCall = (currentCallId) => {
|
|
15345
15407
|
if (!this.rejectCallWhenBusy)
|
|
15346
15408
|
return false;
|
|
15347
|
-
|
|
15409
|
+
return this.state.calls.some((c) => c.cid !== currentCallId &&
|
|
15348
15410
|
c.ringing &&
|
|
15411
|
+
!c.isCreatedByMe &&
|
|
15349
15412
|
c.state.callingState !== CallingState.IDLE &&
|
|
15350
15413
|
c.state.callingState !== CallingState.LEFT &&
|
|
15351
15414
|
c.state.callingState !== CallingState.RECONNECTING_FAILED);
|
|
15352
|
-
return hasOngoingRingingCall;
|
|
15353
15415
|
};
|
|
15354
15416
|
const apiKey = typeof apiKeyOrArgs === 'string' ? apiKeyOrArgs : apiKeyOrArgs.apiKey;
|
|
15355
15417
|
const clientOptions = typeof apiKeyOrArgs === 'string' ? opts : apiKeyOrArgs.options;
|