@livedigital/client 2.43.0 → 2.44.0-audio-observer.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.
@@ -0,0 +1,28 @@
1
+ import { TrackLabel } from './common';
2
+ export declare enum ChannelAudioObserverEvents {
3
+ DominantSpeaker = "dominant-speaker",
4
+ PeersVolumes = "peers-volumes",
5
+ Silence = "silence"
6
+ }
7
+ export declare type DominantSpeakerEvent = {
8
+ event: ChannelAudioObserverEvents.DominantSpeaker;
9
+ data: {
10
+ peerId: string;
11
+ producerId: string;
12
+ trackLabel: TrackLabel.Microphone;
13
+ };
14
+ };
15
+ export declare type SilenceEvent = {
16
+ event: ChannelAudioObserverEvents.Silence;
17
+ };
18
+ export declare type PeerVolume = {
19
+ peerId: string;
20
+ producerId: string;
21
+ value: number;
22
+ trackLabel: TrackLabel.Microphone;
23
+ };
24
+ export declare type PeersVolumesEvent = {
25
+ event: ChannelAudioObserverEvents.PeersVolumes;
26
+ volumes: PeerVolume[];
27
+ };
28
+ export declare type AudioObserverEvents = DominantSpeakerEvent | SilenceEvent | PeersVolumesEvent;
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  import { MediaKind, RtpEncodingParameters, RtpParameters } from 'mediasoup-client/lib/types';
3
- import { ConsumerOptions } from 'mediasoup-client/lib/Consumer';
3
+ import { Consumer, ConsumerOptions } from 'mediasoup-client/lib/Consumer';
4
4
  import { RtpCapabilities } from 'mediasoup-client/src/RtpParameters';
5
5
  import { ProducerCodecOptions } from 'mediasoup-client/lib/Producer';
6
6
  import { ConnectionState } from 'mediasoup-client/src/Transport';
@@ -232,6 +232,15 @@ export declare type CreateConsumerPayload = {
232
232
  channelId?: string;
233
233
  producerPeerId: string;
234
234
  };
