@livedigital/client 1.9.0 → 1.10.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.
@@ -1,5 +1,5 @@
1
1
  import { MediaKind } from 'mediasoup-client/lib/RtpParameters';
2
- import { ProducerData } from '../types/common';
2
+ import { ProducerData, TrackLabel } from '../types/common';
3
3
 
4
4
  class PeerProducer {
5
5
  readonly id: string;
@@ -8,13 +8,19 @@ class PeerProducer {
8
8
 
9
9
  readonly peerId: string;
10
10
 
11
+ readonly label: TrackLabel;
12
+
11
13
  public score = 10;
12
14
 
13
15
  constructor(params: ProducerData) {
14
- const { id, kind, peerId } = params;
16
+ const {
17
+ id, kind, peerId, label,
18
+ } = params;
19
+
15
20
  this.id = id;
16
21
  this.kind = kind;
17
22
  this.peerId = peerId;
23
+ this.label = label;
18
24
  }
19
25
  }
20
26
 
@@ -1,12 +1,18 @@
1
- import { RtpCapabilities, RtpEncodingParameters } from 'mediasoup-client/lib/RtpParameters';
1
+ import { RtpCapabilities } from 'mediasoup-client/lib/RtpParameters';
2
+ import { VIDEO_CONSTRAINS } from 'constants/videoConstrains';
2
3
  import {
3
4
  PeerResponse,
4
5
  JoinChannelParams,
5
6
  SocketResponse,
6
- PublishAudioOptions,
7
- PublishVideoOptions,
7
+ CreateCameraVideoTrackOptions,
8
+ CreateMicrophoneAudioTrackOptions,
9
+ CreateScreenVideoTrackOptions,
10
+ CreateScreenAudioTrackOptions,
11
+ Track,
12
+ TrackLabel,
13
+ EndTrackPayload,
14
+ StartTrackPayload,
8
15
  } from '../types/common';
9
- import { WEBCAM_SIMULCAST_ENCODINGS } from '../constants/simulcastEncodings';
10
16
  import EnhancedEventEmitter from '../EnhancedEventEmitter';
11
17
  import System from './system';
12
18
  import Peer from './Peer';
@@ -84,8 +90,7 @@ class Engine {
84
90
 
85
91
  this.network.socket.disconnect();
86
92
  this.peersRepository.clear();
87
- await this.unpublishAudio();
88
- await this.unpublishVideo();
93
+ await this.unpublish();
89
94
  await this.network.closeSendTransport();
90
95
  await this.network.closeReceiveTransport();
91
96
 
@@ -154,253 +159,217 @@ class Engine {
154
159
  this.peersRepository.delete(peerId);
155
160
  }
156
161
 
157
- public async publishVideo(track?: MediaStreamTrack, options?: PublishVideoOptions): Promise<void> {
158
- if (!track) {
159
- if (this.system.isEnableVideoDevicesLock) {
160
- this.logger.warn('publishVideo()', { message: 'Video devices is locked' });
161
- return;
162
- }
163
-
164
- if (!this.initialized) {
165
- throw new Error('First you need to connect');
166
- }
162
+ public async pause(track: Track): Promise<void> {
163
+ const producer = track.getProducer();
164
+ if (!producer) {
165
+ this.logger.warn('pause()', { message: 'Track is not published', kind: track.kind });
166
+ return;
167
+ }
167
168
 
168
- if (!this.media.mediasoupDevice.canProduce('video')) {
169
- throw new Error('Cant produce video');
170
- }
169
+ try {
170
+ await this.pauseRemoteProducer(producer.id);
171
+ producer.pause();
172
+ this.logger.debug('pause()', { kind: track.kind });
173
+ } catch (error) {
174
+ this.logger.error('pause()', { error, kind: track.kind });
175
+ throw new Error('Can`t pause track');
176
+ }
177
+ }
171
178
 
172
- if (!this.system.availableVideoDevices.length) {
173
- throw new Error('No available media devices');
174
- }
179
+ public async resume(track: Track): Promise<void> {
180
+ const producer = track.getProducer();
181
+ if (!producer) {
182
+ this.logger.warn('resume()', { message: 'Track is not published' });
183
+ return;
184
+ }
175
185
 
176
- await this.system.startVideoStream(options?.videoTrackConstraints);
177
- this.system.setIsEnableVideoDevicesLock(true);
186
+ try {
187
+ await this.resumeRemoteProducer(producer.id);
188
+ producer.resume();
189
+ this.logger.debug('resume()', { kind: track.kind });
190
+ } catch (error) {
191
+ this.logger.error('resume()', { error, kind: track.kind });
192
+ throw new Error('Can`t resume track');
178
193
  }
194
+ }
179
195
 
196
+ async createCameraVideoTrack(options?: CreateCameraVideoTrackOptions): Promise<Track | null> {
180
197
  try {
181
- if (this.media.videoProducer) {
182
- this.media.closeVideoProducer();
183
- await this.network.closeRemoteProducer(this.media.videoProducer.id);
184
- }
198
+ const track = await this.media.createUserMediaTrack({
199
+ audio: false,
200
+ video: {
201
+ deviceId: options?.deviceId,
202
+ frameRate: options?.frameRate || 15,
203
+ width: options?.width || VIDEO_CONSTRAINS.hd.width,
204
+ height: options?.height || VIDEO_CONSTRAINS.hd.height,
205
+ },
206
+ });
185
207
 
186
- const trackToProduce = track || this.system.videoStream?.getVideoTracks().pop();
187
- if (!trackToProduce) {
188
- this.logger.warn('publishVideo()', { message: 'No media tracks to publish' });
189
- return;
208
+ track.setLabel(TrackLabel.Camera);
209
+ if (options?.encoderConfig) {
210
+ track.setEncoderConfig(options.encoderConfig);
190
211
  }
191
212
 
192
- const codec = this.media.getCodec();
193
- const encodings = options?.encodings || WEBCAM_SIMULCAST_ENCODINGS;
194
- const codecOptions = options?.codecOptions || {
195
- videoGoogleStartBitrate: 1000,
196
- };
213
+ this.logger.debug('createCameraVideoTrack()', { options, track });
214
+ return track;
215
+ } catch (error) {
216
+ this.logger.error('createCameraVideoTrack()', { error, options });
217
+ return null;
218
+ }
219
+ }
197
220
 
198
- const videoProducer = await this.network.sendTransport?.produce({
199
- track: trackToProduce,
200
- encodings,
201
- codecOptions,
202
- codec,
203
- stopTracks: true,
204
- disableTrackOnPause: false,
205
- zeroRtpOnPause: true,
206
- appData: {
207
- peerId: this.mySocketId,
221
+ async createMicrophoneAudioTrack(options?: CreateMicrophoneAudioTrackOptions): Promise<Track | null> {
222
+ try {
223
+ const track = await this.media.createUserMediaTrack({
224
+ video: false,
225
+ audio: {
226
+ deviceId: options?.deviceId,
208
227
  },
209
228
  });
210
229
 
211
- if (!videoProducer) {
212
- return;
230
+ track.setLabel(TrackLabel.Microphone);
231
+ if (options?.encoderConfig) {
232
+ track.setEncoderConfig(options.encoderConfig);
213
233
  }
214
234
 
215
- this.media.setVideoProducer(videoProducer);
216
- const myPeer = this.peersRepository.get(<string> this.mySocketId);
217
- myPeer?.observer.safeEmit(PEER_EVENTS.trackStart, { kind: videoProducer.kind, track: trackToProduce });
218
- this.logger.debug('publishVideo()', { codec });
235
+ this.logger.debug('createMicrophoneAudioTrack()', { options, track });
236
+ return track;
219
237
  } catch (error) {
220
- this.logger.error('publishVideo()', { error });
221
- throw new Error('Enable video error');
222
- } finally {
223
- this.system.setIsEnableVideoDevicesLock(false);
238
+ this.logger.error('createMicrophoneAudioTrack()', { error, options });
239
+ return null;
224
240
  }
225
241
  }
226
242
 
227
- public async publishAudio(track?: MediaStreamTrack, options?: PublishAudioOptions): Promise<void> {
228
- if (!track) {
229
- if (this.system.isEnableAudioDevicesLock) {
230
- this.logger.warn('publishAudio()', { message: 'Audio devices is locked' });
231
- return;
232
- }
233
-
234
- if (!this.initialized) {
235
- throw new Error('First you need to connect');
236
- }
243
+ async createScreenVideoTrack(options?: CreateScreenVideoTrackOptions): Promise<Track | null> {
244
+ try {
245
+ const track = await this.media.createDisplayMediaTrack({
246
+ audio: false,
247
+ video: {
248
+ frameRate: options?.frameRate || 30,
249
+ width: options?.width || VIDEO_CONSTRAINS.fullhd.width,
250
+ height: options?.height || VIDEO_CONSTRAINS.fullhd.height,
251
+ },
252
+ });
237
253
 
238
- if (!this.system.availableAudioDevices.length) {
239
- throw new Error('No available media devices');
240
- }
254
+ track.mediaStreamTrack.addEventListener('ended', () => {
255
+ this.unpublish(track);
256
+ });
241
257
 
242
- if (!this.media.mediasoupDevice.canProduce('audio')) {
243
- throw new Error('Cant produce audio');
258
+ track.setLabel(TrackLabel.ScreenVideo);
259
+ if (options?.encoderConfig) {
260
+ track.setEncoderConfig(options.encoderConfig);
244
261
  }
245
262
 
246
- await this.system.startAudioStream(options?.audioTrackConstraints);
247
- this.system.setIsEnableAudioDevicesLock(true);
263
+ this.logger.debug('createScreenVideoTrack()', { options, track });
264
+ return track;
265
+ } catch (error) {
266
+ this.logger.error('createScreenVideoTrack()', { error, options });
267
+ return null;
248
268
  }
269
+ }
249
270
 
250
- const codecOptions = options?.codecOptions || {
251
- opusFec: true,
252
- };
253
-
271
+ async createScreenAudioTrack(options?: CreateScreenAudioTrackOptions): Promise<Track | null> {
254
272
  try {
255
- if (this.media.audioProducer) {
256
- this.media.closeAudioProducer();
257
- await this.network.closeRemoteProducer(this.media.audioProducer.id);
258
- }
273
+ const track = await this.media.createDisplayMediaTrack({
274
+ video: false,
275
+ audio: true,
276
+ });
259
277
 
260
- const trackToProduce = track || this.system.audioStream?.getAudioTracks().pop();
261
- if (!trackToProduce) {
262
- this.logger.warn('publishAudio()', { message: 'No media tracks to publish' });
263
- return;
278
+ track.mediaStreamTrack.addEventListener('ended', () => {
279
+ this.unpublish(track);
280
+ });
281
+
282
+ track.setLabel(TrackLabel.ScreenAudio);
283
+ if (options?.encoderConfig) {
284
+ track.setEncoderConfig(options.encoderConfig);
264
285
  }
265
286
 
266
- const audioProducer = await this.network.sendTransport?.produce({
267
- track: trackToProduce,
287
+ this.logger.debug('createScreenAudioTrack()', { options, track });
288
+ return track;
289
+ } catch (error) {
290
+ this.logger.error('createScreenAudioTrack()', { error, options });
291
+ return null;
292
+ }
293
+ }
294
+
295
+ public async publish(tracks: Track | Track[]): Promise<void> {
296
+ const tracksToPublish = Array.isArray(tracks) ? tracks : [tracks];
297
+ const actions = tracksToPublish.map(async (track) => {
298
+ const encodings = 'getEncodings' in track
299
+ ? track.getEncodings()
300
+ : undefined;
301
+
302
+ const codecOptions = 'getCodecOptions' in track
303
+ ? track.getCodecOptions()
304
+ : undefined;
305
+
306
+ const producer = await this.network.sendTransport?.produce({
307
+ track: track.mediaStreamTrack,
308
+ encodings,
268
309
  codecOptions,
310
+ codec: this.media.getTrackCodec(track),
269
311
  stopTracks: true,
270
- disableTrackOnPause: false,
312
+ disableTrackOnPause: true,
271
313
  zeroRtpOnPause: true,
272
314
  appData: {
273
315
  peerId: this.mySocketId,
316
+ label: track.getLabel(),
274
317
  },
275
318
  });
276
319
 
277
- if (!audioProducer) {
320
+ if (!producer) {
278
321
  return;
279
322
  }
280
323
 
281
- this.media.setAudioProducer(audioProducer);
324
+ track.setProducer(producer);
282
325
  const myPeer = this.peersRepository.get(<string> this.mySocketId);
283
- myPeer?.observer.safeEmit(PEER_EVENTS.trackStart, { kind: 'audio', track: trackToProduce });
284
- this.logger.debug('publishAudio()');
285
- } catch (error) {
286
- this.logger.error('publishAudio()', { error });
287
- throw new Error('Enable audio error');
288
- } finally {
289
- this.system.setIsEnableAudioDevicesLock(false);
290
- }
291
- }
326
+ myPeer?.observer.safeEmit(PEER_EVENTS.trackStart, {
327
+ producerId: producer.id,
328
+ track: track.mediaStreamTrack,
329
+ label: track.getLabel(),
330
+ } as StartTrackPayload);
292
331
 
293
- public async unpublishVideo(): Promise<void> {
294
- if (!this.media.videoProducer) {
295
- this.logger.warn('unpublishVideo()', { message: 'No published video' });
296
- return;
297
- }
332
+ this.logger.debug('publish()', { track });
333
+ });
298
334
 
299
335
  try {
300
- if (this.network.socket.connection?.connected) {
301
- await this.network.closeRemoteProducer(this.media.videoProducer.id);
302
- }
303
-
304
- this.media.closeVideoProducer();
305
- this.system.stopVideoStream();
306
- const myPeer = this.peersRepository.get(<string> this.mySocketId);
307
- if (myPeer) {
308
- myPeer.observer.safeEmit(PEER_EVENTS.trackEnd, { kind: 'video' });
309
- }
310
-
311
- this.logger.debug('unpublishVideo()');
336
+ await Promise.all(actions);
312
337
  } catch (error) {
313
- this.logger.error('unpublishVideo()', { error });
314
- throw new Error('Error unpublish video');
338
+ this.logger.error('publish()', { error });
315
339
  }
316
340
  }
317
341
 
318
- public async unpublishAudio(): Promise<void> {
319
- if (!this.media.audioProducer) {
320
- this.logger.warn('unpublishAudio()', { message: 'No published audio' });
321
- return;
342
+ public async unpublish(tracks?: Track | Track[]): Promise<void> {
343
+ let tracksToUnpublish = [];
344
+ if (!tracks) {
345
+ tracksToUnpublish = this.media.getAllTracks();
346
+ } else {
347
+ tracksToUnpublish = Array.isArray(tracks) ? tracks : [tracks];
322
348
  }
323
349
 
324
- try {
325
- if (this.network.socket.connection?.connected) {
326
- await this.network.closeRemoteProducer(this.media.audioProducer.id);
350
+ const actions = tracksToUnpublish.map(async (track) => {
351
+ const producer = track.getProducer();
352
+ if (producer && this.network.socket.connection?.connected) {
353
+ await this.network.closeRemoteProducer(producer.id);
327
354
  }
328
355
 
329
- this.media.closeAudioProducer();
330
- this.system.stopAudioStream();
331
356
  const myPeer = this.peersRepository.get(<string> this.mySocketId);
332
357
  if (myPeer) {
333
- myPeer.observer.safeEmit(PEER_EVENTS.trackEnd, { kind: 'audio' });
358
+ myPeer.observer.safeEmit(PEER_EVENTS.trackEnd, {
359
+ producerId: track.getProducer()?.id,
360
+ kind: track.getProducer()?.kind,
361
+ label: track.getLabel(),
362
+ } as EndTrackPayload);
334
363
  }
335
364
 
336
- this.logger.debug('unpublishAudio()');
337
- } catch (error) {
338
- this.logger.error('unpublishAudio()', { error });
339
- throw new Error('Error unpublish audio');
340
- }
341
- }
342
-
343
- public async pauseVideo(): Promise<void> {
344
- if (!this.media.videoProducer) {
345
- this.logger.warn('pauseVideo()', { message: 'No published video' });
346
- return;
347
- }
348
-
349
- try {
350
- await this.pauseRemoteProducer(this.media.videoProducer.id);
351
- this.media.videoProducer.pause();
352
- this.logger.debug('pauseVideo()');
353
- } catch (error) {
354
- this.logger.error('pauseVideo()', { error });
355
- throw new Error('Can`t pause video');
356
- }
357
- }
358
-
359
- public async resumeVideo(): Promise<void> {
360
- if (!this.media.videoProducer) {
361
- this.logger.warn('resumeVideo()', { message: 'No published video' });
362
- return;
363
- }
364
-
365
- try {
366
- await this.resumeRemoteProducer(this.media.videoProducer.id);
367
- this.media.videoProducer.resume();
368
- this.logger.debug('resumeVideo()');
369
- } catch (error) {
370
- this.logger.error('resumeVideo()', { error });
371
- throw new Error('Can`t resume video');
372
- }
373
- }
374
-
375
- public async pauseAudio(): Promise<void> {
376
- if (!this.media.audioProducer) {
377
- this.logger.warn('pauseAudio()', { message: 'No published audio' });
378
- return;
379
- }
380
-
381
- try {
382
- await this.pauseRemoteProducer(this.media.audioProducer.id);
383
- this.media.audioProducer.pause();
384
- this.logger.debug('pauseAudio()');
385
- } catch (error) {
386
- this.logger.error('pauseAudio()', { error });
387
- throw new Error('Can`t pause audio');
388
- }
389
- }
390
-
391
- public async resumeAudio(): Promise<void> {
392
- if (!this.media.audioProducer) {
393
- this.logger.warn('resumeAudio()', { message: 'No published audio' });
394
- return;
395
- }
365
+ await this.media.deleteTrack(track);
366
+ this.logger.debug('unpublish()', { track });
367
+ });
396
368
 
397
369
  try {
398
- await this.resumeRemoteProducer(this.media.audioProducer.id);
399
- this.media.audioProducer.resume();
400
- this.logger.debug('resumeAudio()');
370
+ await Promise.all(actions);
401
371
  } catch (error) {
402
- this.logger.error('resumeAudio()', { error });
403
- throw new Error('Can`t resume audio');
372
+ this.logger.error('unpublish()', { error });
404
373
  }
405
374
  }
406
375
  }
@@ -4,10 +4,6 @@ class Consumer extends MediasoupConsumer {
4
4
  public score = 10;
5
5
 
6
6
  public producerScore = 10;
7
-
8
- constructor(consumer: MediasoupConsumer) {
9
- super(consumer);
10
- }
11
7
  }
12
8
 
13
9
  export default Consumer;
@@ -1,21 +1,12 @@
1
- import Consumer from './Consumer';
2
1
  import { Consumer as MediasoupConsumer } from 'mediasoup-client/lib/types';
3
2
  import { parseScalabilityMode } from 'mediasoup-client';
3
+ import Consumer from './Consumer';
4
4
 
5
5
  class VideoConsumer extends Consumer {
6
6
  public spatialLayers = 0;
7
7
 
8
8
  public temporalLayers = 0;
9
9
 
10
- private currentSpatialLayer = 0;
11
-
12
- private currentTemporalLayer = 0;
13
-
14
- currentPreferredLayers: { spatialLayer?: number, temporalLayer?: number } = {
15
- spatialLayer: undefined,
16
- temporalLayer: undefined,
17
- };
18
-
19
10
  constructor(consumer: MediasoupConsumer) {
20
11
  super(consumer);
21
12
  this.parseScalabilityMode();
@@ -29,14 +20,6 @@ class VideoConsumer extends Consumer {
29
20
  this.temporalLayers = temporalLayers;
30
21
  }
31
22
  }
32
-
33
- setCurrentSpatialLayer(currentSpatialLayer: number): void {
34
- this.currentSpatialLayer = currentSpatialLayer;
35
- }
36
-
37
- setCurrentTemporalLayer(currentTemporalLayer: number): void {
38
- this.currentTemporalLayer = currentTemporalLayer;
39
- }
40
23
  }
41
24
 
42
25
  export default VideoConsumer;
@@ -1,19 +1,15 @@
1
1
  import { Device } from 'mediasoup-client';
2
- import { Producer } from 'mediasoup-client/lib/Producer';
3
2
  import { RtpCapabilities, RtpCodecCapability } from 'mediasoup-client/lib/RtpParameters';
3
+ import VideoTrack from './tracks/VideoTrack';
4
+ import AudioTrack from './tracks/AudioTrack';
5
+ import { Track } from '../../types/common';
4
6
 
5
7
  class Media {
6
8
  public mediasoupDevice: Device;
7
9
 
8
- public audioProducer?: Producer;
9
-
10
- public videoProducer?: Producer;
11
-
12
10
  public isDeviceLoaded = false;
13
11
 
14
- public isVideoDisabled = false;
15
-
16
- public isAudioDisabled = false;
12
+ private tracks: Map<string, Track> = new Map();
17
13
 
18
14
  constructor() {
19
15
  this.mediasoupDevice = new Device();
@@ -26,46 +22,50 @@ class Media {
26
22
  }
27
23
  }
28
24
 
29
- getCodec(): RtpCodecCapability | undefined {
25
+ getTrackCodec(track: Track): RtpCodecCapability | undefined {
26
+ if (!(track instanceof VideoTrack)) {
27
+ return undefined;
28
+ }
29
+
30
30
  if (!this.mediasoupDevice.rtpCapabilities?.codecs) {
31
31
  return undefined;
32
32
  }
33
33
 
34
- const preferredCodec = 'h264';
35
34
  return this.mediasoupDevice
36
35
  .rtpCapabilities
37
36
  .codecs
38
- .find((c) => c.mimeType.toLowerCase() === `video/${preferredCodec}`);
37
+ .find((c) => c.mimeType.toLowerCase() === `video/${track.getPreferredCodec()}`);
39
38
  }
40
39
 
41
- setVideoProducer(producer: Producer): void {
42
- this.videoProducer = producer;
43
- this.isVideoDisabled = false;
44
- }
40
+ private createTrack(stream: MediaStream): Track {
41
+ const mediaStreamTrack = stream.getTracks()[0];
42
+
43
+ const track = mediaStreamTrack.kind === 'audio'
44
+ ? new AudioTrack(mediaStreamTrack)
45
+ : new VideoTrack(mediaStreamTrack);
45
46
 
46
- setAudioProducer(producer: Producer): void {
47
- this.audioProducer = producer;
48
- this.isAudioDisabled = false;
47
+ this.tracks.set(track.id, track);
48
+ return track;
49
49
  }
50
50
 
51
- closeVideoProducer(): void {
52
- if (!this.videoProducer) {
53
- return;
54
- }
51
+ async createUserMediaTrack(constraints: MediaStreamConstraints): Promise<Track> {
52
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
53
+ return this.createTrack(stream);
54
+ }
55
55
 
56
- this.videoProducer.close();
57
- this.videoProducer = undefined;
58
- this.isVideoDisabled = true;
56
+ async createDisplayMediaTrack(constraints: MediaStreamConstraints): Promise<Track> {
57
+ const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
58
+ return this.createTrack(stream);
59
59
  }
60
60
 
61
- closeAudioProducer(): void {
62
- if (!this.audioProducer) {
63
- return;
64
- }
61
+ async deleteTrack(track: Track) {
62
+ track.closeProducer();
63
+ track.mediaStreamTrack.stop();
64
+ this.tracks.delete(track.id);
65
+ }
65
66
 
66
- this.audioProducer.close();
67
- this.audioProducer = undefined;
68
- this.isAudioDisabled = true;
67
+ getAllTracks(): Track[] {
68
+ return Array.from(this.tracks.values());
69
69
  }
70
70
  }
71
71
 
@@ -0,0 +1,18 @@
1
+ import { ProducerCodecOptions } from 'mediasoup-client/lib/Producer';
2
+ import { AudioEncoderConfig } from 'types/common';
3
+ import BaseTrack from './BaseTrack';
4
+ import TrackWithCodecOptions from './TrackWithCodecOptions';
5
+
6
+ class AudioTrack extends BaseTrack implements TrackWithCodecOptions {
7
+ getEncoderConfig(): AudioEncoderConfig {
8
+ return this.encoderConfig;
9
+ }
10
+
11
+ getCodecOptions(): ProducerCodecOptions {
12
+ return {
13
+ opusFec: this.getEncoderConfig().enableFec || true,
14
+ };
15
+ }
16
+ }
17
+
18
+ export default AudioTrack;