@stream-io/video-client 1.52.0 → 1.52.1-beta.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.
@@ -127,6 +127,7 @@ export declare class Call {
127
127
  private joinResponseTimeout?;
128
128
  private rpcRequestTimeout?;
129
129
  private joinCallData?;
130
+ private selfSubEnabled;
130
131
  private hasJoinedOnce;
131
132
  private deviceSettingsAppliedOnce;
132
133
  private credentials?;
@@ -190,6 +191,16 @@ export declare class Call {
190
191
  * Retrieves the current user ID.
191
192
  */
192
193
  get currentUserId(): string | undefined;
194
+ /**
195
+ * A flag indicating whether self-subscription is enabled for the call.
196
+ */
197
+ get isSelfSubEnabled(): boolean;
198
+ /**
199
+ * The largest video publish dimension across the current publish options.
200
+ *
201
+ * @internal
202
+ */
203
+ getMaxVideoPublishDimension: () => VideoDimension | undefined;
193
204
  /**
194
205
  * A flag indicating whether the call was created by the current user.
195
206
  */
@@ -262,10 +273,11 @@ export declare class Call {
262
273
  *
263
274
  * @returns a promise which resolves once the call join-flow has finished.
264
275
  */
265
- join: ({ maxJoinRetries, joinResponseTimeout, rpcRequestTimeout, ...data }?: JoinCallData & {
276
+ join: ({ maxJoinRetries, joinResponseTimeout, rpcRequestTimeout, selfSubEnabled, ...data }?: JoinCallData & {
266
277
  maxJoinRetries?: number;
267
278
  joinResponseTimeout?: number;
268
279
  rpcRequestTimeout?: number;
280
+ selfSubEnabled?: boolean;
269
281
  }) => Promise<void>;
270
282
  /**
271
283
  * Will make a single attempt to watch for call related WebSocket events
@@ -1,4 +1,6 @@
1
- import type { JsonReadOptions, JsonValue, JsonWriteOptions } from '@protobuf-ts/runtime';
1
+ import type { JsonValue } from '@protobuf-ts/runtime';
2
+ import type { JsonReadOptions } from '@protobuf-ts/runtime';
3
+ import type { JsonWriteOptions } from '@protobuf-ts/runtime';
2
4
  import { MessageType } from '@protobuf-ts/runtime';
3
5
  /**
4
6
  * `Struct` represents a structured data value, consisting of fields
@@ -1,4 +1,6 @@
1
- import type { JsonReadOptions, JsonValue, JsonWriteOptions } from '@protobuf-ts/runtime';
1
+ import type { JsonValue } from '@protobuf-ts/runtime';
2
+ import type { JsonReadOptions } from '@protobuf-ts/runtime';
3
+ import type { JsonWriteOptions } from '@protobuf-ts/runtime';
2
4
  import { MessageType } from '@protobuf-ts/runtime';
3
5
  /**
4
6
  * A Timestamp represents a point in time independent of any time zone or local
@@ -1,6 +1,27 @@
1
1
  import { MessageType } from '@protobuf-ts/runtime';
2
- import { CallEndedReason, CallGrants, CallState, ClientCapability, ClientDetails, Codec, ConnectionQuality, DegradationPreference, Error as Error$, GoAwayReason, ICETrickle as ICETrickle$, Participant, ParticipantCount, ParticipantSource, PeerType, Pin, PublishOption, SubscribeOption, TrackInfo, TrackType, TrackUnpublishReason, WebsocketReconnectStrategy } from '../models/models';
2
+ import { CallEndedReason } from '../models/models';
3
+ import { GoAwayReason } from '../models/models';
4
+ import { CallGrants } from '../models/models';
5
+ import { DegradationPreference } from '../models/models';
6
+ import { Codec } from '../models/models';
7
+ import { ConnectionQuality } from '../models/models';
8
+ import { CallState } from '../models/models';
3
9
  import { TrackSubscriptionDetails } from '../signal_rpc/signal';
10
+ import { TrackInfo } from '../models/models';
11
+ import { ParticipantSource } from '../models/models';
12
+ import { ClientCapability } from '../models/models';
13
+ import { SubscribeOption } from '../models/models';
14
+ import { ClientDetails } from '../models/models';
15
+ import { TrackUnpublishReason } from '../models/models';
16
+ import { Participant } from '../models/models';
17
+ import { TrackType } from '../models/models';
18
+ import { ParticipantCount } from '../models/models';
19
+ import { PeerType } from '../models/models';
20
+ import { WebsocketReconnectStrategy } from '../models/models';
21
+ import { Error as Error$ } from '../models/models';
22
+ import { Pin } from '../models/models';
23
+ import { PublishOption } from '../models/models';
24
+ import { ICETrickle as ICETrickle$ } from '../models/models';
4
25
  /**
5
26
  * SFUEvent is a message that is sent from the SFU to the client.
6
27
  *
@@ -411,6 +411,10 @@ export interface TrackInfo {
411
411
  * @generated from protobuf field: int32 publish_option_id = 12;
412
412
  */
413
413
  publishOptionId: number;
414
+ /**
415
+ * @generated from protobuf field: bool self_sub_audio_video = 13;
416
+ */
417
+ selfSubAudioVideo: boolean;
414
418
  }
415
419
  /**
416
420
  * @generated from protobuf message stream.video.sfu.models.Error
@@ -1,6 +1,27 @@
1
- import type { RpcOptions, RpcTransport, ServiceInfo, UnaryCall } from '@protobuf-ts/runtime-rpc';
2
- import type { ICERestartRequest, ICERestartResponse, ICETrickleResponse, SendAnswerRequest, SendAnswerResponse, SendMetricsRequest, SendMetricsResponse, SendStatsRequest, SendStatsResponse, SetPublisherRequest, SetPublisherResponse, StartNoiseCancellationRequest, StartNoiseCancellationResponse, StopNoiseCancellationRequest, StopNoiseCancellationResponse, UpdateMuteStatesRequest, UpdateMuteStatesResponse, UpdateSubscriptionsRequest, UpdateSubscriptionsResponse } from './signal';
1
+ import type { RpcTransport } from '@protobuf-ts/runtime-rpc';
2
+ import type { ServiceInfo } from '@protobuf-ts/runtime-rpc';
3
+ import type { StopNoiseCancellationResponse } from './signal';
4
+ import type { StopNoiseCancellationRequest } from './signal';
5
+ import type { StartNoiseCancellationResponse } from './signal';
6
+ import type { StartNoiseCancellationRequest } from './signal';
7
+ import type { SendMetricsResponse } from './signal';
8
+ import type { SendMetricsRequest } from './signal';
9
+ import type { SendStatsResponse } from './signal';
10
+ import type { SendStatsRequest } from './signal';
11
+ import type { ICERestartResponse } from './signal';
12
+ import type { ICERestartRequest } from './signal';
13
+ import type { UpdateMuteStatesResponse } from './signal';
14
+ import type { UpdateMuteStatesRequest } from './signal';
15
+ import type { UpdateSubscriptionsResponse } from './signal';
16
+ import type { UpdateSubscriptionsRequest } from './signal';
17
+ import type { ICETrickleResponse } from './signal';
3
18
  import type { ICETrickle } from '../models/models';
19
+ import type { SendAnswerResponse } from './signal';
20
+ import type { SendAnswerRequest } from './signal';
21
+ import type { SetPublisherResponse } from './signal';
22
+ import type { SetPublisherRequest } from './signal';
23
+ import type { UnaryCall } from '@protobuf-ts/runtime-rpc';
24
+ import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
4
25
  /**
5
26
  * @generated from protobuf service stream.video.sfu.signal.SignalServer
6
27
  */
@@ -10,10 +10,13 @@ export declare class Publisher extends BasePeerConnection {
10
10
  private readonly transceiverCache;
11
11
  private readonly clonedTracks;
12
12
  private publishOptions;
13
+ private readonly selfSubEnabled;
13
14
  /**
14
15
  * Constructs a new `Publisher` instance.
15
16
  */
16
- constructor(baseOptions: BasePeerConnectionOpts, publishOptions: PublishOption[]);
17
+ constructor(baseOptions: BasePeerConnectionOpts, publishOptions: PublishOption[], opts?: {
18
+ selfSubEnabled?: boolean;
19
+ });
17
20
  /**
18
21
  * Disposes this Publisher instance.
19
22
  */
@@ -7,6 +7,13 @@ import { BasePeerConnectionOpts } from './types';
7
7
  * @internal
8
8
  */
9
9
  export declare class Subscriber extends BasePeerConnection {
10
+ /**
11
+ * Remote streams received from the SFU. For a self-sub case
12
+ * we need to be able to distinguish between the local capture stream.
13
+ * The map will never contain local streams so we can safely use it to
14
+ * check if the stream is remote and dispose it when needed.
15
+ */
16
+ private trackedStreams;
10
17
  /**
11
18
  * Constructs a new `Subscriber` instance.
12
19
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.52.0",
3
+ "version": "1.52.1-beta.0",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.es.js",
6
6
  "browser": "dist/index.browser.es.js",
package/src/Call.ts CHANGED
@@ -321,6 +321,7 @@ export class Call {
321
321
  private joinResponseTimeout?: number;
322
322
  private rpcRequestTimeout?: number;
323
323
  private joinCallData?: JoinCallData;
324
+ private selfSubEnabled = false;
324
325
  private hasJoinedOnce = false;
325
326
  private deviceSettingsAppliedOnce = false;
326
327
  private credentials?: Credentials;
@@ -817,6 +818,37 @@ export class Call {
817
818
  return this.clientStore.connectedUser?.id;
818
819
  }
819
820
 
821
+ /**
822
+ * A flag indicating whether self-subscription is enabled for the call.
823
+ */
824
+ get isSelfSubEnabled() {
825
+ return this.selfSubEnabled;
826
+ }
827
+
828
+ /**
829
+ * The largest video publish dimension across the current publish options.
830
+ *
831
+ * @internal
832
+ */
833
+ getMaxVideoPublishDimension = (): VideoDimension | undefined => {
834
+ if (!this.currentPublishOptions) return undefined;
835
+ let maxDimension: VideoDimension | undefined;
836
+ let maxArea = 0;
837
+ for (const opt of this.currentPublishOptions) {
838
+ if (opt.trackType !== TrackType.VIDEO) continue;
839
+
840
+ const dim = opt.videoDimension;
841
+ if (!dim || !dim.width || !dim.height) continue;
842
+
843
+ const area = dim.width * dim.height;
844
+ if (area > maxArea) {
845
+ maxDimension = dim;
846
+ maxArea = area;
847
+ }
848
+ }
849
+ return maxDimension;
850
+ };
851
+
820
852
  /**
821
853
  * A flag indicating whether the call was created by the current user.
822
854
  */
@@ -1029,11 +1061,13 @@ export class Call {
1029
1061
  maxJoinRetries = 3,
1030
1062
  joinResponseTimeout,
1031
1063
  rpcRequestTimeout,
1064
+ selfSubEnabled = false,
1032
1065
  ...data
1033
1066
  }: JoinCallData & {
1034
1067
  maxJoinRetries?: number;
1035
1068
  joinResponseTimeout?: number;
1036
1069
  rpcRequestTimeout?: number;
1070
+ selfSubEnabled?: boolean;
1037
1071
  } = {}): Promise<void> => {
1038
1072
  const callingState = this.state.callingState;
1039
1073
 
@@ -1044,6 +1078,11 @@ export class Call {
1044
1078
  if (data?.ring) {
1045
1079
  this.ringingSubject.next(true);
1046
1080
  }
1081
+
1082
+ // we need this to be set before the callingx.joinCall() is
1083
+ // called to avoid registering the test call in the CallKit/Telecom
1084
+ this.selfSubEnabled = selfSubEnabled;
1085
+
1047
1086
  const callingX = globalThis.streamRNVideoSDK?.callingX;
1048
1087
  if (callingX) {
1049
1088
  // for Android/iOS, we need to start the call in the callingx library as soon as possible
@@ -1497,7 +1536,13 @@ export class Call {
1497
1536
  if (closePreviousInstances && this.publisher) {
1498
1537
  await this.publisher.dispose();
1499
1538
  }
1500
- this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
1539
+ this.publisher = new Publisher(
1540
+ basePeerConnectionOptions,
1541
+ publishOptions,
1542
+ {
1543
+ selfSubEnabled: this.selfSubEnabled,
1544
+ },
1545
+ );
1501
1546
  }
1502
1547
 
1503
1548
  this.statsReporter?.stop();
@@ -1,10 +1,4 @@
1
1
  /* eslint-disable */
2
- import type {
3
- JsonObject,
4
- JsonReadOptions,
5
- JsonValue,
6
- JsonWriteOptions,
7
- } from '@protobuf-ts/runtime';
8
2
  // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
9
3
  // @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
10
4
  // tslint:disable
@@ -39,12 +33,13 @@ import type {
39
33
  // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
40
34
  // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
41
35
  //
42
- import {
43
- isJsonObject,
44
- MessageType,
45
- typeofJsonValue,
46
- } from '@protobuf-ts/runtime';
47
-
36
+ import { isJsonObject } from '@protobuf-ts/runtime';
37
+ import { typeofJsonValue } from '@protobuf-ts/runtime';
38
+ import type { JsonValue } from '@protobuf-ts/runtime';
39
+ import type { JsonReadOptions } from '@protobuf-ts/runtime';
40
+ import type { JsonWriteOptions } from '@protobuf-ts/runtime';
41
+ import type { JsonObject } from '@protobuf-ts/runtime';
42
+ import { MessageType } from '@protobuf-ts/runtime';
48
43
  /**
49
44
  * `Struct` represents a structured data value, consisting of fields
50
45
  * which map to dynamically typed values. In some languages, `Struct`
@@ -1,9 +1,4 @@
1
1
  /* eslint-disable */
2
- import type {
3
- JsonReadOptions,
4
- JsonValue,
5
- JsonWriteOptions,
6
- } from '@protobuf-ts/runtime';
7
2
  // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
8
3
  // @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
9
4
  // tslint:disable
@@ -38,8 +33,12 @@ import type {
38
33
  // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
39
34
  // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40
35
  //
41
- import { MessageType, PbLong, typeofJsonValue } from '@protobuf-ts/runtime';
42
-
36
+ import { typeofJsonValue } from '@protobuf-ts/runtime';
37
+ import type { JsonValue } from '@protobuf-ts/runtime';
38
+ import type { JsonReadOptions } from '@protobuf-ts/runtime';
39
+ import type { JsonWriteOptions } from '@protobuf-ts/runtime';
40
+ import { PbLong } from '@protobuf-ts/runtime';
41
+ import { MessageType } from '@protobuf-ts/runtime';
43
42
  /**
44
43
  * A Timestamp represents a point in time independent of any time zone or local
45
44
  * calendar, encoded as a count of seconds and fractions of seconds at
@@ -1,33 +1,31 @@
1
+
1
2
  // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
2
3
  // @generated from protobuf file "video/sfu/event/events.proto" (package "stream.video.sfu.event", syntax proto3)
3
4
  // tslint:disable
4
5
  import { MessageType } from '@protobuf-ts/runtime';
5
- import {
6
- CallEndedReason,
7
- CallGrants,
8
- CallState,
9
- ClientCapability,
10
- ClientDetails,
11
- Codec,
12
- ConnectionQuality,
13
- DegradationPreference,
14
- Error as Error$,
15
- GoAwayReason,
16
- ICETrickle as ICETrickle$,
17
- Participant,
18
- ParticipantCount,
19
- ParticipantSource,
20
- PeerType,
21
- Pin,
22
- PublishOption,
23
- SubscribeOption,
24
- TrackInfo,
25
- TrackType,
26
- TrackUnpublishReason,
27
- WebsocketReconnectStrategy,
28
- } from '../models/models';
6
+ import { CallEndedReason } from '../models/models';
7
+ import { GoAwayReason } from '../models/models';
8
+ import { CallGrants } from '../models/models';
9
+ import { DegradationPreference } from '../models/models';
10
+ import { Codec } from '../models/models';
11
+ import { ConnectionQuality } from '../models/models';
12
+ import { CallState } from '../models/models';
29
13
  import { TrackSubscriptionDetails } from '../signal_rpc/signal';
30
-
14
+ import { TrackInfo } from '../models/models';
15
+ import { ParticipantSource } from '../models/models';
16
+ import { ClientCapability } from '../models/models';
17
+ import { SubscribeOption } from '../models/models';
18
+ import { ClientDetails } from '../models/models';
19
+ import { TrackUnpublishReason } from '../models/models';
20
+ import { Participant } from '../models/models';
21
+ import { TrackType } from '../models/models';
22
+ import { ParticipantCount } from '../models/models';
23
+ import { PeerType } from '../models/models';
24
+ import { WebsocketReconnectStrategy } from '../models/models';
25
+ import { Error as Error$ } from '../models/models';
26
+ import { Pin } from '../models/models';
27
+ import { PublishOption } from '../models/models';
28
+ import { ICETrickle as ICETrickle$ } from '../models/models';
31
29
  /**
32
30
  * SFUEvent is a message that is sent from the SFU to the client.
33
31
  *
@@ -1,10 +1,10 @@
1
+
1
2
  // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
2
3
  // @generated from protobuf file "video/sfu/models/models.proto" (package "stream.video.sfu.models", syntax proto3)
3
4
  // tslint:disable
4
5
  import { MessageType } from '@protobuf-ts/runtime';
5
6
  import { Struct } from '../../../google/protobuf/struct';
6
7
  import { Timestamp } from '../../../google/protobuf/timestamp';
7
-
8
8
  /**
9
9
  * CallState is the current state of the call
10
10
  * as seen by an SFU.
@@ -415,6 +415,10 @@ export interface TrackInfo {
415
415
  * @generated from protobuf field: int32 publish_option_id = 12;
416
416
  */
417
417
  publishOptionId: number;
418
+ /**
419
+ * @generated from protobuf field: bool self_sub_audio_video = 13;
420
+ */
421
+ selfSubAudioVideo: boolean;
418
422
  }
419
423
  /**
420
424
  * @generated from protobuf message stream.video.sfu.models.Error
@@ -1770,6 +1774,12 @@ class TrackInfo$Type extends MessageType<TrackInfo> {
1770
1774
  kind: 'scalar',
1771
1775
  T: 5 /*ScalarType.INT32*/,
1772
1776
  },
1777
+ {
1778
+ no: 13,
1779
+ name: 'self_sub_audio_video',
1780
+ kind: 'scalar',
1781
+ T: 8 /*ScalarType.BOOL*/,
1782
+ },
1773
1783
  ]);
1774
1784
  }
1775
1785
  }
@@ -1,37 +1,33 @@
1
+
1
2
  // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
2
3
  // @generated from protobuf file "video/sfu/signal_rpc/signal.proto" (package "stream.video.sfu.signal", syntax proto3)
3
4
  // tslint:disable
4
- import type {
5
- RpcOptions,
6
- RpcTransport,
7
- ServiceInfo,
8
- UnaryCall,
9
- } from '@protobuf-ts/runtime-rpc';
10
- import { stackIntercept } from '@protobuf-ts/runtime-rpc';
11
- import type {
12
- ICERestartRequest,
13
- ICERestartResponse,
14
- ICETrickleResponse,
15
- SendAnswerRequest,
16
- SendAnswerResponse,
17
- SendMetricsRequest,
18
- SendMetricsResponse,
19
- SendStatsRequest,
20
- SendStatsResponse,
21
- SetPublisherRequest,
22
- SetPublisherResponse,
23
- StartNoiseCancellationRequest,
24
- StartNoiseCancellationResponse,
25
- StopNoiseCancellationRequest,
26
- StopNoiseCancellationResponse,
27
- UpdateMuteStatesRequest,
28
- UpdateMuteStatesResponse,
29
- UpdateSubscriptionsRequest,
30
- UpdateSubscriptionsResponse,
31
- } from './signal';
5
+ import type { RpcTransport } from '@protobuf-ts/runtime-rpc';
6
+ import type { ServiceInfo } from '@protobuf-ts/runtime-rpc';
32
7
  import { SignalServer } from './signal';
8
+ import type { StopNoiseCancellationResponse } from './signal';
9
+ import type { StopNoiseCancellationRequest } from './signal';
10
+ import type { StartNoiseCancellationResponse } from './signal';
11
+ import type { StartNoiseCancellationRequest } from './signal';
12
+ import type { SendMetricsResponse } from './signal';
13
+ import type { SendMetricsRequest } from './signal';
14
+ import type { SendStatsResponse } from './signal';
15
+ import type { SendStatsRequest } from './signal';
16
+ import type { ICERestartResponse } from './signal';
17
+ import type { ICERestartRequest } from './signal';
18
+ import type { UpdateMuteStatesResponse } from './signal';
19
+ import type { UpdateMuteStatesRequest } from './signal';
20
+ import type { UpdateSubscriptionsResponse } from './signal';
21
+ import type { UpdateSubscriptionsRequest } from './signal';
22
+ import type { ICETrickleResponse } from './signal';
33
23
  import type { ICETrickle } from '../models/models';
34
-
24
+ import type { SendAnswerResponse } from './signal';
25
+ import type { SendAnswerRequest } from './signal';
26
+ import { stackIntercept } from '@protobuf-ts/runtime-rpc';
27
+ import type { SetPublisherResponse } from './signal';
28
+ import type { SetPublisherRequest } from './signal';
29
+ import type { UnaryCall } from '@protobuf-ts/runtime-rpc';
30
+ import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
35
31
  /**
36
32
  * @generated from protobuf service stream.video.sfu.signal.SignalServer
37
33
  */
@@ -1,3 +1,4 @@
1
+
1
2
  // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
2
3
  // @generated from protobuf file "video/sfu/signal_rpc/signal.proto" (package "stream.video.sfu.signal", syntax proto3)
3
4
  // tslint:disable
@@ -192,6 +192,6 @@ export const getClientDetails = async (): Promise<ClientDetails> => {
192
192
  .join(' '),
193
193
  version: '',
194
194
  },
195
- webrtcVersion: browserVersion,
195
+ webrtcVersion: webRtcInfo?.version || '',
196
196
  };
197
197
  };
@@ -39,6 +39,7 @@ export class Publisher extends BasePeerConnection {
39
39
  private readonly transceiverCache = new TransceiverCache();
40
40
  private readonly clonedTracks = new Set<MediaStreamTrack>();
41
41
  private publishOptions: PublishOption[];
42
+ private readonly selfSubEnabled: boolean;
42
43
 
43
44
  /**
44
45
  * Constructs a new `Publisher` instance.
@@ -46,9 +47,11 @@ export class Publisher extends BasePeerConnection {
46
47
  constructor(
47
48
  baseOptions: BasePeerConnectionOpts,
48
49
  publishOptions: PublishOption[],
50
+ opts: { selfSubEnabled?: boolean } = {},
49
51
  ) {
50
52
  super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
51
53
  this.publishOptions = publishOptions;
54
+ this.selfSubEnabled = opts.selfSubEnabled ?? false;
52
55
 
53
56
  this.on('iceRestart', (iceRestart) => {
54
57
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED) return;
@@ -576,6 +579,7 @@ export class Publisher extends BasePeerConnection {
576
579
  muted: !isTrackLive,
577
580
  codec: publishOption.codec,
578
581
  publishOptionId: publishOption.id,
582
+ selfSubAudioVideo: this.selfSubEnabled,
579
583
  };
580
584
  };
581
585
 
@@ -14,6 +14,14 @@ import { enableStereo, removeCodecsExcept } from './helpers/sdp';
14
14
  * @internal
15
15
  */
16
16
  export class Subscriber extends BasePeerConnection {
17
+ /**
18
+ * Remote streams received from the SFU. For a self-sub case
19
+ * we need to be able to distinguish between the local capture stream.
20
+ * The map will never contain local streams so we can safely use it to
21
+ * check if the stream is remote and dispose it when needed.
22
+ */
23
+ private trackedStreams: WeakSet<MediaStream> = new WeakSet();
24
+
17
25
  /**
18
26
  * Constructs a new `Subscriber` instance.
19
27
  */
@@ -75,6 +83,7 @@ export class Subscriber extends BasePeerConnection {
75
83
  const participantToUpdate = this.state.participants.find(
76
84
  (p) => p.trackLookupPrefix === trackId,
77
85
  );
86
+ const isSelfSub = !!participantToUpdate?.isLocalParticipant;
78
87
  this.logger.debug(
79
88
  `[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`,
80
89
  track.id,
@@ -107,6 +116,10 @@ export class Subscriber extends BasePeerConnection {
107
116
 
108
117
  this.trackIdToTrackType.set(track.id, trackType);
109
118
 
119
+ if (isSelfSub) {
120
+ this.trackedStreams.add(primaryStream);
121
+ }
122
+
110
123
  if (!participantToUpdate) {
111
124
  this.logger.warn(
112
125
  `[onTrack]: Received track for unknown participant: ${trackId}`,
@@ -127,6 +140,13 @@ export class Subscriber extends BasePeerConnection {
127
140
  return;
128
141
  }
129
142
 
143
+ // Self-sub loopback audio routes to the speaker by default, which
144
+ // would echo the local user's voice. Default-mute here; consumers
145
+ // (the loopback recording hook) re-enable explicitly when needed.
146
+ if (isSelfSub && e.track.kind === 'audio') {
147
+ e.track.enabled = false;
148
+ }
149
+
130
150
  // get the previous stream to dispose it later
131
151
  // usually this happens during migration, when the stream is replaced
132
152
  // with a new one but the old one is still in the state
@@ -137,8 +157,15 @@ export class Subscriber extends BasePeerConnection {
137
157
  [streamKindProp]: primaryStream,
138
158
  });
139
159
 
140
- // now, dispose the previous stream if it exists
141
160
  if (previousStream) {
161
+ if (isSelfSub && !this.trackedStreams.has(previousStream)) {
162
+ // this is the local capture stream, we don't want to dispose it
163
+ this.logger.debug(
164
+ `[onTrack]: Skipping cleanup of previous ${e.track.kind} stream for userId: ${participantToUpdate.userId} because it is not tracked`,
165
+ );
166
+ return;
167
+ }
168
+
142
169
  this.logger.info(
143
170
  `[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`,
144
171
  );