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,755 @@
1
+ import log from '../../logger';
2
+ import { RoomOptions } from '../../options';
3
+ import { DataPacket, DataPacket_Kind, ParticipantPermission } from '../../proto/livekit_models';
4
+ import {
5
+ AddTrackRequest,
6
+ DataChannelInfo,
7
+ SignalTarget,
8
+ SubscribedQualityUpdate,
9
+ TrackPublishedResponse,
10
+ TrackUnpublishedResponse,
11
+ } from '../../proto/livekit_rtc';
12
+ import { TrackInvalidError, UnexpectedConnectionState } from '../errors';
13
+ import { ParticipantEvent, TrackEvent } from '../events';
14
+ import RTCEngine from '../RTCEngine';
15
+ import LocalAudioTrack from '../track/LocalAudioTrack';
16
+ import LocalTrack from '../track/LocalTrack';
17
+ import LocalTrackPublication from '../track/LocalTrackPublication';
18
+ import LocalVideoTrack, { videoLayersFromEncodings } from '../track/LocalVideoTrack';
19
+ import {
20
+ CreateLocalTracksOptions,
21
+ ScreenShareCaptureOptions,
22
+ ScreenSharePresets,
23
+ TrackPublishOptions,
24
+ VideoCodec,
25
+ } from '../track/options';
26
+ import { Track } from '../track/Track';
27
+ import { constraintsForOptions, mergeDefaultOptions } from '../track/utils';
28
+ import { isFireFox } from '../utils';
29
+ import Participant from './Participant';
30
+ import { ParticipantTrackPermission, trackPermissionToProto } from './ParticipantTrackPermission';
31
+ import { computeVideoEncodings, mediaTrackToLocalTrack } from './publishUtils';
32
+ import RemoteParticipant from './RemoteParticipant';
33
+
34
+ export default class LocalParticipant extends Participant {
35
+ audioTracks: Map<string, LocalTrackPublication>;
36
+
37
+ videoTracks: Map<string, LocalTrackPublication>;
38
+
39
+ /** map of track sid => all published tracks */
40
+ tracks: Map<string, LocalTrackPublication>;
41
+
42
+ private pendingPublishing = new Set<Track.Source>();
43
+
44
+ private cameraError: Error | undefined;
45
+
46
+ private microphoneError: Error | undefined;
47
+
48
+ private engine: RTCEngine;
49
+
50
+ // keep a pointer to room options
51
+ private roomOptions?: RoomOptions;
52
+
53
+ /** @internal */
54
+ constructor(sid: string, identity: string, engine: RTCEngine, options: RoomOptions) {
55
+ super(sid, identity);
56
+ this.audioTracks = new Map();
57
+ this.videoTracks = new Map();
58
+ this.tracks = new Map();
59
+ this.engine = engine;
60
+ this.roomOptions = options;
61
+
62
+ this.engine.client.onRemoteMuteChanged = (trackSid: string, muted: boolean) => {
63
+ const pub = this.tracks.get(trackSid);
64
+ if (!pub || !pub.track) {
65
+ return;
66
+ }
67
+ if (muted) {
68
+ pub.mute();
69
+ } else {
70
+ pub.unmute();
71
+ }
72
+ };
73
+
74
+ this.engine.client.onSubscribedQualityUpdate = this.handleSubscribedQualityUpdate;
75
+
76
+ this.engine.client.onLocalTrackUnpublished = this.handleLocalTrackUnpublished;
77
+ }
78
+
79
+ get lastCameraError(): Error | undefined {
80
+ return this.cameraError;
81
+ }
82
+
83
+ get lastMicrophoneError(): Error | undefined {
84
+ return this.microphoneError;
85
+ }
86
+
87
+ getTrack(source: Track.Source): LocalTrackPublication | undefined {
88
+ const track = super.getTrack(source);
89
+ if (track) {
90
+ return track as LocalTrackPublication;
91
+ }
92
+ }
93
+
94
+ getTrackByName(name: string): LocalTrackPublication | undefined {
95
+ const track = super.getTrackByName(name);
96
+ if (track) {
97
+ return track as LocalTrackPublication;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Enable or disable a participant's camera track.
103
+ *
104
+ * If a track has already published, it'll mute or unmute the track.
105
+ * Resolves with a `LocalTrackPublication` instance if successful and `undefined` otherwise
106
+ */
107
+ setCameraEnabled(enabled: boolean): Promise<LocalTrackPublication | undefined> {
108
+ return this.setTrackEnabled(Track.Source.Camera, enabled);
109
+ }
110
+
111
+ /**
112
+ * Enable or disable a participant's microphone track.
113
+ *
114
+ * If a track has already published, it'll mute or unmute the track.
115
+ * Resolves with a `LocalTrackPublication` instance if successful and `undefined` otherwise
116
+ */
117
+ setMicrophoneEnabled(enabled: boolean): Promise<LocalTrackPublication | undefined> {
118
+ return this.setTrackEnabled(Track.Source.Microphone, enabled);
119
+ }
120
+
121
+ /**
122
+ * Start or stop sharing a participant's screen
123
+ * Resolves with a `LocalTrackPublication` instance if successful and `undefined` otherwise
124
+ */
125
+ setScreenShareEnabled(enabled: boolean): Promise<LocalTrackPublication | undefined> {
126
+ return this.setTrackEnabled(Track.Source.ScreenShare, enabled);
127
+ }
128
+
129
+ /** @internal */
130
+ setPermissions(permissions: ParticipantPermission): boolean {
131
+ const prevPermissions = this.permissions;
132
+ const changed = super.setPermissions(permissions);
133
+ if (changed && prevPermissions) {
134
+ this.emit(ParticipantEvent.ParticipantPermissionsChanged, prevPermissions);
135
+ }
136
+ return changed;
137
+ }
138
+
139
+ /**
140
+ * Enable or disable publishing for a track by source. This serves as a simple
141
+ * way to manage the common tracks (camera, mic, or screen share).
142
+ * Resolves with LocalTrackPublication if successful and void otherwise
143
+ */
144
+ private async setTrackEnabled(
145
+ source: Track.Source,
146
+ enabled: boolean,
147
+ ): Promise<LocalTrackPublication | undefined> {
148
+ log.debug('setTrackEnabled', { source, enabled });
149
+ let track = this.getTrack(source);
150
+ if (enabled) {
151
+ if (track) {
152
+ await track.unmute();
153
+ } else {
154
+ let localTrack: LocalTrack | undefined;
155
+ if (this.pendingPublishing.has(source)) {
156
+ log.info('skipping duplicate published source', { source });
157
+ // no-op it's already been requested
158
+ return;
159
+ }
160
+ this.pendingPublishing.add(source);
161
+ try {
162
+ switch (source) {
163
+ case Track.Source.Camera:
164
+ [localTrack] = await this.createTracks({
165
+ video: true,
166
+ });
167
+ break;
168
+ case Track.Source.Microphone:
169
+ [localTrack] = await this.createTracks({
170
+ audio: true,
171
+ });
172
+ break;
173
+ case Track.Source.ScreenShare:
174
+ [localTrack] = await this.createScreenTracks({ audio: false });
175
+ break;
176
+ default:
177
+ throw new TrackInvalidError(source);
178
+ }
179
+
180
+ track = await this.publishTrack(localTrack);
181
+ } catch (e) {
182
+ if (e instanceof Error && !(e instanceof TrackInvalidError)) {
183
+ this.emit(ParticipantEvent.MediaDevicesError, e);
184
+ }
185
+ throw e;
186
+ } finally {
187
+ this.pendingPublishing.delete(source);
188
+ }
189
+ }
190
+ } else if (track && track.track) {
191
+ // screenshare cannot be muted, unpublish instead
192
+ if (source === Track.Source.ScreenShare) {
193
+ track = this.unpublishTrack(track.track);
194
+ } else {
195
+ await track.mute();
196
+ }
197
+ }
198
+ return track;
199
+ }
200
+
201
+ /**
202
+ * Publish both camera and microphone at the same time. This is useful for
203
+ * displaying a single Permission Dialog box to the end user.
204
+ */
205
+ async enableCameraAndMicrophone() {
206
+ if (
207
+ this.pendingPublishing.has(Track.Source.Camera) ||
208
+ this.pendingPublishing.has(Track.Source.Microphone)
209
+ ) {
210
+ // no-op it's already been requested
211
+ return;
212
+ }
213
+
214
+ this.pendingPublishing.add(Track.Source.Camera);
215
+ this.pendingPublishing.add(Track.Source.Microphone);
216
+ try {
217
+ const tracks: LocalTrack[] = await this.createTracks({
218
+ audio: true,
219
+ video: true,
220
+ });
221
+
222
+ await Promise.all(tracks.map((track) => this.publishTrack(track)));
223
+ } finally {
224
+ this.pendingPublishing.delete(Track.Source.Camera);
225
+ this.pendingPublishing.delete(Track.Source.Microphone);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Create local camera and/or microphone tracks
231
+ * @param options
232
+ * @returns
233
+ */
234
+ async createTracks(options?: CreateLocalTracksOptions): Promise<LocalTrack[]> {
235
+ const opts = mergeDefaultOptions(
236
+ options,
237
+ this.roomOptions?.audioCaptureDefaults,
238
+ this.roomOptions?.videoCaptureDefaults,
239
+ );
240
+
241
+ const constraints = constraintsForOptions(opts);
242
+ let stream: MediaStream | undefined;
243
+ try {
244
+ stream = await navigator.mediaDevices.getUserMedia(constraints);
245
+ } catch (err) {
246
+ if (err instanceof Error) {
247
+ if (constraints.audio) {
248
+ this.microphoneError = err;
249
+ }
250
+ if (constraints.video) {
251
+ this.cameraError = err;
252
+ }
253
+ }
254
+
255
+ throw err;
256
+ }
257
+
258
+ if (constraints.audio) {
259
+ this.microphoneError = undefined;
260
+ }
261
+ if (constraints.video) {
262
+ this.cameraError = undefined;
263
+ }
264
+
265
+ return stream.getTracks().map((mediaStreamTrack) => {
266
+ const isAudio = mediaStreamTrack.kind === 'audio';
267
+ let trackOptions = isAudio ? options!.audio : options!.video;
268
+ if (typeof trackOptions === 'boolean' || !trackOptions) {
269
+ trackOptions = {};
270
+ }
271
+ let trackConstraints: MediaTrackConstraints | undefined;
272
+ const conOrBool = isAudio ? constraints.audio : constraints.video;
273
+ if (typeof conOrBool !== 'boolean') {
274
+ trackConstraints = conOrBool;
275
+ }
276
+ const track = mediaTrackToLocalTrack(mediaStreamTrack, trackConstraints);
277
+ if (track.kind === Track.Kind.Video) {
278
+ track.source = Track.Source.Camera;
279
+ } else if (track.kind === Track.Kind.Audio) {
280
+ track.source = Track.Source.Microphone;
281
+ }
282
+ track.mediaStream = stream;
283
+ return track;
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Creates a screen capture tracks with getDisplayMedia().
289
+ * A LocalVideoTrack is always created and returned.
290
+ * If { audio: true }, and the browser supports audio capture, a LocalAudioTrack is also created.
291
+ */
292
+ async createScreenTracks(options?: ScreenShareCaptureOptions): Promise<Array<LocalTrack>> {
293
+ if (options === undefined) {
294
+ options = {};
295
+ }
296
+ if (options.resolution === undefined) {
297
+ options.resolution = ScreenSharePresets.h1080fps15.resolution;
298
+ }
299
+
300
+ let videoConstraints: MediaTrackConstraints | boolean = true;
301
+ if (options.resolution) {
302
+ videoConstraints = {
303
+ width: options.resolution.width,
304
+ height: options.resolution.height,
305
+ frameRate: options.resolution.frameRate,
306
+ };
307
+ }
308
+ // typescript definition is missing getDisplayMedia: https://github.com/microsoft/TypeScript/issues/33232
309
+ // @ts-ignore
310
+ const stream: MediaStream = await navigator.mediaDevices.getDisplayMedia({
311
+ audio: options.audio ?? false,
312
+ video: videoConstraints,
313
+ });
314
+
315
+ const tracks = stream.getVideoTracks();
316
+ if (tracks.length === 0) {
317
+ throw new TrackInvalidError('no video track found');
318
+ }
319
+ const screenVideo = new LocalVideoTrack(tracks[0]);
320
+ screenVideo.source = Track.Source.ScreenShare;
321
+ const localTracks: Array<LocalTrack> = [screenVideo];
322
+ if (stream.getAudioTracks().length > 0) {
323
+ const screenAudio = new LocalAudioTrack(stream.getAudioTracks()[0]);
324
+ screenAudio.source = Track.Source.ScreenShareAudio;
325
+ localTracks.push(screenAudio);
326
+ }
327
+ return localTracks;
328
+ }
329
+
330
+ /**
331
+ * Publish a new track to the room
332
+ * @param track
333
+ * @param options
334
+ */
335
+ async publishTrack(
336
+ track: LocalTrack | MediaStreamTrack,
337
+ options?: TrackPublishOptions,
338
+ ): Promise<LocalTrackPublication> {
339
+ const opts: TrackPublishOptions = {
340
+ ...this.roomOptions?.publishDefaults,
341
+ ...options,
342
+ };
343
+
344
+ // convert raw media track into audio or video track
345
+ if (track instanceof MediaStreamTrack) {
346
+ switch (track.kind) {
347
+ case 'audio':
348
+ track = new LocalAudioTrack(track);
349
+ break;
350
+ case 'video':
351
+ track = new LocalVideoTrack(track);
352
+ break;
353
+ default:
354
+ throw new TrackInvalidError(`unsupported MediaStreamTrack kind ${track.kind}`);
355
+ }
356
+ }
357
+
358
+ // is it already published? if so skip
359
+ let existingPublication: LocalTrackPublication | undefined;
360
+ this.tracks.forEach((publication) => {
361
+ if (!publication.track) {
362
+ return;
363
+ }
364
+ if (publication.track === track) {
365
+ existingPublication = <LocalTrackPublication>publication;
366
+ }
367
+ });
368
+
369
+ if (existingPublication) return existingPublication;
370
+
371
+ if (opts.source) {
372
+ track.source = opts.source;
373
+ }
374
+ if (opts.stopMicTrackOnMute && track instanceof LocalAudioTrack) {
375
+ track.stopOnMute = true;
376
+ }
377
+
378
+ if (track.source === Track.Source.ScreenShare && isFireFox()) {
379
+ // Firefox does not work well with simulcasted screen share
380
+ // we frequently get no data on layer 0 when enabled
381
+ opts.simulcast = false;
382
+ }
383
+
384
+ // handle track actions
385
+ track.on(TrackEvent.Muted, this.onTrackMuted);
386
+ track.on(TrackEvent.Unmuted, this.onTrackUnmuted);
387
+ track.on(TrackEvent.Ended, this.onTrackUnpublish);
388
+ track.on(TrackEvent.UpstreamPaused, this.onTrackUpstreamPaused);
389
+ track.on(TrackEvent.UpstreamResumed, this.onTrackUpstreamResumed);
390
+
391
+ // create track publication from track
392
+ const req = AddTrackRequest.fromPartial({
393
+ // get local track id for use during publishing
394
+ cid: track.mediaStreamTrack.id,
395
+ name: options?.name,
396
+ type: Track.kindToProto(track.kind),
397
+ muted: track.isMuted,
398
+ source: Track.sourceToProto(track.source),
399
+ disableDtx: !(opts?.dtx ?? true),
400
+ });
401
+
402
+ // compute encodings and layers for video
403
+ let encodings: RTCRtpEncodingParameters[] | undefined;
404
+ if (track.kind === Track.Kind.Video) {
405
+ // TODO: support react native, which doesn't expose getSettings
406
+ const settings = track.mediaStreamTrack.getSettings();
407
+ const width = settings.width ?? track.dimensions?.width;
408
+ const height = settings.height ?? track.dimensions?.height;
409
+ // width and height should be defined for video
410
+ req.width = width ?? 0;
411
+ req.height = height ?? 0;
412
+ encodings = computeVideoEncodings(
413
+ track.source === Track.Source.ScreenShare,
414
+ width,
415
+ height,
416
+ opts,
417
+ );
418
+ req.layers = videoLayersFromEncodings(req.width, req.height, encodings);
419
+ } else if (track.kind === Track.Kind.Audio && opts.audioBitrate) {
420
+ encodings = [
421
+ {
422
+ maxBitrate: opts.audioBitrate,
423
+ },
424
+ ];
425
+ }
426
+
427
+ const ti = await this.engine.addTrack(req);
428
+ const publication = new LocalTrackPublication(track.kind, ti, track);
429
+ track.sid = ti.sid;
430
+
431
+ if (!this.engine.publisher) {
432
+ throw new UnexpectedConnectionState('publisher is closed');
433
+ }
434
+ log.debug(`publishing ${track.kind} with encodings`, { encodings, trackInfo: ti });
435
+ const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
436
+ if (encodings) {
437
+ transceiverInit.sendEncodings = encodings;
438
+ }
439
+ const transceiver = this.engine.publisher.pc.addTransceiver(
440
+ track.mediaStreamTrack,
441
+ transceiverInit,
442
+ );
443
+ this.engine.negotiate();
444
+
445
+ // store RTPSender
446
+ track.sender = transceiver.sender;
447
+ if (track instanceof LocalVideoTrack) {
448
+ track.startMonitor(this.engine.client);
449
+ } else if (track instanceof LocalAudioTrack) {
450
+ track.startMonitor();
451
+ }
452
+
453
+ if (opts.videoCodec) {
454
+ this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
455
+ }
456
+ this.addTrackPublication(publication);
457
+
458
+ // send event for publication
459
+ this.emit(ParticipantEvent.LocalTrackPublished, publication);
460
+ return publication;
461
+ }
462
+
463
+ unpublishTrack(
464
+ track: LocalTrack | MediaStreamTrack,
465
+ stopOnUnpublish?: boolean,
466
+ ): LocalTrackPublication | undefined {
467
+ // look through all published tracks to find the right ones
468
+ const publication = this.getPublicationForTrack(track);
469
+
470
+ log.debug('unpublishing track', { track, method: 'unpublishTrack' });
471
+
472
+ if (!publication || !publication.track) {
473
+ log.warn('track was not unpublished because no publication was found', {
474
+ track,
475
+ method: 'unpublishTrack',
476
+ });
477
+ return undefined;
478
+ }
479
+
480
+ track = publication.track;
481
+
482
+ track.off(TrackEvent.Muted, this.onTrackMuted);
483
+ track.off(TrackEvent.Unmuted, this.onTrackUnmuted);
484
+ track.off(TrackEvent.Ended, this.onTrackUnpublish);
485
+ track.off(TrackEvent.UpstreamPaused, this.onTrackUpstreamPaused);
486
+ track.off(TrackEvent.UpstreamResumed, this.onTrackUpstreamResumed);
487
+
488
+ if (stopOnUnpublish === undefined) {
489
+ stopOnUnpublish = this.roomOptions?.stopLocalTrackOnUnpublish ?? true;
490
+ }
491
+ if (stopOnUnpublish) {
492
+ track.stop();
493
+ }
494
+
495
+ const { mediaStreamTrack } = track;
496
+
497
+ if (this.engine.publisher) {
498
+ const senders = this.engine.publisher.pc.getSenders();
499
+ senders.forEach((sender) => {
500
+ if (sender.track === mediaStreamTrack) {
501
+ try {
502
+ this.engine.publisher?.pc.removeTrack(sender);
503
+ this.engine.negotiate();
504
+ } catch (e) {
505
+ log.warn('failed to remove track', { error: e, method: 'unpublishTrack' });
506
+ }
507
+ }
508
+ });
509
+ }
510
+
511
+ // remove from our maps
512
+ this.tracks.delete(publication.trackSid);
513
+ switch (publication.kind) {
514
+ case Track.Kind.Audio:
515
+ this.audioTracks.delete(publication.trackSid);
516
+ break;
517
+ case Track.Kind.Video:
518
+ this.videoTracks.delete(publication.trackSid);
519
+ break;
520
+ default:
521
+ break;
522
+ }
523
+
524
+ this.emit(ParticipantEvent.LocalTrackUnpublished, publication);
525
+ publication.setTrack(undefined);
526
+
527
+ return publication;
528
+ }
529
+
530
+ unpublishTracks(tracks: LocalTrack[] | MediaStreamTrack[]): LocalTrackPublication[] {
531
+ const publications: LocalTrackPublication[] = [];
532
+ tracks.forEach((track: LocalTrack | MediaStreamTrack) => {
533
+ const pub = this.unpublishTrack(track);
534
+ if (pub) {
535
+ publications.push(pub);
536
+ }
537
+ });
538
+ return publications;
539
+ }
540
+
541
+ /**
542
+ * Publish a new data payload to the room. Data will be forwarded to each
543
+ * participant in the room if the destination argument is empty
544
+ *
545
+ * @param data Uint8Array of the payload. To send string data, use TextEncoder.encode
546
+ * @param kind whether to send this as reliable or lossy.
547
+ * For data that you need delivery guarantee (such as chat messages), use Reliable.
548
+ * For data that should arrive as quickly as possible, but you are ok with dropped
549
+ * packets, use Lossy.
550
+ * @param destination the participants who will receive the message
551
+ */
552
+ async publishData(
553
+ data: Uint8Array,
554
+ kind: DataPacket_Kind,
555
+ destination?: RemoteParticipant[] | string[],
556
+ ) {
557
+ const dest: string[] = [];
558
+ if (destination !== undefined) {
559
+ destination.forEach((val: any) => {
560
+ if (val instanceof RemoteParticipant) {
561
+ dest.push(val.sid);
562
+ } else {
563
+ dest.push(val);
564
+ }
565
+ });
566
+ }
567
+
568
+ const packet: DataPacket = {
569
+ kind,
570
+ user: {
571
+ participantSid: this.sid,
572
+ payload: data,
573
+ destinationSids: dest,
574
+ },
575
+ };
576
+
577
+ await this.engine.sendDataPacket(packet, kind);
578
+ }
579
+
580
+ /**
581
+ * Control who can subscribe to LocalParticipant's published tracks.
582
+ *
583
+ * By default, all participants can subscribe. This allows fine-grained control over
584
+ * who is able to subscribe at a participant and track level.
585
+ *
586
+ * Note: if access is given at a track-level (i.e. both [allParticipantsAllowed] and
587
+ * [ParticipantTrackPermission.allTracksAllowed] are false), any newer published tracks
588
+ * will not grant permissions to any participants and will require a subsequent
589
+ * permissions update to allow subscription.
590
+ *
591
+ * @param allParticipantsAllowed Allows all participants to subscribe all tracks.
592
+ * Takes precedence over [[participantTrackPermissions]] if set to true.
593
+ * By default this is set to true.
594
+ * @param participantTrackPermissions Full list of individual permissions per
595
+ * participant/track. Any omitted participants will not receive any permissions.
596
+ */
597
+ setTrackSubscriptionPermissions(
598
+ allParticipantsAllowed: boolean,
599
+ participantTrackPermissions: ParticipantTrackPermission[] = [],
600
+ ) {
601
+ this.engine.client.sendUpdateSubscriptionPermissions(
602
+ allParticipantsAllowed,
603
+ participantTrackPermissions.map((p) => trackPermissionToProto(p)),
604
+ );
605
+ }
606
+
607
+ /** @internal */
608
+ private onTrackUnmuted = (track: LocalTrack) => {
609
+ this.onTrackMuted(track, track.isUpstreamPaused);
610
+ };
611
+
612
+ // when the local track changes in mute status, we'll notify server as such
613
+ /** @internal */
614
+ private onTrackMuted = (track: LocalTrack, muted?: boolean) => {
615
+ if (muted === undefined) {
616
+ muted = true;
617
+ }
618
+
619
+ if (!track.sid) {
620
+ log.error('could not update mute status for unpublished track', track);
621
+ return;
622
+ }
623
+
624
+ this.engine.updateMuteStatus(track.sid, muted);
625
+ };
626
+
627
+ private onTrackUpstreamPaused = (track: LocalTrack) => {
628
+ log.debug('upstream paused');
629
+ this.onTrackMuted(track, true);
630
+ };
631
+
632
+ private onTrackUpstreamResumed = (track: LocalTrack) => {
633
+ log.debug('upstream resumed');
634
+ this.onTrackMuted(track, track.isMuted);
635
+ };
636
+
637
+ private handleSubscribedQualityUpdate = (update: SubscribedQualityUpdate) => {
638
+ if (!this.roomOptions?.dynacast) {
639
+ return;
640
+ }
641
+ const pub = this.videoTracks.get(update.trackSid);
642
+ if (!pub) {
643
+ log.warn('received subscribed quality update for unknown track', {
644
+ method: 'handleSubscribedQualityUpdate',
645
+ sid: update.trackSid,
646
+ });
647
+ return;
648
+ }
649
+ pub.videoTrack?.setPublishingLayers(update.subscribedQualities);
650
+ };
651
+
652
+ private handleLocalTrackUnpublished = (unpublished: TrackUnpublishedResponse) => {
653
+ const track = this.tracks.get(unpublished.trackSid);
654
+ if (!track) {
655
+ log.warn('received unpublished event for unknown track', {
656
+ method: 'handleLocalTrackUnpublished',
657
+ trackSid: unpublished.trackSid,
658
+ });
659
+ return;
660
+ }
661
+ this.unpublishTrack(track.track!);
662
+ };
663
+
664
+ private onTrackUnpublish = (track: LocalTrack) => {
665
+ this.unpublishTrack(track);
666
+ };
667
+
668
+ private getPublicationForTrack(
669
+ track: LocalTrack | MediaStreamTrack,
670
+ ): LocalTrackPublication | undefined {
671
+ let publication: LocalTrackPublication | undefined;
672
+ this.tracks.forEach((pub) => {
673
+ const localTrack = pub.track;
674
+ if (!localTrack) {
675
+ return;
676
+ }
677
+
678
+ // this looks overly complicated due to this object tree
679
+ if (track instanceof MediaStreamTrack) {
680
+ if (localTrack instanceof LocalAudioTrack || localTrack instanceof LocalVideoTrack) {
681
+ if (localTrack.mediaStreamTrack === track) {
682
+ publication = <LocalTrackPublication>pub;
683
+ }
684
+ }
685
+ } else if (track === localTrack) {
686
+ publication = <LocalTrackPublication>pub;
687
+ }
688
+ });
689
+ return publication;
690
+ }
691
+
692
+ private setPreferredCodec(
693
+ transceiver: RTCRtpTransceiver,
694
+ kind: Track.Kind,
695
+ videoCodec: VideoCodec,
696
+ ) {
697
+ if (!('getCapabilities' in RTCRtpSender)) {
698
+ return;
699
+ }
700
+ const cap = RTCRtpSender.getCapabilities(kind);
701
+ if (!cap) return;
702
+ const selected = cap.codecs.find((c) => {
703
+ const codec = c.mimeType.toLowerCase();
704
+ const matchesVideoCodec = codec === `video/${videoCodec}`;
705
+
706
+ // for h264 codecs that have sdpFmtpLine available, use only if the
707
+ // profile-level-id is 42e01f for cross-browser compatibility
708
+ if (videoCodec === 'h264' && c.sdpFmtpLine) {
709
+ return matchesVideoCodec && c.sdpFmtpLine.includes('profile-level-id=42e01f');
710
+ }
711
+
712
+ return matchesVideoCodec || codec === 'audio/opus';
713
+ });
714
+ if (selected && 'setCodecPreferences' in transceiver) {
715
+ // @ts-ignore
716
+ transceiver.setCodecPreferences([selected]);
717
+ }
718
+ }
719
+
720
+ /** @internal */
721
+ publishedTracksInfo(): TrackPublishedResponse[] {
722
+ const infos: TrackPublishedResponse[] = [];
723
+ this.tracks.forEach((track: LocalTrackPublication) => {
724
+ if (track.track !== undefined) {
725
+ infos.push({
726
+ cid: track.track.mediaStreamTrack.id,
727
+ track: track.trackInfo,
728
+ });
729
+ }
730
+ });
731
+ return infos;
732
+ }
733
+
734
+ /** @internal */
735
+ dataChannelsInfo(): DataChannelInfo[] {
736
+ const infos: DataChannelInfo[] = [];
737
+ const getInfo = (dc: RTCDataChannel | undefined, target: SignalTarget) => {
738
+ if (dc?.id !== undefined && dc.id !== null) {
739
+ infos.push({
740
+ label: dc.label,
741
+ id: dc.id,
742
+ target,
743
+ });
744
+ }
745
+ };
746
+ getInfo(this.engine.dataChannelForKind(DataPacket_Kind.LOSSY), SignalTarget.PUBLISHER);
747
+ getInfo(this.engine.dataChannelForKind(DataPacket_Kind.RELIABLE), SignalTarget.PUBLISHER);
748
+ getInfo(this.engine.dataChannelForKind(DataPacket_Kind.LOSSY, true), SignalTarget.SUBSCRIBER);
749
+ getInfo(
750
+ this.engine.dataChannelForKind(DataPacket_Kind.RELIABLE, true),
751
+ SignalTarget.SUBSCRIBER,
752
+ );
753
+ return infos;
754
+ }
755
+ }