@livedigital/client 1.13.1 → 1.15.1

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.
@@ -125,9 +125,13 @@ export declare type CreateScreenVideoTrackOptions = CreateVideoTrackOptions & {
125
125
  export declare type CreateMicrophoneAudioTrackOptions = CreateTrackOptions & {
126
126
  encoderConfig?: AudioEncoderConfig;
127
127
  };
128
- export declare type CreateScreenAudioTrackOptions = CreateVideoTrackOptions & {
128
+ export declare type CreateScreenAudioTrackOptions = CreateMicrophoneAudioTrackOptions & {
129
129
  encoderConfig?: AudioEncoderConfig;
130
130
  };
131
+ export declare type CreateScreenMediaOptions = {
132
+ video: CreateScreenVideoTrackOptions;
133
+ audio: CreateScreenAudioTrackOptions;
134
+ };
131
135
  export declare type Track = VideoTrack | AudioTrack;
132
136
  export declare enum TrackLabel {
133
137
  Camera = "camera",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@livedigital/client",
3
3
  "author": "vlprojects",
4
4
  "license": "MIT",
5
- "version": "1.13.1",
5
+ "version": "1.15.1",
6
6
  "private": false,
7
7
  "bugs": {
8
8
  "url": "https://github.com/vlprojects/livedigital-sdk/issues"
@@ -1,7 +1,6 @@
1
1
  export const CHANNEL_EVENTS = {
2
2
  channelEvent: 'channel.event',
3
3
  channelJoin: 'channel.join',
4
- channelBroadcast: 'channel.broadcast',
5
4
  channelGetPeers: 'channel.getPeers',
6
5
  channelLeave: 'peer.disconnected',
7
6
  };
@@ -10,8 +9,6 @@ export const CLIENT_EVENTS = {
10
9
  channelEvent: 'channel-event',
11
10
  peerJoined: 'peer-joined',
12
11
  peerLeft: 'peer-left',
13
- channelLeave: 'peer-left',
14
- peerBroadcasted: 'peer-broadcasted',
15
12
  channelRejoinRequired: 'channel-rejoin-required',
16
13
  devicesListUpdated: 'devices-list-updated',
17
14
  };
@@ -24,14 +21,14 @@ export const PEER_EVENTS = {
24
21
  trackEnd: 'track-end',
25
22
  };
26
23
 
27
- export const SOCKET_IO_EVENTS = {
28
- connected: 'connected',
29
- reconnecting: 'reconnecting',
30
- reconnected: 'reconnected',
31
- disconnected: 'disconnected',
32
- reconnect: 'reconnect',
33
- error: 'error',
34
- };
24
+ export enum SocketIOEvents {
25
+ Connected = 'connected',
26
+ Reconnecting = 'reconnecting',
27
+ Reconnected = 'reconnected',
28
+ Disconnected = 'disconnected',
29
+ Reconnect = 'reconnect',
30
+ Error = 'error',
31
+ }
35
32
 
36
33
  export const MEDIASOUP_EVENTS = {
37
34
  newProducer: 'peer.newProducer',
@@ -235,6 +235,8 @@ class Peer {
235
235
  rtpCapabilities: this.engine.media.mediasoupDevice.rtpCapabilities,
236
236
  appData: {
237
237
  peerId: this.engine.mySocketId,
238
+ appId: this.engine.appId,
239
+ channelId: this.engine.channelId,
238
240
  },
239
241
  producerPeerId: this.id,
240
242
  }) as ConsumerOptions;
@@ -299,6 +301,10 @@ class Peer {
299
301
  }
300
302
 
301
303
  private handleNewProducer(producerData: ProducerData): void {
304
+ if (this.producers.get(producerData.id)) {
305
+ return;
306
+ }
307
+
302
308
  const producer = new PeerProducer(producerData);
303
309
  this.producers.set(producer.id, producer);
304
310
  this.logger.debug('Peer published media:', { peer: this, producer });
@@ -26,27 +26,11 @@ class ChannelEventHandler {
26
26
  });
27
27
 
28
28
  connection.on(CHANNEL_EVENTS.channelJoin, (peerData: PeerResponse) => {
29
- this.engine.setPeer(peerData);
30
- const peer = this.engine.peers.find((item) => item.id === peerData.id);
31
- if (!peer) {
32
- return;
33
- }
34
-
29
+ const peer = this.engine.setPeer(peerData);
35
30
  this.engine.clientEventEmitter.safeEmit(CLIENT_EVENTS.peerJoined, peer);
36
31
  this.logger.debug('Peer joined to the channel', { peer });
37
32
  });
38
33
 
39
- connection.on(CHANNEL_EVENTS.channelBroadcast, (peerData: PeerResponse) => {
40
- this.engine.setPeer(peerData);
41
- const peer = this.engine.peers.find((item) => item.id === peerData.id);
42
- if (!peer) {
43
- return;
44
- }
45
-
46
- this.engine.clientEventEmitter.safeEmit(CLIENT_EVENTS.peerBroadcasted, peer);
47
- this.logger.debug('Peer broadcasted to the channel', { peer });
48
- });
49
-
50
34
  connection.on(CHANNEL_EVENTS.channelLeave, (peerId: string) => {
51
35
  this.engine.removePeer(peerId);
52
36
  this.engine.clientEventEmitter.safeEmit(CLIENT_EVENTS.peerLeft, peerId);
@@ -1,16 +1,15 @@
1
1
  import { RtpCapabilities } from 'mediasoup-client/lib/RtpParameters';
2
2
  import {
3
- PeerResponse,
4
- JoinChannelParams,
5
- SocketResponse,
6
3
  CreateCameraVideoTrackOptions,
7
4
  CreateMicrophoneAudioTrackOptions,
8
- CreateScreenVideoTrackOptions,
9
- CreateScreenAudioTrackOptions,
10
- Track,
11
- TrackLabel,
5
+ CreateScreenMediaOptions,
12
6
  EndTrackPayload,
7
+ JoinChannelParams,
8
+ PeerResponse,
9
+ SocketResponse,
13
10
  StartTrackPayload,
11
+ Track,
12
+ TrackLabel,
14
13
  } from '../types/common';
15
14
  import EnhancedEventEmitter from '../EnhancedEventEmitter';
16
15
  import System from './system';
@@ -21,11 +20,7 @@ import ChannelEventHandler from './handlers/ChannelEventHandler';
21
20
  import MediaSoupEventHandler from './handlers/MediaSoupEventHandler';
22
21
  import Logger from './Logger';
23
22
  import {
24
- CHANNEL_EVENTS,
25
- CLIENT_EVENTS,
26
- MEDIASOUP_EVENTS,
27
- PEER_EVENTS,
28
- SOCKET_IO_EVENTS,
23
+ CHANNEL_EVENTS, CLIENT_EVENTS, MEDIASOUP_EVENTS, PEER_EVENTS, SocketIOEvents,
29
24
  } from '../constants/events';
30
25
 
31
26
  type EngineParams = {
@@ -52,6 +47,12 @@ class Engine {
52
47
 
53
48
  private isJoined = false;
54
49
 
50
+ private app?: string;
51
+
52
+ private channel?: string;
53
+
54
+ private isRoomJoining = false;
55
+
55
56
  private logger: Logger;
56
57
 
57
58
  constructor(params: EngineParams) {
@@ -86,7 +87,8 @@ class Engine {
86
87
  try {
87
88
  this.initialized = false;
88
89
  this.isJoined = false;
89
-
90
+ this.app = undefined;
91
+ this.channel = undefined;
90
92
  this.network.socket.disconnect();
91
93
  this.peersRepository.clear();
92
94
  await this.unpublish();
@@ -109,10 +111,10 @@ class Engine {
109
111
  }
110
112
 
111
113
  private watchSocketState(): void {
112
- this.network.socket.observer.on('state', (state: string) => {
114
+ this.network.socket.observer.on('state', ({ state }: { state: SocketIOEvents }) => {
113
115
  this.clientEventEmitter.emit(state);
114
116
 
115
- if (state === SOCKET_IO_EVENTS.reconnected) {
117
+ if (state === SocketIOEvents.Reconnected) {
116
118
  this.network.socket.disconnect();
117
119
  this.clientEventEmitter.emit(CLIENT_EVENTS.channelRejoinRequired);
118
120
  }
@@ -129,29 +131,28 @@ class Engine {
129
131
 
130
132
  public async join(params: JoinChannelParams): Promise<void> {
131
133
  try {
132
- this.logger.debug('join()');
133
- const { webSocketUrl } = await this.network.loadBalancerClient.getNode({ channelId: params.channelId });
134
+ this.logger.debug('join()', { params });
135
+ this.isRoomJoining = true;
136
+ const { webSocketUrl } = await this.getAvailableNode({ channelId: params.channelId });
134
137
  this.network.socket.connect(webSocketUrl);
135
- await this.network.socket.request(CHANNEL_EVENTS.channelJoin, params);
136
- await this.initialize();
137
- const { peers } = await this.network.socket.request(CHANNEL_EVENTS.channelGetPeers) as { peers: PeerResponse[] };
138
- peers.forEach((peer: PeerResponse) => this.setPeer(peer));
139
- this.channelEventsHandler.subscribeToEvents();
140
- this.mediaSoupEventsHandler.subscribeToEvents();
141
- this.isJoined = true;
142
- this.logger.debug(`Successfully joined to ${params.channelId}. Peers in channel:`, this.peersRepository);
138
+ await this.waitForSocketConnection();
139
+ await this.performJoin(params);
143
140
  } catch (error) {
144
141
  this.logger.error('join()', { error });
145
- throw new Error(error);
142
+ throw error;
143
+ } finally {
144
+ this.isRoomJoining = false;
146
145
  }
147
146
  }
148
147
 
149
- public setPeer(peerData: PeerResponse): void {
150
- this.peersRepository.set(peerData.id, new Peer({
148
+ public setPeer(peerData: PeerResponse): Peer {
149
+ const peer = new Peer({
151
150
  ...peerData,
152
151
  engine: this,
153
152
  loginDate: new Date(),
154
- }));
153
+ });
154
+ this.peersRepository.set(peer.id, peer);
155
+ return peer;
155
156
  }
156
157
 
157
158
  public removePeer(peerId: string): void {
@@ -195,7 +196,7 @@ class Engine {
195
196
  async createCameraVideoTrack(options?: CreateCameraVideoTrackOptions): Promise<Track> {
196
197
  const trackParams = Media.getCameraVideoTrackParams(options);
197
198
  try {
198
- const track = await this.media.createUserMediaTrack({
199
+ const [track] = await this.media.createUserMediaTracks({
199
200
  audio: false,
200
201
  video: trackParams.videoTrackOptions,
201
202
  });
@@ -213,7 +214,7 @@ class Engine {
213
214
 
214
215
  async createMicrophoneAudioTrack(options?: CreateMicrophoneAudioTrackOptions): Promise<Track> {
215
216
  try {
216
- const track = await this.media.createUserMediaTrack({
217
+ const [track] = await this.media.createUserMediaTracks({
217
218
  video: false,
218
219
  audio: {
219
220
  deviceId: options?.deviceId,
@@ -233,50 +234,38 @@ class Engine {
233
234
  }
234
235
  }
235
236
 
236
- async createScreenVideoTrack(options?: CreateScreenVideoTrackOptions): Promise<Track> {
237
- const trackParams = Media.getScreenVideoTrackParams(options);
237
+ async createScreenMediaTracks(options?: CreateScreenMediaOptions): Promise<Track[]> {
238
+ const videoTrackParams = Media.getScreenVideoTrackParams(options?.video);
238
239
  try {
239
- const track = await this.media.createDisplayMediaTrack({
240
- audio: false,
241
- video: trackParams.videoTrackOptions,
240
+ const tracks = await this.media.createDisplayMediaTracks({
241
+ audio: options?.audio,
242
+ video: videoTrackParams.videoTrackOptions,
242
243
  });
243
244
 
244
- track.mediaStreamTrack.addEventListener('ended', () => {
245
- this.unpublish(track);
246
- });
245
+ tracks.forEach((track) => {
246
+ track.mediaStreamTrack.addEventListener('ended', () => {
247
+ this.unpublish(track);
248
+ });
247
249
 
248
- track.setLabel(TrackLabel.ScreenVideo);
249
- track.setEncoderConfig(trackParams.encoderConfig);
250
+ if (track.kind === 'video') {
251
+ track.setLabel(TrackLabel.ScreenVideo);
252
+ track.setEncoderConfig(videoTrackParams.encoderConfig);
253
+ this.logger.debug('createScreenMediaTrack()', { trackParams: videoTrackParams, track });
254
+ return;
255
+ }
250
256
 
251
- this.logger.debug('createScreenVideoTrack()', { trackParams, track });
252
- return track;
253
- } catch (error) {
254
- this.logger.error('createScreenVideoTrack()', { error, trackParams });
255
- throw new Error(`Can not create screen video track: ${error.message}`);
256
- }
257
- }
257
+ track.setLabel(TrackLabel.ScreenAudio);
258
+ if (options?.audio) {
259
+ track.setEncoderConfig(options.audio);
260
+ }
258
261
 
259
- async createScreenAudioTrack(options?: CreateScreenAudioTrackOptions): Promise<Track> {
260
- try {
261
- const track = await this.media.createDisplayMediaTrack({
262
- video: false,
263
- audio: true,
262
+ this.logger.debug('createScreenMediaTrack()', { trackParams: options?.audio, track });
264
263
  });
265
264
 
266
- track.mediaStreamTrack.addEventListener('ended', () => {
267
- this.unpublish(track);
268
- });
269
-
270
- track.setLabel(TrackLabel.ScreenAudio);
271
- if (options?.encoderConfig) {
272
- track.setEncoderConfig(options.encoderConfig);
273
- }
274
-
275
- this.logger.debug('createScreenAudioTrack()', { options, track });
276
- return track;
265
+ return tracks;
277
266
  } catch (error) {
278
- this.logger.error('createScreenAudioTrack()', { error, options });
279
- throw new Error(`Can not create screen audio track: ${error.message}`);
267
+ this.logger.error('createScreenMediaTrack()', { error, trackParams: videoTrackParams });
268
+ throw new Error(`Can not create screen media tracks: ${error.message}`);
280
269
  }
281
270
  }
282
271
 
@@ -364,6 +353,63 @@ class Engine {
364
353
  deleteTrack(tracks: Track): void {
365
354
  return this.media.deleteTrack(tracks);
366
355
  }
356
+
357
+ public get channelId(): string | undefined {
358
+ return this.channel;
359
+ }
360
+
361
+ public get appId(): string | undefined {
362
+ return this.app;
363
+ }
364
+
365
+ private async getAvailableNode({ channelId }: { channelId: string }): Promise<{ webSocketUrl: string }> {
366
+ try {
367
+ const response = await this.network.loadBalancerClient.getNode({ channelId });
368
+ return { webSocketUrl: response.webSocketUrl };
369
+ } catch (error) {
370
+ this.logger.error('getAvailableNode()', { error: 'No available nodes' });
371
+ throw error;
372
+ }
373
+ }
374
+
375
+ private waitForSocketConnection(): Promise<void> {
376
+ return new Promise((resolve, reject) => {
377
+ const onSocketStateChange = async (data: { state: SocketIOEvents, error?: string }) => {
378
+ const { error, state } = data;
379
+ if (error) {
380
+ this.network.socket.observer.removeListener('state', onSocketStateChange);
381
+ reject(error);
382
+ return;
383
+ }
384
+
385
+ if (this.isRoomJoining && state === SocketIOEvents.Connected) {
386
+ this.network.socket.observer.removeListener('state', onSocketStateChange);
387
+ resolve();
388
+ }
389
+ };
390
+
391
+ this.network.socket.observer.on('state', onSocketStateChange);
392
+ });
393
+ }
394
+
395
+ private async performJoin(params: JoinChannelParams): Promise<void> {
396
+ try {
397
+ await this.network.socket.request(CHANNEL_EVENTS.channelJoin, params);
398
+ await this.initialize();
399
+ const { peers } = await this.network.socket
400
+ .request(CHANNEL_EVENTS.channelGetPeers) as { peers: PeerResponse[] };
401
+ peers.forEach((peer: PeerResponse) => this.setPeer(peer));
402
+ this.channelEventsHandler.subscribeToEvents();
403
+ this.mediaSoupEventsHandler.subscribeToEvents();
404
+ this.app = params.appId;
405
+ this.channel = params.channelId;
406
+ this.isJoined = true;
407
+ this.logger.debug(`Successfully joined to ${params.channelId}. Peers in channel:`, this.peersRepository);
408
+ } catch (error) {
409
+ this.logger.error('performJoin()', { error });
410
+ throw error;
411
+ }
412
+ }
367
413
  }
368
414
 
369
415
  export default Engine;
@@ -46,28 +46,33 @@ class Media {
46
46
  .find((c) => c.mimeType.toLowerCase() === `video/${track.getPreferredCodec()}`);
47
47
  }
48
48
 
49
- private createTrack(stream: MediaStream): Track {
50
- const mediaStreamTrack = stream.getTracks()[0];
51
-
52
- const track = mediaStreamTrack.kind === 'audio'
53
- ? new AudioTrack(mediaStreamTrack)
54
- : new VideoTrack(mediaStreamTrack);
55
-
56
- this.tracks.set(track.id, track);
57
- this.#logger.debug('createTrack() track created', { streamId: stream.id, trackId: track.id, kind: track.kind });
58
-
59
- return track;
49
+ private createTracks(stream: MediaStream): Track[] {
50
+ const mediaStreamTracks = stream.getTracks();
51
+ return mediaStreamTracks.map((mediaStreamTrack) => {
52
+ const track = mediaStreamTrack.kind === 'audio'
53
+ ? new AudioTrack(mediaStreamTrack)
54
+ : new VideoTrack(mediaStreamTrack);
55
+
56
+ this.tracks.set(track.id, track);
57
+ this.#logger.debug('createTrack() track created', {
58
+ streamId: stream.id,
59
+ trackId: track.id,
60
+ kind: track.kind,
61
+ });
62
+
63
+ return track;
64
+ });
60
65
  }
61
66
 
62
- async createUserMediaTrack(constraints: MediaStreamConstraints): Promise<Track> {
67
+ async createUserMediaTracks(constraints: MediaStreamConstraints): Promise<Track[]> {
63
68
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
64
69
  this.#logger.debug('createUserMediaTrack() stream created', { streamId: stream.id });
65
- return this.createTrack(stream);
70
+ return this.createTracks(stream);
66
71
  }
67
72
 
68
- async createDisplayMediaTrack(constraints: MediaStreamConstraints): Promise<Track> {
73
+ async createDisplayMediaTracks(constraints: MediaStreamConstraints): Promise<Track[]> {
69
74
  const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
70
- return this.createTrack(stream);
75
+ return this.createTracks(stream);
71
76
  }
72
77
 
73
78
  deleteTrack(track: Track) {
@@ -1,6 +1,6 @@
1
1
  import { io, Socket } from 'socket.io-client';
2
2
  import EnhancedEventEmitter from '../../EnhancedEventEmitter';
3
- import { SOCKET_IO_EVENTS } from '../../constants/events';
3
+ import { SocketIOEvents } from '../../constants/events';
4
4
  import { SocketResponse } from '../../types/common';
5
5
  import Logger from '../Logger';
6
6
 
@@ -41,20 +41,20 @@ class SocketIO {
41
41
 
42
42
  connection.on('connect', () => {
43
43
  const state = !this.reconnectingAttempt
44
- ? SOCKET_IO_EVENTS.connected
45
- : SOCKET_IO_EVENTS.reconnected;
44
+ ? SocketIOEvents.Connected
45
+ : SocketIOEvents.Reconnected;
46
46
 
47
47
  this.isConnected = true;
48
48
  this.reconnectingAttempt = 0;
49
49
 
50
50
  this.logger.debug('connection.on(`connect`)');
51
- this.observer.safeEmit('state', state);
51
+ this.observer.safeEmit('state', { state });
52
52
  });
53
53
 
54
54
  connection.on('connect_error', (error: unknown) => {
55
55
  this.connectionError = error as string;
56
56
  this.logger.error('connection.on(`connect_error`)', error);
57
- this.observer.safeEmit('state', SOCKET_IO_EVENTS.error);
57
+ this.observer.safeEmit('state', { state: SocketIOEvents.Error, error });
58
58
  });
59
59
 
60
60
  connection.on('disconnect', ((reason: string) => {
@@ -62,18 +62,18 @@ class SocketIO {
62
62
  this.reconnectingAttempt = 0;
63
63
  this.disconnectReason = reason;
64
64
  this.logger.error('connection.on(`disconnect`)', reason);
65
- this.observer.safeEmit('state', SOCKET_IO_EVENTS.disconnected);
65
+ this.observer.safeEmit('state', { state: SocketIOEvents.Disconnected });
66
66
  }));
67
67
 
68
68
  connection.io.on('reconnect_attempt', (attempt: number) => {
69
69
  this.reconnectingAttempt = attempt;
70
70
  this.logger.warn('connection.on(`reconnect_attempt`)', attempt);
71
- this.observer.safeEmit('state', SOCKET_IO_EVENTS.reconnecting);
71
+ this.observer.safeEmit('state', { state: SocketIOEvents.Reconnecting });
72
72
  });
73
73
 
74
74
  connection.io.on('reconnect', () => {
75
75
  this.logger.debug('connection.on(`reconnect`)');
76
- this.observer.safeEmit('state', SOCKET_IO_EVENTS.reconnect);
76
+ this.observer.safeEmit('state', { state: SocketIOEvents.Reconnect });
77
77
  });
78
78
  }
79
79
 
package/src/index.ts CHANGED
@@ -2,8 +2,7 @@ import {
2
2
  AvailableMediaDevices,
3
3
  CreateCameraVideoTrackOptions,
4
4
  CreateMicrophoneAudioTrackOptions,
5
- CreateScreenAudioTrackOptions,
6
- CreateScreenVideoTrackOptions,
5
+ CreateScreenMediaOptions,
7
6
  JoinChannelParams,
8
7
  Track,
9
8
  } from './types/common';
@@ -96,12 +95,8 @@ class Client {
96
95
  return this.engine.createMicrophoneAudioTrack(options);
97
96
  }
98
97
 
99
- createScreenVideoTrack(options?: CreateScreenVideoTrackOptions): Promise<Track> {
100
- return this.engine.createScreenVideoTrack(options);
101
- }
102
-
103
- createScreenAudioTrack(options?: CreateScreenAudioTrackOptions): Promise<Track> {
104
- return this.engine.createScreenAudioTrack(options);
98
+ createScreenMediaTracks(options?: CreateScreenMediaOptions): Promise<Track[]> {
99
+ return this.engine.createScreenMediaTracks(options);
105
100
  }
106
101
 
107
102
  deleteTrack(track: Track): void {
@@ -152,10 +152,15 @@ export type CreateMicrophoneAudioTrackOptions = CreateTrackOptions & {
152
152
  encoderConfig?: AudioEncoderConfig,
153
153
  };
154
154
 
155
- export type CreateScreenAudioTrackOptions = CreateVideoTrackOptions & {
155
+ export type CreateScreenAudioTrackOptions = CreateMicrophoneAudioTrackOptions & {
156
156
  encoderConfig?: AudioEncoderConfig,
157
157
  };
158
158
 
159
+ export type CreateScreenMediaOptions = {
160
+ video: CreateScreenVideoTrackOptions,
161
+ audio: CreateScreenAudioTrackOptions,
162
+ };
163
+
159
164
  export type Track = VideoTrack | AudioTrack;
160
165
 
161
166
  export enum TrackLabel {