livekit-client 0.18.4-RC7 → 0.18.5

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 +638 -517
  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 -875
  21. package/dist/proto/livekit_models.d.ts.map +1 -0
  22. package/dist/proto/livekit_rtc.d.ts +3904 -3903
  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 -25
  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 +29 -27
  63. package/dist/room/track/RemoteVideoTrack.d.ts.map +1 -0
  64. package/dist/room/track/Track.d.ts +105 -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 +232 -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 +779 -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 +258 -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 +249 -0
  116. package/src/room/track/Track.ts +365 -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 +26 -0
  120. package/src/room/track/options.ts +292 -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,229 @@
1
+ import log from '../../logger';
2
+ import DeviceManager from '../DeviceManager';
3
+ import { TrackInvalidError } from '../errors';
4
+ import { TrackEvent } from '../events';
5
+ import { getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, isMobile } from '../utils';
6
+ import { attachToElement, detachTrack, Track } from './Track';
7
+
8
+ export default class LocalTrack extends Track {
9
+ /** @internal */
10
+ sender?: RTCRtpSender;
11
+
12
+ protected constraints: MediaTrackConstraints;
13
+
14
+ protected wasMuted: boolean;
15
+
16
+ protected reacquireTrack: boolean;
17
+
18
+ protected constructor(
19
+ mediaTrack: MediaStreamTrack,
20
+ kind: Track.Kind,
21
+ constraints?: MediaTrackConstraints,
22
+ ) {
23
+ super(mediaTrack, kind);
24
+ this._mediaStreamTrack.addEventListener('ended', this.handleEnded);
25
+ this.constraints = constraints ?? mediaTrack.getConstraints();
26
+ this.reacquireTrack = false;
27
+ this.wasMuted = false;
28
+ }
29
+
30
+ get id(): string {
31
+ return this._mediaStreamTrack.id;
32
+ }
33
+
34
+ get dimensions(): Track.Dimensions | undefined {
35
+ if (this.kind !== Track.Kind.Video) {
36
+ return undefined;
37
+ }
38
+
39
+ const { width, height } = this._mediaStreamTrack.getSettings();
40
+ if (width && height) {
41
+ return {
42
+ width,
43
+ height,
44
+ };
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ private _isUpstreamPaused: boolean = false;
50
+
51
+ get isUpstreamPaused() {
52
+ return this._isUpstreamPaused;
53
+ }
54
+
55
+ /**
56
+ * @returns DeviceID of the device that is currently being used for this track
57
+ */
58
+ async getDeviceId(): Promise<string | undefined> {
59
+ // screen share doesn't have a usable device id
60
+ if (this.source === Track.Source.ScreenShare) {
61
+ return;
62
+ }
63
+ const { deviceId, groupId } = this._mediaStreamTrack.getSettings();
64
+ const kind = this.kind === Track.Kind.Audio ? 'audioinput' : 'videoinput';
65
+
66
+ return DeviceManager.getInstance().normalizeDeviceId(kind, deviceId, groupId);
67
+ }
68
+
69
+ async mute(): Promise<LocalTrack> {
70
+ this.setTrackMuted(true);
71
+ return this;
72
+ }
73
+
74
+ async unmute(): Promise<LocalTrack> {
75
+ this.setTrackMuted(false);
76
+ return this;
77
+ }
78
+
79
+ async replaceTrack(track: MediaStreamTrack): Promise<LocalTrack> {
80
+ if (!this.sender) {
81
+ throw new TrackInvalidError('unable to replace an unpublished track');
82
+ }
83
+
84
+ // detach
85
+ this.attachedElements.forEach((el) => {
86
+ detachTrack(this._mediaStreamTrack, el);
87
+ });
88
+ this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
89
+ // on Safari, the old audio track must be stopped before attempting to acquire
90
+ // the new track, otherwise the new track will stop with
91
+ // 'A MediaStreamTrack ended due to a capture failure`
92
+ this._mediaStreamTrack.stop();
93
+
94
+ track.addEventListener('ended', this.handleEnded);
95
+ log.debug('replace MediaStreamTrack');
96
+
97
+ await this.sender.replaceTrack(track);
98
+ this._mediaStreamTrack = track;
99
+
100
+ this.attachedElements.forEach((el) => {
101
+ attachToElement(track, el);
102
+ });
103
+
104
+ this.mediaStream = new MediaStream([track]);
105
+ return this;
106
+ }
107
+
108
+ protected async restart(constraints?: MediaTrackConstraints): Promise<LocalTrack> {
109
+ if (!this.sender) {
110
+ throw new TrackInvalidError('unable to restart an unpublished track');
111
+ }
112
+ if (!constraints) {
113
+ constraints = this.constraints;
114
+ }
115
+ log.debug('restarting track with constraints', constraints);
116
+
117
+ const streamConstraints: MediaStreamConstraints = {
118
+ audio: false,
119
+ video: false,
120
+ };
121
+
122
+ if (this.kind === Track.Kind.Video) {
123
+ streamConstraints.video = constraints;
124
+ } else {
125
+ streamConstraints.audio = constraints;
126
+ }
127
+
128
+ // detach
129
+ this.attachedElements.forEach((el) => {
130
+ detachTrack(this._mediaStreamTrack, el);
131
+ });
132
+ this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
133
+ // on Safari, the old audio track must be stopped before attempting to acquire
134
+ // the new track, otherwise the new track will stop with
135
+ // 'A MediaStreamTrack ended due to a capture failure`
136
+ this._mediaStreamTrack.stop();
137
+
138
+ // create new track and attach
139
+ const mediaStream = await navigator.mediaDevices.getUserMedia(streamConstraints);
140
+ const newTrack = mediaStream.getTracks()[0];
141
+ newTrack.addEventListener('ended', this.handleEnded);
142
+ log.debug('re-acquired MediaStreamTrack');
143
+
144
+ await this.sender.replaceTrack(newTrack);
145
+ this._mediaStreamTrack = newTrack;
146
+
147
+ this.attachedElements.forEach((el) => {
148
+ attachToElement(newTrack, el);
149
+ });
150
+
151
+ this.mediaStream = mediaStream;
152
+ this.constraints = constraints;
153
+ return this;
154
+ }
155
+
156
+ protected setTrackMuted(muted: boolean) {
157
+ if (this.isMuted === muted) {
158
+ return;
159
+ }
160
+
161
+ this.isMuted = muted;
162
+ this._mediaStreamTrack.enabled = !muted;
163
+ this.emit(muted ? TrackEvent.Muted : TrackEvent.Unmuted, this);
164
+ }
165
+
166
+ protected get needsReAcquisition(): boolean {
167
+ return (
168
+ this._mediaStreamTrack.readyState !== 'live' ||
169
+ this._mediaStreamTrack.muted ||
170
+ !this._mediaStreamTrack.enabled ||
171
+ this.reacquireTrack
172
+ );
173
+ }
174
+
175
+ protected async handleAppVisibilityChanged() {
176
+ await super.handleAppVisibilityChanged();
177
+ if (!isMobile()) return;
178
+ log.debug(`visibility changed, is in Background: ${this.isInBackground}`);
179
+
180
+ if (!this.isInBackground && this.needsReAcquisition) {
181
+ log.debug(`track needs to be reaquired, restarting ${this.source}`);
182
+ await this.restart();
183
+ this.reacquireTrack = false;
184
+ // Restore muted state if had to be restarted
185
+ this.setTrackMuted(this.wasMuted);
186
+ }
187
+
188
+ // store muted state each time app goes to background
189
+ if (this.isInBackground) {
190
+ this.wasMuted = this.isMuted;
191
+ }
192
+ }
193
+
194
+ private handleEnded = () => {
195
+ if (this.isInBackground) {
196
+ this.reacquireTrack = true;
197
+ }
198
+ this.emit(TrackEvent.Ended, this);
199
+ };
200
+
201
+ async pauseUpstream() {
202
+ if (this._isUpstreamPaused === true) {
203
+ return;
204
+ }
205
+ if (!this.sender) {
206
+ log.warn('unable to pause upstream for an unpublished track');
207
+ return;
208
+ }
209
+ this._isUpstreamPaused = true;
210
+ this.emit(TrackEvent.UpstreamPaused, this);
211
+ const emptyTrack =
212
+ this.kind === Track.Kind.Audio ? getEmptyAudioStreamTrack() : getEmptyVideoStreamTrack();
213
+ await this.sender.replaceTrack(emptyTrack);
214
+ }
215
+
216
+ async resumeUpstream() {
217
+ if (this._isUpstreamPaused === false) {
218
+ return;
219
+ }
220
+ if (!this.sender) {
221
+ log.warn('unable to resume upstream for an unpublished track');
222
+ return;
223
+ }
224
+ this._isUpstreamPaused = false;
225
+ this.emit(TrackEvent.UpstreamResumed, this);
226
+
227
+ await this.sender.replaceTrack(this._mediaStreamTrack);
228
+ }
229
+ }
@@ -0,0 +1,87 @@
1
+ import { TrackInfo } from '../../proto/livekit_models';
2
+ import { TrackEvent } from '../events';
3
+ import LocalAudioTrack from './LocalAudioTrack';
4
+ import LocalTrack from './LocalTrack';
5
+ import LocalVideoTrack from './LocalVideoTrack';
6
+ import { TrackPublishOptions } from './options';
7
+ import { Track } from './Track';
8
+ import { TrackPublication } from './TrackPublication';
9
+
10
+ export default class LocalTrackPublication extends TrackPublication {
11
+ track?: LocalTrack = undefined;
12
+
13
+ options?: TrackPublishOptions;
14
+
15
+ get isUpstreamPaused() {
16
+ return this.track?.isUpstreamPaused;
17
+ }
18
+
19
+ constructor(kind: Track.Kind, ti: TrackInfo, track?: LocalTrack) {
20
+ super(kind, ti.sid, ti.name);
21
+
22
+ this.updateInfo(ti);
23
+ this.setTrack(track);
24
+ }
25
+
26
+ setTrack(track?: Track) {
27
+ if (this.track) {
28
+ this.track.off(TrackEvent.Ended, this.handleTrackEnded);
29
+ }
30
+
31
+ super.setTrack(track);
32
+
33
+ if (track) {
34
+ track.on(TrackEvent.Ended, this.handleTrackEnded);
35
+ }
36
+ }
37
+
38
+ get isMuted(): boolean {
39
+ if (this.track) {
40
+ return this.track.isMuted;
41
+ }
42
+ return super.isMuted;
43
+ }
44
+
45
+ get audioTrack(): LocalAudioTrack | undefined {
46
+ return super.audioTrack as LocalAudioTrack | undefined;
47
+ }
48
+
49
+ get videoTrack(): LocalVideoTrack | undefined {
50
+ return super.videoTrack as LocalVideoTrack | undefined;
51
+ }
52
+
53
+ /**
54
+ * Mute the track associated with this publication
55
+ */
56
+ async mute() {
57
+ return this.track?.mute();
58
+ }
59
+
60
+ /**
61
+ * Unmute track associated with this publication
62
+ */
63
+ async unmute() {
64
+ return this.track?.unmute();
65
+ }
66
+
67
+ /**
68
+ * Pauses the media stream track associated with this publication from being sent to the server
69
+ * and signals "muted" event to other participants
70
+ * Useful if you want to pause the stream without pausing the local media stream track
71
+ */
72
+ async pauseUpstream() {
73
+ await this.track?.pauseUpstream();
74
+ }
75
+
76
+ /**
77
+ * Resumes sending the media stream track associated with this publication to the server after a call to [[pauseUpstream()]]
78
+ * and signals "unmuted" event to other participants (unless the track is explicitly muted)
79
+ */
80
+ async resumeUpstream() {
81
+ await this.track?.resumeUpstream();
82
+ }
83
+
84
+ handleTrackEnded = () => {
85
+ this.emit(TrackEvent.Ended);
86
+ };
87
+ }
@@ -0,0 +1,72 @@
1
+ import { VideoQuality } from '../../proto/livekit_models';
2
+ import { videoLayersFromEncodings } from './LocalVideoTrack';
3
+
4
+ describe('videoLayersFromEncodings', () => {
5
+ it('returns single layer for no encoding', () => {
6
+ const layers = videoLayersFromEncodings(640, 360);
7
+ expect(layers).toHaveLength(1);
8
+ expect(layers[0].quality).toBe(VideoQuality.HIGH);
9
+ expect(layers[0].width).toBe(640);
10
+ expect(layers[0].height).toBe(360);
11
+ });
12
+
13
+ it('returns single layer for explicit encoding', () => {
14
+ const layers = videoLayersFromEncodings(640, 360, [
15
+ {
16
+ maxBitrate: 200_000,
17
+ },
18
+ ]);
19
+ expect(layers).toHaveLength(1);
20
+ expect(layers[0].quality).toBe(VideoQuality.HIGH);
21
+ expect(layers[0].bitrate).toBe(200_000);
22
+ });
23
+
24
+ it('returns three layers for simulcast', () => {
25
+ const layers = videoLayersFromEncodings(1280, 720, [
26
+ {
27
+ scaleResolutionDownBy: 4,
28
+ rid: 'q',
29
+ maxBitrate: 125_000,
30
+ },
31
+ {
32
+ scaleResolutionDownBy: 2,
33
+ rid: 'h',
34
+ maxBitrate: 500_000,
35
+ },
36
+ {
37
+ rid: 'f',
38
+ maxBitrate: 1_200_000,
39
+ },
40
+ ]);
41
+
42
+ expect(layers).toHaveLength(3);
43
+ expect(layers[0].quality).toBe(VideoQuality.LOW);
44
+ expect(layers[0].width).toBe(320);
45
+ expect(layers[2].quality).toBe(VideoQuality.HIGH);
46
+ expect(layers[2].height).toBe(720);
47
+ });
48
+
49
+ it('handles portrait', () => {
50
+ const layers = videoLayersFromEncodings(720, 1280, [
51
+ {
52
+ scaleResolutionDownBy: 4,
53
+ rid: 'q',
54
+ maxBitrate: 125_000,
55
+ },
56
+ {
57
+ scaleResolutionDownBy: 2,
58
+ rid: 'h',
59
+ maxBitrate: 500_000,
60
+ },
61
+ {
62
+ rid: 'f',
63
+ maxBitrate: 1_200_000,
64
+ },
65
+ ]);
66
+ expect(layers).toHaveLength(3);
67
+ expect(layers[0].quality).toBe(VideoQuality.LOW);
68
+ expect(layers[0].height).toBe(320);
69
+ expect(layers[2].quality).toBe(VideoQuality.HIGH);
70
+ expect(layers[2].width).toBe(720);
71
+ });
72
+ });
@@ -0,0 +1,295 @@
1
+ import { SignalClient } from '../../api/SignalClient';
2
+ import log from '../../logger';
3
+ import { VideoLayer, VideoQuality } from '../../proto/livekit_models';
4
+ import { SubscribedQuality } from '../../proto/livekit_rtc';
5
+ import { computeBitrate, monitorFrequency, VideoSenderStats } from '../stats';
6
+ import { isFireFox, isMobile } from '../utils';
7
+ import LocalTrack from './LocalTrack';
8
+ import { VideoCaptureOptions } from './options';
9
+ import { Track } from './Track';
10
+ import { constraintsForOptions } from './utils';
11
+
12
+ export default class LocalVideoTrack extends LocalTrack {
13
+ /* internal */
14
+ signalClient?: SignalClient;
15
+
16
+ private prevStats?: Map<string, VideoSenderStats>;
17
+
18
+ private encodings?: RTCRtpEncodingParameters[];
19
+
20
+ constructor(mediaTrack: MediaStreamTrack, constraints?: MediaTrackConstraints) {
21
+ super(mediaTrack, Track.Kind.Video, constraints);
22
+ }
23
+
24
+ get isSimulcast(): boolean {
25
+ if (this.sender && this.sender.getParameters().encodings.length > 1) {
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+
31
+ /* @internal */
32
+ startMonitor(signalClient: SignalClient) {
33
+ this.signalClient = signalClient;
34
+ // save original encodings
35
+ const params = this.sender?.getParameters();
36
+ if (params) {
37
+ this.encodings = params.encodings;
38
+ }
39
+
40
+ setTimeout(() => {
41
+ this.monitorSender();
42
+ }, monitorFrequency);
43
+ }
44
+
45
+ stop() {
46
+ this.sender = undefined;
47
+ this._mediaStreamTrack.getConstraints();
48
+ super.stop();
49
+ }
50
+
51
+ async mute(): Promise<LocalVideoTrack> {
52
+ if (this.source === Track.Source.Camera) {
53
+ log.debug('stopping camera track');
54
+ // also stop the track, so that camera indicator is turned off
55
+ this._mediaStreamTrack.stop();
56
+ }
57
+ await super.mute();
58
+ return this;
59
+ }
60
+
61
+ async unmute(): Promise<LocalVideoTrack> {
62
+ if (this.source === Track.Source.Camera) {
63
+ log.debug('reacquiring camera track');
64
+ await this.restartTrack();
65
+ }
66
+ await super.unmute();
67
+ return this;
68
+ }
69
+
70
+ async getSenderStats(): Promise<VideoSenderStats[]> {
71
+ if (!this.sender) {
72
+ return [];
73
+ }
74
+
75
+ const items: VideoSenderStats[] = [];
76
+
77
+ const stats = await this.sender.getStats();
78
+ stats.forEach((v) => {
79
+ if (v.type === 'outbound-rtp') {
80
+ const vs: VideoSenderStats = {
81
+ type: 'video',
82
+ streamId: v.id,
83
+ frameHeight: v.frameHeight,
84
+ frameWidth: v.frameWidth,
85
+ firCount: v.firCount,
86
+ pliCount: v.pliCount,
87
+ nackCount: v.nackCount,
88
+ packetsSent: v.packetsSent,
89
+ bytesSent: v.bytesSent,
90
+ framesSent: v.framesSent,
91
+ timestamp: v.timestamp,
92
+ rid: v.rid ?? '',
93
+ retransmittedPacketsSent: v.retransmittedPacketsSent,
94
+ qualityLimitationReason: v.qualityLimitationReason,
95
+ qualityLimitationResolutionChanges: v.qualityLimitationResolutionChanges,
96
+ };
97
+
98
+ // locate the appropriate remote-inbound-rtp item
99
+ const r = stats.get(v.remoteId);
100
+ if (r) {
101
+ vs.jitter = r.jitter;
102
+ vs.packetsLost = r.packetsLost;
103
+ vs.roundTripTime = r.roundTripTime;
104
+ }
105
+
106
+ items.push(vs);
107
+ }
108
+ });
109
+
110
+ return items;
111
+ }
112
+
113
+ setPublishingQuality(maxQuality: VideoQuality) {
114
+ const qualities: SubscribedQuality[] = [];
115
+ for (let q = VideoQuality.LOW; q <= VideoQuality.HIGH; q += 1) {
116
+ qualities.push({
117
+ quality: q,
118
+ enabled: q <= maxQuality,
119
+ });
120
+ }
121
+ log.debug(`setting publishing quality. max quality ${maxQuality}`);
122
+ this.setPublishingLayers(qualities);
123
+ }
124
+
125
+ async setDeviceId(deviceId: string) {
126
+ if (this.constraints.deviceId === deviceId) {
127
+ return;
128
+ }
129
+ this.constraints.deviceId = deviceId;
130
+ // when video is muted, underlying media stream track is stopped and
131
+ // will be restarted later
132
+ if (!this.isMuted) {
133
+ await this.restartTrack();
134
+ }
135
+ }
136
+
137
+ async restartTrack(options?: VideoCaptureOptions) {
138
+ let constraints: MediaTrackConstraints | undefined;
139
+ if (options) {
140
+ const streamConstraints = constraintsForOptions({ video: options });
141
+ if (typeof streamConstraints.video !== 'boolean') {
142
+ constraints = streamConstraints.video;
143
+ }
144
+ }
145
+ await this.restart(constraints);
146
+ }
147
+
148
+ /**
149
+ * @internal
150
+ * Sets layers that should be publishing
151
+ */
152
+ async setPublishingLayers(qualities: SubscribedQuality[]) {
153
+ log.debug('setting publishing layers', qualities);
154
+ if (!this.sender || !this.encodings) {
155
+ return;
156
+ }
157
+ const params = this.sender.getParameters();
158
+ const { encodings } = params;
159
+ if (!encodings) {
160
+ return;
161
+ }
162
+
163
+ if (encodings.length !== this.encodings.length) {
164
+ log.warn('cannot set publishing layers, encodings mismatch');
165
+ return;
166
+ }
167
+
168
+ let hasChanged = false;
169
+ encodings.forEach((encoding, idx) => {
170
+ let rid = encoding.rid ?? '';
171
+ if (rid === '') {
172
+ rid = 'q';
173
+ }
174
+ const quality = videoQualityForRid(rid);
175
+ const subscribedQuality = qualities.find((q) => q.quality === quality);
176
+ if (!subscribedQuality) {
177
+ return;
178
+ }
179
+ if (encoding.active !== subscribedQuality.enabled) {
180
+ hasChanged = true;
181
+ encoding.active = subscribedQuality.enabled;
182
+ log.debug(
183
+ `setting layer ${subscribedQuality.quality} to ${
184
+ encoding.active ? 'enabled' : 'disabled'
185
+ }`,
186
+ );
187
+
188
+ // FireFox does not support setting encoding.active to false, so we
189
+ // have a workaround of lowering its bitrate and resolution to the min.
190
+ if (isFireFox()) {
191
+ if (subscribedQuality.enabled) {
192
+ encoding.scaleResolutionDownBy = this.encodings![idx].scaleResolutionDownBy;
193
+ encoding.maxBitrate = this.encodings![idx].maxBitrate;
194
+ /* @ts-ignore */
195
+ encoding.maxFrameRate = this.encodings![idx].maxFrameRate;
196
+ } else {
197
+ encoding.scaleResolutionDownBy = 4;
198
+ encoding.maxBitrate = 10;
199
+ /* @ts-ignore */
200
+ encoding.maxFrameRate = 2;
201
+ }
202
+ }
203
+ }
204
+ });
205
+
206
+ if (hasChanged) {
207
+ params.encodings = encodings;
208
+ await this.sender.setParameters(params);
209
+ }
210
+ }
211
+
212
+ private monitorSender = async () => {
213
+ if (!this.sender) {
214
+ this._currentBitrate = 0;
215
+ return;
216
+ }
217
+
218
+ let stats: VideoSenderStats[] | undefined;
219
+ try {
220
+ stats = await this.getSenderStats();
221
+ } catch (e) {
222
+ log.error('could not get audio sender stats', { error: e });
223
+ return;
224
+ }
225
+ const statsMap = new Map<string, VideoSenderStats>(stats.map((s) => [s.rid, s]));
226
+
227
+ if (this.prevStats) {
228
+ let totalBitrate = 0;
229
+ statsMap.forEach((s, key) => {
230
+ const prev = this.prevStats?.get(key);
231
+ totalBitrate += computeBitrate(s, prev);
232
+ });
233
+ this._currentBitrate = totalBitrate;
234
+ }
235
+
236
+ this.prevStats = statsMap;
237
+ setTimeout(() => {
238
+ this.monitorSender();
239
+ }, monitorFrequency);
240
+ };
241
+
242
+ protected async handleAppVisibilityChanged() {
243
+ await super.handleAppVisibilityChanged();
244
+ if (!isMobile()) return;
245
+ if (this.isInBackground && this.source === Track.Source.Camera) {
246
+ this._mediaStreamTrack.enabled = false;
247
+ }
248
+ }
249
+ }
250
+
251
+ export function videoQualityForRid(rid: string): VideoQuality {
252
+ switch (rid) {
253
+ case 'f':
254
+ return VideoQuality.HIGH;
255
+ case 'h':
256
+ return VideoQuality.MEDIUM;
257
+ case 'q':
258
+ return VideoQuality.LOW;
259
+ default:
260
+ return VideoQuality.UNRECOGNIZED;
261
+ }
262
+ }
263
+
264
+ export function videoLayersFromEncodings(
265
+ width: number,
266
+ height: number,
267
+ encodings?: RTCRtpEncodingParameters[],
268
+ ): VideoLayer[] {
269
+ // default to a single layer, HQ
270
+ if (!encodings) {
271
+ return [
272
+ {
273
+ quality: VideoQuality.HIGH,
274
+ width,
275
+ height,
276
+ bitrate: 0,
277
+ ssrc: 0,
278
+ },
279
+ ];
280
+ }
281
+ return encodings.map((encoding) => {
282
+ const scale = encoding.scaleResolutionDownBy ?? 1;
283
+ let quality = videoQualityForRid(encoding.rid ?? '');
284
+ if (quality === VideoQuality.UNRECOGNIZED && encodings.length === 1) {
285
+ quality = VideoQuality.HIGH;
286
+ }
287
+ return {
288
+ quality,
289
+ width: width / scale,
290
+ height: height / scale,
291
+ bitrate: encoding.maxBitrate ?? 0,
292
+ ssrc: 0,
293
+ };
294
+ });
295
+ }