livekit-client 0.18.4-RC6 → 0.18.4

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.
Files changed (127) hide show
  1. package/README.md +2 -5
  2. package/dist/api/RequestQueue.d.ts +13 -12
  3. package/dist/api/RequestQueue.d.ts.map +1 -0
  4. package/dist/api/SignalClient.d.ts +67 -66
  5. package/dist/api/SignalClient.d.ts.map +1 -0
  6. package/dist/connect.d.ts +24 -23
  7. package/dist/connect.d.ts.map +1 -0
  8. package/dist/index.d.ts +27 -26
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/livekit-client.esm.mjs +546 -486
  11. package/dist/livekit-client.esm.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/logger.d.ts +26 -25
  15. package/dist/logger.d.ts.map +1 -0
  16. package/dist/options.d.ts +128 -127
  17. package/dist/options.d.ts.map +1 -0
  18. package/dist/proto/google/protobuf/timestamp.d.ts +133 -132
  19. package/dist/proto/google/protobuf/timestamp.d.ts.map +1 -0
  20. package/dist/proto/livekit_models.d.ts +876 -868
  21. package/dist/proto/livekit_models.d.ts.map +1 -0
  22. package/dist/proto/livekit_rtc.d.ts +3904 -3859
  23. package/dist/proto/livekit_rtc.d.ts.map +1 -0
  24. package/dist/room/DeviceManager.d.ts +8 -7
  25. package/dist/room/DeviceManager.d.ts.map +1 -0
  26. package/dist/room/PCTransport.d.ts +16 -15
  27. package/dist/room/PCTransport.d.ts.map +1 -0
  28. package/dist/room/RTCEngine.d.ts +67 -66
  29. package/dist/room/RTCEngine.d.ts.map +1 -0
  30. package/dist/room/Room.d.ts +166 -165
  31. package/dist/room/Room.d.ts.map +1 -0
  32. package/dist/room/errors.d.ts +29 -28
  33. package/dist/room/errors.d.ts.map +1 -0
  34. package/dist/room/events.d.ts +391 -390
  35. package/dist/room/events.d.ts.map +1 -0
  36. package/dist/room/participant/LocalParticipant.d.ts +126 -125
  37. package/dist/room/participant/LocalParticipant.d.ts.map +1 -0
  38. package/dist/room/participant/Participant.d.ts +94 -93
  39. package/dist/room/participant/Participant.d.ts.map +1 -0
  40. package/dist/room/participant/ParticipantTrackPermission.d.ts +26 -19
  41. package/dist/room/participant/ParticipantTrackPermission.d.ts.map +1 -0
  42. package/dist/room/participant/RemoteParticipant.d.ts +40 -39
  43. package/dist/room/participant/RemoteParticipant.d.ts.map +1 -0
  44. package/dist/room/participant/publishUtils.d.ts +18 -17
  45. package/dist/room/participant/publishUtils.d.ts.map +1 -0
  46. package/dist/room/stats.d.ts +66 -65
  47. package/dist/room/stats.d.ts.map +1 -0
  48. package/dist/room/track/LocalAudioTrack.d.ts +20 -19
  49. package/dist/room/track/LocalAudioTrack.d.ts.map +1 -0
  50. package/dist/room/track/LocalTrack.d.ts +28 -27
  51. package/dist/room/track/LocalTrack.d.ts.map +1 -0
  52. package/dist/room/track/LocalTrackPublication.d.ts +38 -37
  53. package/dist/room/track/LocalTrackPublication.d.ts.map +1 -0
  54. package/dist/room/track/LocalVideoTrack.d.ts +31 -30
  55. package/dist/room/track/LocalVideoTrack.d.ts.map +1 -0
  56. package/dist/room/track/RemoteAudioTrack.d.ts +20 -19
  57. package/dist/room/track/RemoteAudioTrack.d.ts.map +1 -0
  58. package/dist/room/track/RemoteTrack.d.ts +16 -15
  59. package/dist/room/track/RemoteTrack.d.ts.map +1 -0
  60. package/dist/room/track/RemoteTrackPublication.d.ts +51 -50
  61. package/dist/room/track/RemoteTrackPublication.d.ts.map +1 -0
  62. package/dist/room/track/RemoteVideoTrack.d.ts +28 -27
  63. package/dist/room/track/RemoteVideoTrack.d.ts.map +1 -0
  64. package/dist/room/track/Track.d.ts +101 -100
  65. package/dist/room/track/Track.d.ts.map +1 -0
  66. package/dist/room/track/TrackPublication.d.ts +50 -49
  67. package/dist/room/track/TrackPublication.d.ts.map +1 -0
  68. package/dist/room/track/create.d.ts +24 -23
  69. package/dist/room/track/create.d.ts.map +1 -0
  70. package/dist/room/track/defaults.d.ts +5 -4
  71. package/dist/room/track/defaults.d.ts.map +1 -0
  72. package/dist/room/track/options.d.ts +223 -222
  73. package/dist/room/track/options.d.ts.map +1 -0
  74. package/dist/room/track/types.d.ts +19 -18
  75. package/dist/room/track/types.d.ts.map +1 -0
  76. package/dist/room/track/utils.d.ts +14 -13
  77. package/dist/room/track/utils.d.ts.map +1 -0
  78. package/dist/room/utils.d.ts +17 -15
  79. package/dist/room/utils.d.ts.map +1 -0
  80. package/dist/test/mocks.d.ts +12 -11
  81. package/dist/test/mocks.d.ts.map +1 -0
  82. package/dist/version.d.ts +3 -2
  83. package/dist/version.d.ts.map +1 -0
  84. package/package.json +4 -5
  85. package/src/api/RequestQueue.ts +53 -0
  86. package/src/api/SignalClient.ts +497 -0
  87. package/src/connect.ts +98 -0
  88. package/src/index.ts +49 -0
  89. package/src/logger.ts +56 -0
  90. package/src/options.ts +156 -0
  91. package/src/proto/google/protobuf/timestamp.ts +216 -0
  92. package/src/proto/livekit_models.ts +2456 -0
  93. package/src/proto/livekit_rtc.ts +2859 -0
  94. package/src/room/DeviceManager.ts +80 -0
  95. package/src/room/PCTransport.ts +88 -0
  96. package/src/room/RTCEngine.ts +695 -0
  97. package/src/room/Room.ts +970 -0
  98. package/src/room/errors.ts +65 -0
  99. package/src/room/events.ts +438 -0
  100. package/src/room/participant/LocalParticipant.ts +755 -0
  101. package/src/room/participant/Participant.ts +287 -0
  102. package/src/room/participant/ParticipantTrackPermission.ts +42 -0
  103. package/src/room/participant/RemoteParticipant.ts +263 -0
  104. package/src/room/participant/publishUtils.test.ts +144 -0
  105. package/src/room/participant/publishUtils.ts +229 -0
  106. package/src/room/stats.ts +134 -0
  107. package/src/room/track/LocalAudioTrack.ts +134 -0
  108. package/src/room/track/LocalTrack.ts +229 -0
  109. package/src/room/track/LocalTrackPublication.ts +87 -0
  110. package/src/room/track/LocalVideoTrack.test.ts +72 -0
  111. package/src/room/track/LocalVideoTrack.ts +295 -0
  112. package/src/room/track/RemoteAudioTrack.ts +86 -0
  113. package/src/room/track/RemoteTrack.ts +62 -0
  114. package/src/room/track/RemoteTrackPublication.ts +207 -0
  115. package/src/room/track/RemoteVideoTrack.ts +240 -0
  116. package/src/room/track/Track.ts +358 -0
  117. package/src/room/track/TrackPublication.ts +120 -0
  118. package/src/room/track/create.ts +122 -0
  119. package/src/room/track/defaults.ts +27 -0
  120. package/src/room/track/options.ts +281 -0
  121. package/src/room/track/types.ts +20 -0
  122. package/src/room/track/utils.test.ts +110 -0
  123. package/src/room/track/utils.ts +113 -0
  124. package/src/room/utils.ts +115 -0
  125. package/src/test/mocks.ts +17 -0
  126. package/src/version.ts +2 -0
  127. package/CHANGELOG.md +0 -5