235
+ export declare type CreateDataConsumerPayload = {
236
+ producerId: string;
237
+ appId?: string;
238
+ channelId?: string;
239
+ };
240
+ export declare type CreateConsumerResponse = {
241
+ consumer: Consumer;
242
+ isProducerPaused: boolean;
243
+ };
235
244
  export declare enum DeviceErrors {
236
245
  NotFoundError = "NotFoundError",
237
246
  NoDevices = "NoDevices",
@@ -8,6 +8,7 @@ import ChannelEventHandler from '../engine/handlers/ChannelEventHandler';
8
8
  import MediaSoupEventHandler from '../engine/handlers/MediaSoupEventHandler';
9
9
  import { LoadBalancerApiClientParams } from '../engine/network/LoadBalancerClient';
10
10
  import { Logger, LogLevel, LogMessageHandler } from './common';
11
+ import ChannelAudioObserverEventHandler from '../engine/handlers/ChannelAudioObserverEventHandler';
11
12
  export declare type IssuesHandler = (issues: IssueDetectorResult) => void;
12
13
  export declare type NetworkScoresUpdatedHandler = (networks: NetworkScores) => void;
13
14
  export interface CreateIssueDetectorParams {
@@ -44,11 +45,16 @@ export interface CreateChannelEventHandlerParams {
44
45
  engine: Engine;
45
46
  onLogMessage?: LogMessageHandler;
46
47
  }
48
+ export interface CreateAudioObserverEventHandlerParams {
49
+ engine: Engine;
50
+ onLogMessage?: LogMessageHandler;
51
+ }
47
52
  export interface EngineDependenciesFactory {
48
53
  createSystem: (params: CreateSystemParams) => System;
49
54
  createMedia: (params: CreateMediaParams) => Media;
50
55
  createNetwork: (params: CreateNetworkParams) => Network;
51
56
  createChannelEventHandler: (params: CreateChannelEventHandlerParams) => ChannelEventHandler;
52
57
  createMediaSoupEventHandler: (params: CreateMediaSoupEventHandlerParams) => MediaSoupEventHandler;
58
+ createAudioObserverEventHandler: (params: CreateAudioObserverEventHandlerParams) => ChannelAudioObserverEventHandler;
53
59
  createIssueDetector: (params: CreateIssueDetectorParams) => WebRTCIssueDetector;
54
60
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@livedigital/client",
3
3
  "author": "vlprojects",
4
4
  "license": "MIT",
5
- "version": "2.43.0",
5
+ "version": "2.44.0-audio-observer.1",
6
6
  "private": false,
7
7
  "bugs": {
8
8
  "url": "https://github.com/vlprojects/livedigital-sdk/issues"
@@ -8,6 +8,7 @@ export const CHANNEL_EVENTS = {
8
8
  activityConfirmationAcquired: 'channel.activityConfirmationAcquired',
9
9
  activityConfirmationRequired: 'channel.activityConfirmationRequired',
10
10
  activityConfirmationExpired: 'channel.activityConfirmationExpired',
11
+ getAudioObserverProducer: 'channel.getAudioObserverProducer',
11
12
  };
12
13
 
13
14
  export const CLIENT_EVENTS = {
@@ -21,6 +22,11 @@ export const CLIENT_EVENTS = {
21
22
  devicesListUpdated: 'devices-list-updated',
22
23
  transportConnectionTimeout: 'transport-connection-timeout',
23
24
  trackPublishingFailed: 'track-publishing-failed',
25
+ activeSpeakerChanged: 'active-speaker-changed',
26
+ };
27
+
28
+ export const TRACK_EVENTS = {
29
+ volumeChanged: 'volume-changed',
24
30
  };
25
31
 
26
32
  export const INTERNAL_CLIENT_EVENTS = {
@@ -66,6 +72,7 @@ export const MEDIASOUP_EVENTS = {
66
72
  producerSetMaxSpatialLayer: 'producer.setMaxSpatialLayer',
67
73
  producerRequestMaxSpatialLayer: 'producer.requestMaxSpatialLayer',
68
74
  createConsumer: 'consumer.create',
75
+ createDataConsumer: 'dataConsumer.create',
69
76
  closeConsumer: 'consumer.close',
70
77
  pauseConsumer: 'consumer.pause',
71
78
  resumeConsumer: 'consumer.resume',
@@ -28,6 +28,7 @@ import {
28
28
  } from '../types/engine';
29
29
  import SocketIO from './network/Socket';
30
30
  import LoadBalancerApiClient from './network/LoadBalancerClient';
31
+ import ChannelAudioObserverEventHandler from './handlers/ChannelAudioObserverEventHandler';
31
32
 
32
33
  /* eslint-disable class-methods-use-this */
33
34
  class DefaultEngineDependenciesFactory implements EngineDependenciesFactory {
@@ -63,6 +64,10 @@ class DefaultEngineDependenciesFactory implements EngineDependenciesFactory {
63
64
  return new MediaSoupEventHandler(params);
64
65
  }
65
66
 
67
+ createAudioObserverEventHandler(params: CreateMediaSoupEventHandlerParams): ChannelAudioObserverEventHandler {
68
+ return new ChannelAudioObserverEventHandler(params);
69
+ }
70
+
66
71
  createIssueDetector(params: CreateIssueDetectorParams): WebRTCIssueDetector {
67
72
  const logger = params.logger ?? console;
68
73
  const compositeStatsParser = new CompositeRTCStatsParser({
@@ -0,0 +1,129 @@
1
+ import { serializeError } from 'serialize-error';
2
+ import { LogMessageHandler, TrackLabel } from '../../types/common';
3
+ import Engine from '../index';
4
+ import Logger from '../Logger';
5
+ import InvalidPayloadError from '../../errors/InvalidPayloadError';
6
+ import {
7
+ AudioObserverEvents,
8
+ ChannelAudioObserverEvents,
9
+ DominantSpeakerEvent, PeersVolumesEvent,
10
+ } from '../../types/channelAudioObserver';
11
+
12
+ class ChannelAudioObserverEventHandler {
13
+ private readonly engine: Engine;
14
+
15
+ private readonly logger: Logger;
16
+
17
+ constructor(params: { engine: Engine, onLogMessage?: LogMessageHandler }) {
18
+ const { engine, onLogMessage } = params;
19
+ this.engine = engine;
20
+ this.logger = new Logger({
21
+ logLevel: engine.logLevel,
22
+ namespace: 'ChannelAudioObserverEventHandler',
23
+ onLogMessage,
24
+ });
25
+ }
26
+
27
+ public handle(data: string): void {
28
+ try {
29
+ const payload = JSON.parse(data);
30
+ ChannelAudioObserverEventHandler.validateEventPayload(payload);
31
+ switch (payload.event) {
32
+ case ChannelAudioObserverEvents.DominantSpeaker: {
33
+ this.handleDominantSpeaker(payload);
34
+ break;
35
+ }
36
+ case ChannelAudioObserverEvents.PeersVolumes: {
37
+ this.handlePeerVolumes(payload);
38
+ break;
39
+ }
40
+ case ChannelAudioObserverEvents.Silence: {
41
+ this.handleSilence();
42
+ break;
43
+ }
44
+ default:
45
+ break;
46
+ }
47
+ } catch (error) {
48
+ this.logger.warn('Failed to handle audio observer event', {
49
+ error: serializeError(error),
50
+ eventData: data,
51
+ });
52
+ }
53
+ }
54
+
55
+ private handleDominantSpeaker(payload: DominantSpeakerEvent): void {
56
+ const { peerId } = payload.data;
57
+ const peer = this.engine.getPeerById(peerId);
58
+ this.engine.setActiveSpeakerPeer(peer);
59
+ }
60
+
61
+ private handlePeerVolumes(payload: PeersVolumesEvent): void {
62
+ payload.volumes.forEach((item) => {
63
+ const { peerId, trackLabel, value } = item;
64
+ const track = this.engine.getPeerById(peerId)?.tracks.get(trackLabel);
65
+ if (!track) {
66
+ return;
67
+ }
68
+
69
+ track.setVolume(value);
70
+ });
71
+ }
72
+
73
+ private handleSilence(): void {
74
+ const tracks = this.engine.peers.flatMap((peer) => Array.from(peer.tracks.values()));
75
+ tracks.forEach((track) => track.setVolume(0));
76
+ this.engine.setActiveSpeakerPeer(undefined);
77
+ }
78
+
79
+ private static validateEventPayload(payload: AudioObserverEvents): void {
80
+ switch (payload.event) {
81
+ case ChannelAudioObserverEvents.Silence: {
82
+ break;
83
+ }
84
+
85
+ case ChannelAudioObserverEvents.DominantSpeaker: {
86
+ if (!payload.data) {
87
+ ChannelAudioObserverEventHandler.throwInvalidPayload();
88
+ }
89
+
90
+ const { peerId, producerId, trackLabel } = payload.data;
91
+ if (typeof peerId !== 'string' || typeof producerId !== 'string' || trackLabel !== TrackLabel.Microphone) {
92
+ ChannelAudioObserverEventHandler.throwInvalidPayload();
93
+ }
94
+ break;
95
+ }
96
+
97
+ case ChannelAudioObserverEvents.PeersVolumes: {
98
+ if (!Array.isArray(payload.volumes)) {
99
+ ChannelAudioObserverEventHandler.throwInvalidPayload();
100
+ }
101
+
102
+ payload.volumes.forEach((item) => {
103
+ const {
104
+ peerId, producerId, value, trackLabel,
105
+ } = item;
106
+ if (
107
+ typeof peerId !== 'string'
108
+ || typeof producerId !== 'string'
109
+ || typeof value !== 'number'
110
+ || trackLabel !== TrackLabel.Microphone
111
+ ) {
112
+ ChannelAudioObserverEventHandler.throwInvalidPayload();
113
+ }
114
+ });
115
+ break;
116
+ }
117
+
118
+ default: {
119
+ ChannelAudioObserverEventHandler.throwInvalidPayload();
120
+ }
121
+ }
122
+ }
123
+
124
+ static throwInvalidPayload(): void {
125
+ throw new InvalidPayloadError('Invalid channel audio observer event handler payload');
126
+ }
127
+ }
128
+
129
+ export default ChannelAudioObserverEventHandler;
@@ -1,6 +1,7 @@
1
1
  import { UnsupportedError as MediasoupUnsupportedError } from 'mediasoup-client/lib/errors';
2
2
  import { RtpCapabilities } from 'mediasoup-client/lib/RtpParameters';
3
3
  import WebRTCIssueDetector from 'webrtc-issue-detector';
4
+ import { DataConsumer } from 'mediasoup-client/lib/DataConsumer';
4
5
  import {
5
6
  CreateCameraVideoTrackOptions,
6
7
  CreateMicrophoneAudioTrackOptions,
@@ -35,6 +36,8 @@ import { LogLevels } from '../constants/common';
35
36
  import { LoadBalancerApiClientParams } from './network/LoadBalancerClient';
36
37
  import validateAppData from '../helpers/appDataValidator';
37
38
  import AudioTrack from './media/tracks/AudioTrack';
39
+ import NeedJoinFirstError from '../errors/NeedJoinFirstError';
40
+ import ChannelAudioObserverEventHandler from './handlers/ChannelAudioObserverEventHandler';
38
41
 
39
42
  type EngineParams = {
40
43
  clientEventEmitter: EnhancedEventEmitter,
@@ -66,6 +69,8 @@ class Engine {
66
69
 
67
70
  private mediaSoupEventsHandler: MediaSoupEventHandler;
68
71
 
72
+ private channelAudioObserverEventHandler: ChannelAudioObserverEventHandler;
73
+
69
74
  private initialized = false;
70
75
 
71
76
  private isJoined = false;
@@ -80,6 +85,10 @@ class Engine {
80
85
 
81
86
  private webRtcIssueDetector?: WebRTCIssueDetector;
82
87
 
88
+ private audioObserver?: DataConsumer;
89
+
90
+ private activeSpeakerPeer?: Peer;
91
+
83
92
  constructor(params: EngineParams) {
84
93
  const {
85
94
  clientEventEmitter,
@@ -114,6 +123,10 @@ class Engine {
114
123
  engine: this,
115
124
  onLogMessage: params.onLogMessage,
116
125
  });
126
+ this.channelAudioObserverEventHandler = dependenciesFactory.createAudioObserverEventHandler({
127
+ engine: this,
128
+ onLogMessage: params.onLogMessage,
129
+ });
117
130
 
118
131
  this.logger = new Logger({
119
132
  logLevel: this.logLevel,
@@ -176,6 +189,7 @@ class Engine {
176
189
  await this.network.closeSendTransport();
177
190
  await this.network.closeReceiveTransport();
178
191
  await this.media.clearTracks();
192
+ this.audioObserver?.close();
179
193
 
180
194
  this.logger.debug('release()', {
181
195
  socketId: this.mySocketId,
@@ -197,6 +211,10 @@ class Engine {
197
211
  });
198
212
  }
199
213
 
214
+ public get activeSpeaker(): Peer | undefined {
215
+ return this.activeSpeakerPeer;
216
+ }
217
+
200
218
  public get peers(): Peer[] {
201
219
  return Array.from(this.peersRepository.values());
202
220
  }
@@ -213,6 +231,10 @@ class Engine {
213
231
  return this.network.socket.id;
214
232
  }
215
233
 
234
+ public getPeerById(id: string): Peer | undefined {
235
+ return this.peersRepository.get(id);
236
+ }
237
+
216
238
  public async confirmActivity(): Promise<void> {
217
239
  if (!this.channel || this.isChannelJoining) {
218
240
  throw new Error('Connect to the channel first');
@@ -411,6 +433,15 @@ class Engine {
411
433
  return this.media.isNoiseSuppressorLoaded;
412
434
  }
413
435
 
436
+ setActiveSpeakerPeer(peer?: Peer): void {
437
+ if (this.activeSpeaker === peer) {
438
+ return;
439
+ }
440
+
441
+ this.activeSpeakerPeer = peer;
442
+ this.clientEventEmitter.safeEmit(CLIENT_EVENTS.activeSpeakerChanged, { peer });
443
+ }
444
+
414
445
  private async connectToSocketServerWithRetry(params: { channelId: string, role: Role }): Promise<void> {
415
446
  const connectToSocketServerAction = async () => this.connectToSocketServer(params);
416
447
  return retryAsync(connectToSocketServerAction, {
@@ -496,6 +527,7 @@ class Engine {
496
527
  this.mediaSoupEventsHandler.subscribeToEvents();
497
528
  this.isJoined = true;
498
529
  this.logger.debug('Successfully joined channel', { channelId: params.channelId });
530
+ await this.createAudioObserver();
499
531
  } catch (error) {
500
532
  this.logger.error('performJoin()', { error });
501
533
  throw error;
@@ -576,6 +608,36 @@ class Engine {
576
608
 
577
609
  myPeer.tracks.set(peerTrack.label, peerTrack);
578
610
  }
611
+
612
+ private async createAudioObserver(): Promise<void> {
613
+ if (!this.isJoined) {
614
+ throw new NeedJoinFirstError('Need to join first');
615
+ }
616
+
617
+ const logCtx = {
618
+ channelId: this.channelId,
619
+ appId: this.appId,
620
+ peerId: this.myPeer?.id,
621
+ };
622
+
623
+ try {
624
+ const { producerId } = await this.network.getAudioObserverProducer() as { producerId: string };
625
+ const audioObserver = await this.network.createDataConsumer({
626
+ producerId,
627
+ appId: this.appId,
628
+ channelId: this.channelId,
629
+ });
630
+
631
+ audioObserver.on('message', (data) => {
632
+ this.channelAudioObserverEventHandler.handle(data);
633
+ });
634
+
635
+ this.audioObserver = audioObserver;
636
+ this.logger.debug('Successfully create audio observer', logCtx);
637
+ } catch (error) {
638
+ this.logger.error('Failed to create audio observer', logCtx);
639
+ }
640
+ }
579
641
  }
580
642
 
581
643
  export default Engine;
@@ -10,7 +10,7 @@ import {
10
10
  LogMessageHandler,
11
11
  } from '../../../types/common';
12
12
  import Logger from '../../Logger';
13
- import { MEDIASOUP_EVENTS, PEER_EVENTS } from '../../../constants/events';
13
+ import { MEDIASOUP_EVENTS, PEER_EVENTS, TRACK_EVENTS } from '../../../constants/events';
14
14
  import PeerConsumer from '../../PeerConsumer';
15
15
  import Engine from '../../index';
16
16
  import EnhancedEventEmitter from '../../../EnhancedEventEmitter';
@@ -52,6 +52,8 @@ class PeerTrack {
52
52
 
53
53
  #muted = false;
54
54
 
55
+ #volumeLevel: number = 0;
56
+
55
57
  readonly observer = new EnhancedEventEmitter();
56
58
 
57
59
  constructor(payload: PeerTrackConstructor) {
@@ -70,6 +72,10 @@ class PeerTrack {
70
72
  this.#peerEventEmitter.safeEmit(PEER_EVENTS.trackStart, this);
71
73
  }
72
74
 
75
+ get volume(): number {
76
+ return this.#volumeLevel;
77
+ }
78
+
73
79
  get isRemote(): boolean {
74
80
  return !!this.consumer;
75
81
  }
@@ -408,6 +414,15 @@ class PeerTrack {
408
414
  }
409
415
  }
410
416
 
417
+ setVolume(value: number): void {
418
+ if (this.#volumeLevel === value) {
419
+ return;
420
+ }
421
+
422
+ this.#volumeLevel = value;
423
+ this.observer.safeEmit(TRACK_EVENTS.volumeChanged, { value });
424
+ }
425
+
411
426
  private async closeConsumer(): Promise<void> {
412
427
  try {
413
428
  if (!this.consumer) {