@stream-io/video-client 1.54.0 → 1.54.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.
@@ -129,6 +129,7 @@ export declare class Call {
129
129
  private joinResponseTimeout?;
130
130
  private rpcRequestTimeout?;
131
131
  private joinCallData?;
132
+ private selfSubEnabled;
132
133
  private hasJoinedOnce;
133
134
  private deviceSettingsAppliedOnce;
134
135
  private credentials?;
@@ -192,6 +193,16 @@ export declare class Call {
192
193
  * Retrieves the current user ID.
193
194
  */
194
195
  get currentUserId(): string | undefined;
196
+ /**
197
+ * A flag indicating whether self-subscription is enabled for the call.
198
+ */
199
+ get isSelfSubEnabled(): boolean;
200
+ /**
201
+ * The largest video publish dimension across the current publish options.
202
+ *
203
+ * @internal
204
+ */
205
+ getMaxVideoPublishDimension: () => VideoDimension | undefined;
195
206
  /**
196
207
  * A flag indicating whether the call was created by the current user.
197
208
  */
@@ -264,10 +275,11 @@ export declare class Call {
264
275
  *
265
276
  * @returns a promise which resolves once the call join-flow has finished.
266
277
  */
267
- join: ({ maxJoinRetries, joinResponseTimeout, rpcRequestTimeout, ...data }?: JoinCallData & {
278
+ join: ({ maxJoinRetries, joinResponseTimeout, rpcRequestTimeout, selfSubEnabled, ...data }?: JoinCallData & {
268
279
  maxJoinRetries?: number;
269
280
  joinResponseTimeout?: number;
270
281
  rpcRequestTimeout?: number;
282
+ selfSubEnabled?: boolean;
271
283
  }) => Promise<void>;
272
284
  /**
273
285
  * 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.54.0",
3
+ "version": "1.54.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
@@ -325,6 +325,7 @@ export class Call {
325
325
  private joinResponseTimeout?: number;
326
326
  private rpcRequestTimeout?: number;
327
327
  private joinCallData?: JoinCallData;
328
+ private selfSubEnabled = false;
328
329
  private hasJoinedOnce = false;
329
330
  private deviceSettingsAppliedOnce = false;
330
331
  private credentials?: Credentials;
@@ -830,6 +831,37 @@ export class Call {
830
831
  return this.clientStore.connectedUser?.id;
831
832
  }
832
833
 
834
+ /**
835
+ * A flag indicating whether self-subscription is enabled for the call.
836
+ */
837
+ get isSelfSubEnabled() {
838
+ return this.selfSubEnabled;
839
+ }
840
+
841
+ /**
842
+ * The largest video publish dimension across the current publish options.
843
+ *
844
+ * @internal
845
+ */
846
+ getMaxVideoPublishDimension = (): VideoDimension | undefined => {
847
+ if (!this.currentPublishOptions) return undefined;
848
+ let maxDimension: VideoDimension | undefined;
849
+ let maxArea = 0;
850
+ for (const opt of this.currentPublishOptions) {
851
+ if (opt.trackType !== TrackType.VIDEO) continue;
852
+
853
+ const dim = opt.videoDimension;
854
+ if (!dim || !dim.width || !dim.height) continue;
855
+
856
+ const area = dim.width * dim.height;
857
+ if (area > maxArea) {
858
+ maxDimension = dim;
859
+ maxArea = area;
860
+ }
861
+ }
862
+ return maxDimension;
863
+ };
864
+
833
865
  /**
834
866
  * A flag indicating whether the call was created by the current user.
835
867
  */
@@ -1042,11 +1074,13 @@ export class Call {
1042
1074
  maxJoinRetries = 3,
1043
1075
  joinResponseTimeout,
1044
1076
  rpcRequestTimeout,
1077
+ selfSubEnabled = false,
1045
1078
  ...data
1046
1079
  }: JoinCallData & {
1047
1080
  maxJoinRetries?: number;
1048
1081
  joinResponseTimeout?: number;
1049
1082
  rpcRequestTimeout?: number;
1083
+ selfSubEnabled?: boolean;
1050
1084
  } = {}): Promise<void> => {
1051
1085
  const callingState = this.state.callingState;
1052
1086
 
@@ -1057,6 +1091,11 @@ export class Call {
1057
1091
  if (data?.ring) {
1058
1092
  this.ringingSubject.next(true);
1059
1093
  }
1094
+
1095
+ // we need this to be set before the callingx.joinCall() is
1096
+ // called to avoid registering the test call in the CallKit/Telecom
1097
+ this.selfSubEnabled = selfSubEnabled;
1098
+
1060
1099
  const callingX = globalThis.streamRNVideoSDK?.callingX;
1061
1100
  if (callingX) {
1062
1101
  // for Android/iOS, we need to start the call in the callingx library as soon as possible
@@ -1553,7 +1592,13 @@ export class Call {
1553
1592
  if (closePreviousInstances && this.publisher) {
1554
1593
  await this.publisher.dispose();
1555
1594
  }
1556
- this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
1595
+ this.publisher = new Publisher(
1596
+ basePeerConnectionOptions,
1597
+ publishOptions,
1598
+ {
1599
+ selfSubEnabled: this.selfSubEnabled,
1600
+ },
1601
+ );
1557
1602
  }
1558
1603
 
1559
1604
  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,
@@ -108,6 +117,10 @@ export class Subscriber extends BasePeerConnection {
108
117
 
109
118
  this.trackIdToTrackType.set(track.id, trackType);
110
119
 
120
+ if (isSelfSub) {
121
+ this.trackedStreams.add(primaryStream);
122
+ }
123
+
111
124
  if (!participantToUpdate) {
112
125
  this.logger.warn(
113
126
  `[onTrack]: Received track for unknown participant: ${trackId}`,
@@ -128,6 +141,13 @@ export class Subscriber extends BasePeerConnection {
128
141
  return;
129
142
  }
130
143
 
144
+ // Self-sub loopback audio routes to the speaker by default, which
145
+ // would echo the local user's voice. Default-mute here; consumers
146
+ // (the loopback recording hook) re-enable explicitly when needed.
147
+ if (isSelfSub && e.track.kind === 'audio') {
148
+ e.track.enabled = false;
149
+ }
150
+
131
151
  // get the previous stream to dispose it later
132
152
  // usually this happens during migration, when the stream is replaced
133
153
  // with a new one but the old one is still in the state
@@ -138,8 +158,15 @@ export class Subscriber extends BasePeerConnection {
138
158
  [streamKindProp]: primaryStream,
139
159
  });
140
160
 
141
- // now, dispose the previous stream if it exists
142
161
  if (previousStream) {
162
+ if (isSelfSub && !this.trackedStreams.has(previousStream)) {
163
+ // this is the local capture stream, we don't want to dispose it
164
+ this.logger.debug(
165
+ `[onTrack]: Skipping cleanup of previous ${e.track.kind} stream for userId: ${participantToUpdate.userId} because it is not tracked`,
166
+ );
167
+ return;
168
+ }
169
+
143
170
  this.logger.info(
144
171
  `[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`,
145
172
  );