@@ -0,0 +1,287 @@
1
+ import { EventEmitter } from 'events';
2
+ import type TypedEmitter from 'typed-emitter';
3
+ import {
4
+ ConnectionQuality as ProtoQuality,
5
+ DataPacket_Kind,
6
+ ParticipantInfo,
7
+ ParticipantPermission,
8
+ } from '../../proto/livekit_models';
9
+ import { ParticipantEvent, TrackEvent } from '../events';
10
+ import LocalTrackPublication from '../track/LocalTrackPublication';
11
+ import RemoteTrackPublication from '../track/RemoteTrackPublication';
12
+ import { Track } from '../track/Track';
13
+ import { TrackPublication } from '../track/TrackPublication';
14
+ import { RemoteTrack } from '../track/types';
15
+
16
+ export enum ConnectionQuality {
17
+ Excellent = 'excellent',
18
+ Good = 'good',
19
+ Poor = 'poor',
20
+ Unknown = 'unknown',
21
+ }
22
+
23
+ function qualityFromProto(q: ProtoQuality): ConnectionQuality {
24
+ switch (q) {
25
+ case ProtoQuality.EXCELLENT:
26
+ return ConnectionQuality.Excellent;
27
+ case ProtoQuality.GOOD:
28
+ return ConnectionQuality.Good;
29
+ case ProtoQuality.POOR:
30
+ return ConnectionQuality.Poor;
31
+ default:
32
+ return ConnectionQuality.Unknown;
33
+ }
34
+ }
35
+
36
+ export default class Participant extends (EventEmitter as new () => TypedEmitter<ParticipantEventCallbacks>) {
37
+ protected participantInfo?: ParticipantInfo;
38
+
39
+ audioTracks: Map<string, TrackPublication>;
40
+
41
+ videoTracks: Map<string, TrackPublication>;
42
+
43
+ /** map of track sid => all published tracks */
44
+ tracks: Map<string, TrackPublication>;
45
+
46
+ /** audio level between 0-1.0, 1 being loudest, 0 being softest */
47
+ audioLevel: number = 0;
48
+
49
+ /** if participant is currently speaking */
50
+ isSpeaking: boolean = false;
51
+
52
+ /** server assigned unique id */
53
+ sid: string;
54
+
55
+ /** client assigned identity, encoded in JWT token */
56
+ identity: string;
57
+
58
+ /** client assigned display name, encoded in JWT token */
59
+ name?: string;
60
+
61
+ /** client metadata, opaque to livekit */
62
+ metadata?: string;
63
+
64
+ lastSpokeAt?: Date | undefined;
65
+
66
+ permissions?: ParticipantPermission;
67
+
68
+ private _connectionQuality: ConnectionQuality = ConnectionQuality.Unknown;
69
+
70
+ /** @internal */
71
+ constructor(sid: string, identity: string) {
72
+ super();
73
+ this.sid = sid;
74
+ this.identity = identity;
75
+ this.audioTracks = new Map();
76
+ this.videoTracks = new Map();
77
+ this.tracks = new Map();
78
+ }
79
+
80
+ getTracks(): TrackPublication[] {
81
+ return Array.from(this.tracks.values());
82
+ }
83
+
84
+ /**
85
+ * Finds the first track that matches the source filter, for example, getting
86
+ * the user's camera track with getTrackBySource(Track.Source.Camera).
87
+ * @param source
88
+ * @returns
89
+ */
90
+ getTrack(source: Track.Source): TrackPublication | undefined {
91
+ if (source === Track.Source.Unknown) {
92
+ return;
93
+ }
94
+ for (const [, pub] of this.tracks) {
95
+ if (pub.source === source) {
96
+ return pub;
97
+ }
98
+ if (pub.source === Track.Source.Unknown) {
99
+ if (
100
+ source === Track.Source.Microphone &&
101
+ pub.kind === Track.Kind.Audio &&
102
+ pub.trackName !== 'screen'
103
+ ) {
104
+ return pub;
105
+ }
106
+ if (
107
+ source === Track.Source.Camera &&
108
+ pub.kind === Track.Kind.Video &&
109
+ pub.trackName !== 'screen'
110
+ ) {
111
+ return pub;
112
+ }
113
+ if (
114
+ source === Track.Source.ScreenShare &&
115
+ pub.kind === Track.Kind.Video &&
116
+ pub.trackName === 'screen'
117
+ ) {
118
+ return pub;
119
+ }
120
+ if (
121
+ source === Track.Source.ScreenShareAudio &&
122
+ pub.kind === Track.Kind.Audio &&
123
+ pub.trackName === 'screen'
124
+ ) {
125
+ return pub;
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Finds the first track that matches the track's name.
133
+ * @param name
134
+ * @returns
135
+ */
136
+ getTrackByName(name: string): TrackPublication | undefined {
137
+ for (const [, pub] of this.tracks) {
138
+ if (pub.trackName === name) {
139
+ return pub;
140
+ }
141
+ }
142
+ }
143
+
144
+ get connectionQuality(): ConnectionQuality {
145
+ return this._connectionQuality;
146
+ }
147
+
148
+ get isCameraEnabled(): boolean {
149
+ const track = this.getTrack(Track.Source.Camera);
150
+ return !(track?.isMuted ?? true);
151
+ }
152
+
153
+ get isMicrophoneEnabled(): boolean {
154
+ const track = this.getTrack(Track.Source.Microphone);
155
+ return !(track?.isMuted ?? true);
156
+ }
157
+
158
+ get isScreenShareEnabled(): boolean {
159
+ const track = this.getTrack(Track.Source.ScreenShare);
160
+ return !!track;
161
+ }
162
+
163
+ /** when participant joined the room */
164
+ get joinedAt(): Date | undefined {
165
+ if (this.participantInfo) {
166
+ return new Date(this.participantInfo.joinedAt * 1000);
167
+ }
168
+ return new Date();
169
+ }
170
+
171
+ /** @internal */
172
+ updateInfo(info: ParticipantInfo) {
173
+ this.identity = info.identity;
174
+ this.sid = info.sid;
175
+ this.name = info.name;
176
+ this.setMetadata(info.metadata);
177
+ if (info.permission) {
178
+ this.setPermissions(info.permission);
179
+ }
180
+ // set this last so setMetadata can detect changes
181
+ this.participantInfo = info;
182
+ }
183
+
184
+ /** @internal */
185
+ setMetadata(md: string) {
186
+ const changed = this.metadata !== md;
187
+ const prevMetadata = this.metadata;
188
+ this.metadata = md;
189
+
190
+ if (changed) {
191
+ this.emit(ParticipantEvent.MetadataChanged, prevMetadata);
192
+ this.emit(ParticipantEvent.ParticipantMetadataChanged, prevMetadata);
193
+ }
194
+ }
195
+
196
+ /** @internal */
197
+ setPermissions(permissions: ParticipantPermission): boolean {
198
+ const changed =
199
+ permissions.canPublish !== this.permissions?.canPublish ||
200
+ permissions.canSubscribe !== this.permissions?.canSubscribe ||
201
+ permissions.canPublishData !== this.permissions?.canPublishData ||
202
+ permissions.hidden !== this.permissions?.hidden ||
203
+ permissions.recorder !== this.permissions?.recorder;
204
+ this.permissions = permissions;
205
+
206
+ return changed;
207
+ }
208
+
209
+ /** @internal */
210
+ setIsSpeaking(speaking: boolean) {
211
+ if (speaking === this.isSpeaking) {
212
+ return;
213
+ }
214
+ this.isSpeaking = speaking;
215
+ if (speaking) {
216
+ this.lastSpokeAt = new Date();
217
+ }
218
+ this.emit(ParticipantEvent.IsSpeakingChanged, speaking);
219
+ }
220
+
221
+ /** @internal */
222
+ setConnectionQuality(q: ProtoQuality) {
223
+ const prevQuality = this._connectionQuality;
224
+ this._connectionQuality = qualityFromProto(q);
225
+ if (prevQuality !== this._connectionQuality) {
226
+ this.emit(ParticipantEvent.ConnectionQualityChanged, this._connectionQuality);
227
+ }
228
+ }
229
+
230
+ protected addTrackPublication(publication: TrackPublication) {
231
+ // forward publication driven events
232
+ publication.on(TrackEvent.Muted, () => {
233
+ this.emit(ParticipantEvent.TrackMuted, publication);
234
+ });
235
+
236
+ publication.on(TrackEvent.Unmuted, () => {
237
+ this.emit(ParticipantEvent.TrackUnmuted, publication);
238
+ });
239
+
240
+ const pub = publication;
241
+ if (pub.track) {
242
+ pub.track.sid = publication.trackSid;
243
+ }
244
+
245
+ this.tracks.set(publication.trackSid, publication);
246
+ switch (publication.kind) {
247
+ case Track.Kind.Audio:
248
+ this.audioTracks.set(publication.trackSid, publication);
249
+ break;
250
+ case Track.Kind.Video:
251
+ this.videoTracks.set(publication.trackSid, publication);
252
+ break;
253
+ default:
254
+ break;
255
+ }
256
+ }
257
+ }
258
+
259
+ export type ParticipantEventCallbacks = {
260
+ trackPublished: (publication: RemoteTrackPublication) => void;
261
+ trackSubscribed: (track: RemoteTrack, publication: RemoteTrackPublication) => void;
262
+ trackSubscriptionFailed: (trackSid: string) => void;
263
+ trackUnpublished: (publication: RemoteTrackPublication) => void;
264
+ trackUnsubscribed: (track: RemoteTrack, publication: RemoteTrackPublication) => void;
265
+ trackMuted: (publication: TrackPublication) => void;
266
+ trackUnmuted: (publication: TrackPublication) => void;
267
+ localTrackPublished: (publication: LocalTrackPublication) => void;
268
+ localTrackUnpublished: (publication: LocalTrackPublication) => void;
269
+ /**
270
+ * @deprecated use [[participantMetadataChanged]] instead
271
+ */
272
+ metadataChanged: (prevMetadata: string | undefined, participant?: any) => void;
273
+ participantMetadataChanged: (prevMetadata: string | undefined, participant?: any) => void;
274
+ dataReceived: (payload: Uint8Array, kind: DataPacket_Kind) => void;
275
+ isSpeakingChanged: (speaking: boolean) => void;
276
+ connectionQualityChanged: (connectionQuality: ConnectionQuality) => void;
277
+ trackStreamStateChanged: (
278
+ publication: RemoteTrackPublication,
279
+ streamState: Track.StreamState,
280
+ ) => void;
281
+ trackSubscriptionPermissionChanged: (
282
+ publication: RemoteTrackPublication,
283
+ status: TrackPublication.SubscriptionStatus,
284
+ ) => void;
285
+ mediaDevicesError: (error: Error) => void;
286
+ participantPermissionsChanged: (prevPermissions: ParticipantPermission) => void;
287
+ };
@@ -0,0 +1,42 @@
1
+ import { TrackPermission } from '../../proto/livekit_rtc';
2
+
3
+ export interface ParticipantTrackPermission {
4
+ /**
5
+ * The participant identity this permission applies to.
6
+ * You can either provide this or `participantSid`
7
+ */
8
+ participantIdentity?: string;
9
+
10
+ /**
11
+ * The participant server id this permission applies to.
12
+ * You can either provide this or `participantIdentity`
13
+ */
14
+ participantSid?: string;
15
+
16
+ /**
17
+ * Grant permission to all all tracks. Takes precedence over allowedTrackSids.
18
+ * false if unset.
19
+ */
20
+ allowAll?: boolean;
21
+
22
+ /**
23
+ * The list of track ids that the target participant can subscribe to.
24
+ * When unset, it'll allow all tracks to be subscribed by the participant.
25
+ * When empty, this participant is disallowed from subscribing to any tracks.
26
+ */
27
+ allowedTrackSids?: string[];
28
+ }
29
+
30
+ export function trackPermissionToProto(perms: ParticipantTrackPermission): TrackPermission {
31
+ if (!perms.participantSid && !perms.participantIdentity) {
32
+ throw new Error(
33
+ 'Invalid track permission, must provide at least one of participantIdentity and participantSid',
34
+ );
35
+ }
36
+ return {
37
+ participantIdentity: perms.participantIdentity ?? '',
38
+ participantSid: perms.participantSid ?? '',
39
+ allTracks: perms.allowAll ?? false,
40
+ trackSids: perms.allowedTrackSids || [],
41
+ };
42
+ }
@@ -0,0 +1,263 @@
1
+ import { SignalClient } from '../../api/SignalClient';
2
+ import log from '../../logger';
3
+ import { ParticipantInfo } from '../../proto/livekit_models';
4
+ import { UpdateSubscription, UpdateTrackSettings } from '../../proto/livekit_rtc';
5
+ import { ParticipantEvent, TrackEvent } from '../events';
6
+ import RemoteAudioTrack from '../track/RemoteAudioTrack';
7
+ import RemoteTrackPublication from '../track/RemoteTrackPublication';
8
+ import RemoteVideoTrack from '../track/RemoteVideoTrack';
9
+ import { Track } from '../track/Track';
10
+ import { AdaptiveStreamSettings, RemoteTrack } from '../track/types';
11
+ import Participant, { ParticipantEventCallbacks } from './Participant';
12
+
13
+ export default class RemoteParticipant extends Participant {
14
+ audioTracks: Map<string, RemoteTrackPublication>;
15
+
16
+ videoTracks: Map<string, RemoteTrackPublication>;
17
+
18
+ tracks: Map<string, RemoteTrackPublication>;
19
+
20
+ signalClient: SignalClient;
21
+
22
+ /** @internal */
23
+ static fromParticipantInfo(signalClient: SignalClient, pi: ParticipantInfo): RemoteParticipant {
24
+ const rp = new RemoteParticipant(signalClient, pi.sid, pi.identity);
25
+ rp.updateInfo(pi);
26
+ return rp;
27
+ }
28
+
29
+ /** @internal */
30
+ constructor(signalClient: SignalClient, id: string, name?: string) {
31
+ super(id, name || '');
32
+ this.signalClient = signalClient;
33
+ this.tracks = new Map();
34
+ this.audioTracks = new Map();
35
+ this.videoTracks = new Map();
36
+ }
37
+
38
+ protected addTrackPublication(publication: RemoteTrackPublication) {
39
+ super.addTrackPublication(publication);
40
+
41
+ // register action events
42
+ publication.on(TrackEvent.UpdateSettings, (settings: UpdateTrackSettings) => {
43
+ this.signalClient.sendUpdateTrackSettings(settings);
44
+ });
45
+ publication.on(TrackEvent.UpdateSubscription, (sub: UpdateSubscription) => {
46
+ sub.participantTracks.forEach((pt) => {
47
+ pt.participantSid = this.sid;
48
+ });
49
+ this.signalClient.sendUpdateSubscription(sub);
50
+ });
51
+ publication.on(TrackEvent.Ended, (track: RemoteTrack) => {
52
+ this.emit(ParticipantEvent.TrackUnsubscribed, track, publication);
53
+ });
54
+ }
55
+
56
+ getTrack(source: Track.Source): RemoteTrackPublication | undefined {
57
+ const track = super.getTrack(source);
58
+ if (track) {
59
+ return track as RemoteTrackPublication;
60
+ }
61
+ }
62
+
63
+ getTrackByName(name: string): RemoteTrackPublication | undefined {
64
+ const track = super.getTrackByName(name);
65
+ if (track) {
66
+ return track as RemoteTrackPublication;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * sets the volume on the participant's microphone track if it exists.
72
+ */
73
+ setVolume(volume: number) {
74
+ const audioPublication = this.getTrack(Track.Source.Microphone);
75
+ if (audioPublication) {
76
+ (audioPublication.track as RemoteAudioTrack).setVolume(volume);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * gets the volume on the participant's microphone track
82
+ * returns undefined if no microphone track exists
83
+ */
84
+ getVolume() {
85
+ const audioPublication = this.getTrack(Track.Source.Microphone);
86
+ if (audioPublication) {
87
+ return (audioPublication.track as RemoteAudioTrack).getVolume();
88
+ }
89
+ }
90
+
91
+ /** @internal */
92
+ addSubscribedMediaTrack(
93
+ mediaTrack: MediaStreamTrack,
94
+ sid: Track.SID,
95
+ mediaStream: MediaStream,
96
+ receiver?: RTCRtpReceiver,
97
+ adaptiveStreamSettings?: AdaptiveStreamSettings,
98
+ triesLeft?: number,
99
+ ) {
100
+ // find the track publication
101
+ // it's possible for the media track to arrive before participant info
102
+ let publication = this.getTrackPublication(sid);
103
+
104
+ // it's also possible that the browser didn't honor our original track id
105
+ // FireFox would use its own local uuid instead of server track id
106
+ if (!publication) {
107
+ if (!sid.startsWith('TR')) {
108
+ // find the first track that matches type
109
+ this.tracks.forEach((p) => {
110
+ if (!publication && mediaTrack.kind === p.kind.toString()) {
111
+ publication = p;
112
+ }
113
+ });
114
+ }
115
+ }
116
+
117
+ // when we couldn't locate the track, it's possible that the metadata hasn't
118
+ // yet arrived. Wait a bit longer for it to arrive, or fire an error
119
+ if (!publication) {
120
+ if (triesLeft === 0) {
121
+ log.error('could not find published track', { participant: this.sid, trackSid: sid });
122
+ this.emit(ParticipantEvent.TrackSubscriptionFailed, sid);
123
+ return;
124
+ }
125
+
126
+ if (triesLeft === undefined) triesLeft = 20;
127
+ setTimeout(() => {
128
+ this.addSubscribedMediaTrack(
129
+ mediaTrack,
130
+ sid,
131
+ mediaStream,
132
+ receiver,
133
+ adaptiveStreamSettings,
134
+ triesLeft! - 1,
135
+ );
136
+ }, 150);
137
+ return;
138
+ }
139
+
140
+ const isVideo = mediaTrack.kind === 'video';
141
+ let track: RemoteTrack;
142
+ if (isVideo) {
143
+ track = new RemoteVideoTrack(mediaTrack, sid, receiver, adaptiveStreamSettings);
144
+ } else {
145
+ track = new RemoteAudioTrack(mediaTrack, sid, receiver);
146
+ }
147
+
148
+ // set track info
149
+ track.source = publication.source;
150
+ // keep publication's muted status
151
+ track.isMuted = publication.isMuted;
152
+ track.setMediaStream(mediaStream);
153
+ track.start();
154
+
155
+ publication.setTrack(track);
156
+
157
+ this.emit(ParticipantEvent.TrackSubscribed, track, publication);
158
+
159
+ return publication;
160
+ }
161
+
162
+ /** @internal */
163
+ get hasMetadata(): boolean {
164
+ return !!this.participantInfo;
165
+ }
166
+
167
+ getTrackPublication(sid: Track.SID): RemoteTrackPublication | undefined {
168
+ return this.tracks.get(sid);
169
+ }
170
+
171
+ /** @internal */
172
+ updateInfo(info: ParticipantInfo) {
173
+ const alreadyHasMetadata = this.hasMetadata;
174
+
175
+ super.updateInfo(info);
176
+
177
+ // we are getting a list of all available tracks, reconcile in here
178
+ // and send out events for changes
179
+
180
+ // reconcile track publications, publish events only if metadata is already there
181
+ // i.e. changes since the local participant has joined
182
+ const validTracks = new Map<string, RemoteTrackPublication>();
183
+ const newTracks = new Map<string, RemoteTrackPublication>();
184
+
185
+ info.tracks.forEach((ti) => {
186
+ let publication = this.getTrackPublication(ti.sid);
187
+ if (!publication) {
188
+ // new publication
189
+ const kind = Track.kindFromProto(ti.type);
190
+ if (!kind) {
191
+ return;
192
+ }
193
+ publication = new RemoteTrackPublication(kind, ti.sid, ti.name);
194
+ publication.updateInfo(ti);
195
+ newTracks.set(ti.sid, publication);
196
+ this.addTrackPublication(publication);
197
+ } else {
198
+ publication.updateInfo(ti);
199
+ }
200
+ validTracks.set(ti.sid, publication);
201
+ });
202
+
203
+ // send new tracks
204
+ if (alreadyHasMetadata) {
205
+ newTracks.forEach((publication) => {
206
+ this.emit(ParticipantEvent.TrackPublished, publication);
207
+ });
208
+ }
209
+
210
+ // detect removed tracks
211
+ this.tracks.forEach((publication) => {
212
+ if (!validTracks.has(publication.trackSid)) {
213
+ this.unpublishTrack(publication.trackSid, true);
214
+ }
215
+ });
216
+ }
217
+
218
+ /** @internal */
219
+ unpublishTrack(sid: Track.SID, sendUnpublish?: boolean) {
220
+ const publication = <RemoteTrackPublication>this.tracks.get(sid);
221
+ if (!publication) {
222
+ return;
223
+ }
224
+
225
+ this.tracks.delete(sid);
226
+
227
+ // remove from the right type map
228
+ switch (publication.kind) {
229
+ case Track.Kind.Audio:
230
+ this.audioTracks.delete(sid);
231
+ break;
232
+ case Track.Kind.Video:
233
+ this.videoTracks.delete(sid);
234
+ break;
235
+ default:
236
+ break;
237
+ }
238
+
239
+ // also send unsubscribe, if track is actively subscribed
240
+ const { track } = publication;
241
+ if (track) {
242
+ const { isSubscribed } = publication;
243
+ track.stop();
244
+ publication.setTrack(undefined);
245
+ // always send unsubscribed, since apps may rely on this
246
+ if (isSubscribed) {
247
+ this.emit(ParticipantEvent.TrackUnsubscribed, track, publication);
248
+ }
249
+ }
250
+ if (sendUnpublish) {
251
+ this.emit(ParticipantEvent.TrackUnpublished, publication);
252
+ }
253
+ }
254
+
255
+ /** @internal */
256
+ emit<E extends keyof ParticipantEventCallbacks>(
257
+ event: E,
258
+ ...args: Parameters<ParticipantEventCallbacks[E]>
259
+ ): boolean {
260
+ log.trace('participant event', { participant: this.sid, event, args });
261
+ return super.emit(event, ...args);
262
+ }
263
+ }