livekit-client 0.15.3 → 0.16.2

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 (90) hide show
  1. package/dist/api/SignalClient.d.ts +6 -3
  2. package/dist/api/SignalClient.js +70 -28
  3. package/dist/api/SignalClient.js.map +1 -1
  4. package/dist/options.d.ts +5 -0
  5. package/dist/proto/livekit_models.d.ts +30 -0
  6. package/dist/proto/livekit_models.js +219 -1
  7. package/dist/proto/livekit_models.js.map +1 -1
  8. package/dist/proto/livekit_rtc.d.ts +15 -10
  9. package/dist/proto/livekit_rtc.js +36 -22
  10. package/dist/proto/livekit_rtc.js.map +1 -1
  11. package/dist/room/RTCEngine.d.ts +11 -2
  12. package/dist/room/RTCEngine.js +196 -44
  13. package/dist/room/RTCEngine.js.map +1 -1
  14. package/dist/room/Room.d.ts +7 -0
  15. package/dist/room/Room.js +70 -16
  16. package/dist/room/Room.js.map +1 -1
  17. package/dist/room/events.d.ts +5 -3
  18. package/dist/room/events.js +5 -3
  19. package/dist/room/events.js.map +1 -1
  20. package/dist/room/participant/LocalParticipant.d.ts +1 -2
  21. package/dist/room/participant/LocalParticipant.js +7 -6
  22. package/dist/room/participant/LocalParticipant.js.map +1 -1
  23. package/dist/room/participant/RemoteParticipant.js +3 -0
  24. package/dist/room/participant/RemoteParticipant.js.map +1 -1
  25. package/dist/room/participant/publishUtils.js +1 -1
  26. package/dist/room/participant/publishUtils.js.map +1 -1
  27. package/dist/room/participant/publishUtils.test.js +9 -0
  28. package/dist/room/participant/publishUtils.test.js.map +1 -1
  29. package/dist/room/track/LocalTrackPublication.d.ts +2 -0
  30. package/dist/room/track/LocalTrackPublication.js.map +1 -1
  31. package/dist/room/track/RemoteTrackPublication.d.ts +2 -1
  32. package/dist/room/track/RemoteTrackPublication.js +22 -8
  33. package/dist/room/track/RemoteTrackPublication.js.map +1 -1
  34. package/dist/room/track/RemoteVideoTrack.js +12 -7
  35. package/dist/room/track/RemoteVideoTrack.js.map +1 -1
  36. package/dist/room/track/Track.js +28 -20
  37. package/dist/room/track/Track.js.map +1 -1
  38. package/dist/room/track/create.js +5 -0
  39. package/dist/room/track/create.js.map +1 -1
  40. package/dist/room/utils.d.ts +3 -0
  41. package/dist/room/utils.js +16 -1
  42. package/dist/room/utils.js.map +1 -1
  43. package/dist/version.d.ts +2 -2
  44. package/dist/version.js +2 -2
  45. package/package.json +3 -3
  46. package/src/api/SignalClient.ts +444 -0
  47. package/src/connect.ts +100 -0
  48. package/src/index.ts +47 -0
  49. package/src/logger.ts +22 -0
  50. package/src/options.ts +152 -0
  51. package/src/proto/livekit_models.ts +1863 -0
  52. package/src/proto/livekit_rtc.ts +3415 -0
  53. package/src/room/DeviceManager.ts +57 -0
  54. package/src/room/PCTransport.ts +86 -0
  55. package/src/room/RTCEngine.ts +598 -0
  56. package/src/room/Room.ts +840 -0
  57. package/src/room/errors.ts +65 -0
  58. package/src/room/events.ts +398 -0
  59. package/src/room/participant/LocalParticipant.ts +685 -0
  60. package/src/room/participant/Participant.ts +214 -0
  61. package/src/room/participant/ParticipantTrackPermission.ts +32 -0
  62. package/src/room/participant/RemoteParticipant.ts +241 -0
  63. package/src/room/participant/publishUtils.test.ts +105 -0
  64. package/src/room/participant/publishUtils.ts +180 -0
  65. package/src/room/stats.ts +130 -0
  66. package/src/room/track/LocalAudioTrack.ts +112 -0
  67. package/src/room/track/LocalTrack.ts +124 -0
  68. package/src/room/track/LocalTrackPublication.ts +66 -0
  69. package/src/room/track/LocalVideoTrack.test.ts +70 -0
  70. package/src/room/track/LocalVideoTrack.ts +416 -0
  71. package/src/room/track/RemoteAudioTrack.ts +58 -0
  72. package/src/room/track/RemoteTrack.ts +59 -0
  73. package/src/room/track/RemoteTrackPublication.ts +198 -0
  74. package/src/room/track/RemoteVideoTrack.ts +220 -0
  75. package/src/room/track/Track.ts +307 -0
  76. package/src/room/track/TrackPublication.ts +120 -0
  77. package/src/room/track/create.ts +120 -0
  78. package/src/room/track/defaults.ts +23 -0
  79. package/src/room/track/options.ts +229 -0
  80. package/src/room/track/types.ts +8 -0
  81. package/src/room/track/utils.test.ts +93 -0
  82. package/src/room/track/utils.ts +76 -0
  83. package/src/room/utils.ts +62 -0
  84. package/src/version.ts +2 -0
  85. package/.github/workflows/publish.yaml +0 -55
  86. package/.github/workflows/test.yaml +0 -36
  87. package/example/index.html +0 -247
  88. package/example/sample.ts +0 -632
  89. package/example/styles.css +0 -144
  90. package/example/webpack.config.js +0 -33
@@ -0,0 +1,214 @@
1
+ import { EventEmitter } from 'events';
2
+ import { ConnectionQuality as ProtoQuality, ParticipantInfo } from '../../proto/livekit_models';
3
+ import { ParticipantEvent, TrackEvent } from '../events';
4
+ import { Track } from '../track/Track';
5
+ import { TrackPublication } from '../track/TrackPublication';
6
+
7
+ export enum ConnectionQuality {
8
+ Excellent = 'excellent',
9
+ Good = 'good',
10
+ Poor = 'poor',
11
+ Unknown = 'unknown',
12
+ }
13
+
14
+ function qualityFromProto(q: ProtoQuality): ConnectionQuality {
15
+ switch (q) {
16
+ case ProtoQuality.EXCELLENT:
17
+ return ConnectionQuality.Excellent;
18
+ case ProtoQuality.GOOD:
19
+ return ConnectionQuality.Good;
20
+ case ProtoQuality.POOR:
21
+ return ConnectionQuality.Poor;
22
+ default:
23
+ return ConnectionQuality.Unknown;
24
+ }
25
+ }
26
+
27
+ export default class Participant extends EventEmitter {
28
+ protected participantInfo?: ParticipantInfo;
29
+
30
+ audioTracks: Map<string, TrackPublication>;
31
+
32
+ videoTracks: Map<string, TrackPublication>;
33
+
34
+ /** map of track sid => all published tracks */
35
+ tracks: Map<string, TrackPublication>;
36
+
37
+ /** audio level between 0-1.0, 1 being loudest, 0 being softest */
38
+ audioLevel: number = 0;
39
+
40
+ /** if participant is currently speaking */
41
+ isSpeaking: boolean = false;
42
+
43
+ /** server assigned unique id */
44
+ sid: string;
45
+
46
+ /** client assigned identity, encoded in JWT token */
47
+ identity: string;
48
+
49
+ /** client assigned display name, encoded in JWT token */
50
+ name?: string;
51
+
52
+ /** client metadata, opaque to livekit */
53
+ metadata?: string;
54
+
55
+ lastSpokeAt?: Date | undefined;
56
+
57
+ private _connectionQuality: ConnectionQuality = ConnectionQuality.Unknown;
58
+
59
+ /** @internal */
60
+ constructor(sid: string, identity: string) {
61
+ super();
62
+ this.sid = sid;
63
+ this.identity = identity;
64
+ this.audioTracks = new Map();
65
+ this.videoTracks = new Map();
66
+ this.tracks = new Map();
67
+ }
68
+
69
+ getTracks(): TrackPublication[] {
70
+ return Array.from(this.tracks.values());
71
+ }
72
+
73
+ /**
74
+ * Finds the first track that matches the source filter, for example, getting
75
+ * the user's camera track with getTrackBySource(Track.Source.Camera).
76
+ * @param source
77
+ * @returns
78
+ */
79
+ getTrack(source: Track.Source): TrackPublication | undefined {
80
+ if (source === Track.Source.Unknown) {
81
+ return;
82
+ }
83
+ for (const [, pub] of this.tracks) {
84
+ if (pub.source === source) {
85
+ return pub;
86
+ }
87
+ if (pub.source === Track.Source.Unknown) {
88
+ if (source === Track.Source.Microphone && pub.kind === Track.Kind.Audio && pub.trackName !== 'screen') {
89
+ return pub;
90
+ }
91
+ if (source === Track.Source.Camera && pub.kind === Track.Kind.Video && pub.trackName !== 'screen') {
92
+ return pub;
93
+ }
94
+ if (source === Track.Source.ScreenShare && pub.kind === Track.Kind.Video && pub.trackName === 'screen') {
95
+ return pub;
96
+ }
97
+ if (source === Track.Source.ScreenShareAudio && pub.kind === Track.Kind.Audio && pub.trackName === 'screen') {
98
+ return pub;
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Finds the first track that matches the track's name.
106
+ * @param name
107
+ * @returns
108
+ */
109
+ getTrackByName(name: string): TrackPublication | undefined {
110
+ for (const [, pub] of this.tracks) {
111
+ if (pub.trackName === name) {
112
+ return pub;
113
+ }
114
+ }
115
+ }
116
+
117
+ get connectionQuality(): ConnectionQuality {
118
+ return this._connectionQuality;
119
+ }
120
+
121
+ get isCameraEnabled(): boolean {
122
+ const track = this.getTrack(Track.Source.Camera);
123
+ return !(track?.isMuted ?? true);
124
+ }
125
+
126
+ get isMicrophoneEnabled(): boolean {
127
+ const track = this.getTrack(Track.Source.Microphone);
128
+ return !(track?.isMuted ?? true);
129
+ }
130
+
131
+ get isScreenShareEnabled(): boolean {
132
+ const track = this.getTrack(Track.Source.ScreenShare);
133
+ return !!track;
134
+ }
135
+
136
+ /** when participant joined the room */
137
+ get joinedAt(): Date | undefined {
138
+ if (this.participantInfo) {
139
+ return new Date(this.participantInfo.joinedAt * 1000);
140
+ }
141
+ return new Date();
142
+ }
143
+
144
+ /** @internal */
145
+ updateInfo(info: ParticipantInfo) {
146
+ this.identity = info.identity;
147
+ this.sid = info.sid;
148
+ this.name = info.name;
149
+ this.setMetadata(info.metadata);
150
+ // set this last so setMetadata can detect changes
151
+ this.participantInfo = info;
152
+ }
153
+
154
+ /** @internal */
155
+ setMetadata(md: string) {
156
+ const changed = !this.participantInfo || this.participantInfo.metadata !== md;
157
+ const prevMetadata = this.metadata;
158
+ this.metadata = md;
159
+
160
+ if (changed) {
161
+ this.emit(ParticipantEvent.MetadataChanged, prevMetadata, this);
162
+ this.emit(ParticipantEvent.ParticipantMetadataChanged, prevMetadata, this);
163
+ }
164
+ }
165
+
166
+ /** @internal */
167
+ setIsSpeaking(speaking: boolean) {
168
+ if (speaking === this.isSpeaking) {
169
+ return;
170
+ }
171
+ this.isSpeaking = speaking;
172
+ if (speaking) {
173
+ this.lastSpokeAt = new Date();
174
+ }
175
+ this.emit(ParticipantEvent.IsSpeakingChanged, speaking);
176
+ }
177
+
178
+ /** @internal */
179
+ setConnectionQuality(q: ProtoQuality) {
180
+ const prevQuality = this._connectionQuality;
181
+ this._connectionQuality = qualityFromProto(q);
182
+ if (prevQuality !== this._connectionQuality) {
183
+ this.emit(ParticipantEvent.ConnectionQualityChanged, this._connectionQuality);
184
+ }
185
+ }
186
+
187
+ protected addTrackPublication(publication: TrackPublication) {
188
+ // forward publication driven events
189
+ publication.on(TrackEvent.Muted, () => {
190
+ this.emit(ParticipantEvent.TrackMuted, publication);
191
+ });
192
+
193
+ publication.on(TrackEvent.Unmuted, () => {
194
+ this.emit(ParticipantEvent.TrackUnmuted, publication);
195
+ });
196
+
197
+ const pub = publication;
198
+ if (pub.track) {
199
+ pub.track.sid = publication.trackSid;
200
+ }
201
+
202
+ this.tracks.set(publication.trackSid, publication);
203
+ switch (publication.kind) {
204
+ case Track.Kind.Audio:
205
+ this.audioTracks.set(publication.trackSid, publication);
206
+ break;
207
+ case Track.Kind.Video:
208
+ this.videoTracks.set(publication.trackSid, publication);
209
+ break;
210
+ default:
211
+ break;
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,32 @@
1
+ import { TrackPermission } from '../../proto/livekit_rtc';
2
+
3
+ export interface ParticipantTrackPermission {
4
+ /**
5
+ * The participant id this permission applies to.
6
+ */
7
+ participantSid: string;
8
+
9
+ /**
10
+ * Grant permission to all all tracks. Takes precedence over allowedTrackSids.
11
+ * false if unset.
12
+ */
13
+ allowAll?: boolean;
14
+
15
+ /**
16
+ * The list of track ids that the target participant can subscribe to.
17
+ * When unset, it'll allow all tracks to be subscribed by the participant.
18
+ * When empty, this participant is disallowed from subscribing to any tracks.
19
+ */
20
+ allowedTrackSids?: string[];
21
+ }
22
+
23
+ export function trackPermissionToProto(perms: ParticipantTrackPermission): TrackPermission {
24
+ if (!perms.participantSid) {
25
+ throw new Error('Invalid track permission, missing participantSid');
26
+ }
27
+ return {
28
+ participantSid: perms.participantSid,
29
+ allTracks: perms.allowAll ?? false,
30
+ trackSids: perms.allowedTrackSids || [],
31
+ };
32
+ }
@@ -0,0 +1,241 @@
1
+ import { SignalClient } from '../../api/SignalClient';
2
+ import log from '../../logger';
3
+ import { ParticipantInfo } from '../../proto/livekit_models';
4
+ import {
5
+ UpdateSubscription,
6
+ UpdateTrackSettings,
7
+ } from '../../proto/livekit_rtc';
8
+ import { ParticipantEvent, TrackEvent } from '../events';
9
+ import RemoteAudioTrack from '../track/RemoteAudioTrack';
10
+ import RemoteTrackPublication from '../track/RemoteTrackPublication';
11
+ import RemoteVideoTrack from '../track/RemoteVideoTrack';
12
+ import { Track } from '../track/Track';
13
+ import { TrackPublication } from '../track/TrackPublication';
14
+ import { RemoteTrack } from '../track/types';
15
+ import Participant from './Participant';
16
+
17
+ export default class RemoteParticipant extends Participant {
18
+ audioTracks: Map<string, RemoteTrackPublication>;
19
+
20
+ videoTracks: Map<string, RemoteTrackPublication>;
21
+
22
+ tracks: Map<string, RemoteTrackPublication>;
23
+
24
+ signalClient: SignalClient;
25
+
26
+ /** @internal */
27
+ static fromParticipantInfo(
28
+ signalClient: SignalClient,
29
+ pi: ParticipantInfo,
30
+ ): RemoteParticipant {
31
+ const rp = new RemoteParticipant(signalClient, pi.sid, pi.identity);
32
+ rp.updateInfo(pi);
33
+ return rp;
34
+ }
35
+
36
+ /** @internal */
37
+ constructor(signalClient: SignalClient, id: string, name?: string) {
38
+ super(id, name || '');
39
+ this.signalClient = signalClient;
40
+ this.tracks = new Map();
41
+ this.audioTracks = new Map();
42
+ this.videoTracks = new Map();
43
+ }
44
+
45
+ protected addTrackPublication(publication: TrackPublication) {
46
+ super.addTrackPublication(publication);
47
+
48
+ // register action events
49
+ publication.on(
50
+ TrackEvent.UpdateSettings,
51
+ (settings: UpdateTrackSettings) => {
52
+ this.signalClient.sendUpdateTrackSettings(settings);
53
+ },
54
+ );
55
+ publication.on(TrackEvent.UpdateSubscription, (sub: UpdateSubscription) => {
56
+ sub.participantTracks.forEach((pt) => {
57
+ pt.participantSid = this.sid;
58
+ });
59
+ this.signalClient.sendUpdateSubscription(sub);
60
+ });
61
+ publication.on(TrackEvent.Ended, (track: RemoteTrack) => {
62
+ this.emit(ParticipantEvent.TrackUnsubscribed, track, publication);
63
+ });
64
+ }
65
+
66
+ getTrack(source: Track.Source): RemoteTrackPublication | undefined {
67
+ const track = super.getTrack(source);
68
+ if (track) {
69
+ return track as RemoteTrackPublication;
70
+ }
71
+ }
72
+
73
+ getTrackByName(name: string): RemoteTrackPublication | undefined {
74
+ const track = super.getTrackByName(name);
75
+ if (track) {
76
+ return track as RemoteTrackPublication;
77
+ }
78
+ }
79
+
80
+ /** @internal */
81
+ addSubscribedMediaTrack(
82
+ mediaTrack: MediaStreamTrack,
83
+ sid: Track.SID,
84
+ mediaStream: MediaStream,
85
+ receiver?: RTCRtpReceiver,
86
+ adaptiveStream?: boolean,
87
+ triesLeft?: number,
88
+ ) {
89
+ // find the track publication
90
+ // it's possible for the media track to arrive before participant info
91
+ let publication = this.getTrackPublication(sid);
92
+
93
+ // it's also possible that the browser didn't honor our original track id
94
+ // FireFox would use its own local uuid instead of server track id
95
+ if (!publication) {
96
+ if (!sid.startsWith('TR')) {
97
+ // find the first track that matches type
98
+ this.tracks.forEach((p) => {
99
+ if (!publication && mediaTrack.kind === p.kind.toString()) {
100
+ publication = p;
101
+ }
102
+ });
103
+ }
104
+ }
105
+
106
+ // when we couldn't locate the track, it's possible that the metadata hasn't
107
+ // yet arrived. Wait a bit longer for it to arrive, or fire an error
108
+ if (!publication) {
109
+ if (triesLeft === 0) {
110
+ log.error('could not find published track', this.sid, sid);
111
+ this.emit(ParticipantEvent.TrackSubscriptionFailed, sid);
112
+ return;
113
+ }
114
+
115
+ if (triesLeft === undefined) triesLeft = 20;
116
+ setTimeout(() => {
117
+ this.addSubscribedMediaTrack(mediaTrack, sid, mediaStream,
118
+ receiver, adaptiveStream, triesLeft! - 1);
119
+ }, 150);
120
+ return;
121
+ }
122
+
123
+ const isVideo = mediaTrack.kind === 'video';
124
+ let track: RemoteTrack;
125
+ if (isVideo) {
126
+ track = new RemoteVideoTrack(mediaTrack, sid, receiver, adaptiveStream);
127
+ } else {
128
+ track = new RemoteAudioTrack(mediaTrack, sid, receiver);
129
+ }
130
+
131
+ // set track info
132
+ track.source = publication.source;
133
+ // keep publication's muted status
134
+ track.isMuted = publication.isMuted;
135
+ track.setMediaStream(mediaStream);
136
+ track.start();
137
+
138
+ publication.setTrack(track);
139
+
140
+ this.emit(ParticipantEvent.TrackSubscribed, track, publication);
141
+
142
+ return publication;
143
+ }
144
+
145
+ /** @internal */
146
+ get hasMetadata(): boolean {
147
+ return !!this.participantInfo;
148
+ }
149
+
150
+ getTrackPublication(sid: Track.SID): RemoteTrackPublication | undefined {
151
+ return this.tracks.get(sid);
152
+ }
153
+
154
+ /** @internal */
155
+ updateInfo(info: ParticipantInfo) {
156
+ const alreadyHasMetadata = this.hasMetadata;
157
+
158
+ super.updateInfo(info);
159
+
160
+ // we are getting a list of all available tracks, reconcile in here
161
+ // and send out events for changes
162
+
163
+ // reconcile track publications, publish events only if metadata is already there
164
+ // i.e. changes since the local participant has joined
165
+ const validTracks = new Map<string, RemoteTrackPublication>();
166
+ const newTracks = new Map<string, RemoteTrackPublication>();
167
+
168
+ info.tracks.forEach((ti) => {
169
+ let publication = this.getTrackPublication(ti.sid);
170
+ if (!publication) {
171
+ // new publication
172
+ const kind = Track.kindFromProto(ti.type);
173
+ if (!kind) {
174
+ return;
175
+ }
176
+ publication = new RemoteTrackPublication(kind, ti.sid, ti.name);
177
+ publication.updateInfo(ti);
178
+ newTracks.set(ti.sid, publication);
179
+ this.addTrackPublication(publication);
180
+ } else {
181
+ publication.updateInfo(ti);
182
+ }
183
+ validTracks.set(ti.sid, publication);
184
+ });
185
+
186
+ // send new tracks
187
+ if (alreadyHasMetadata) {
188
+ newTracks.forEach((publication) => {
189
+ this.emit(ParticipantEvent.TrackPublished, publication);
190
+ });
191
+ }
192
+
193
+ // detect removed tracks
194
+ this.tracks.forEach((publication) => {
195
+ if (!validTracks.has(publication.trackSid)) {
196
+ this.unpublishTrack(publication.trackSid, true);
197
+ }
198
+ });
199
+ }
200
+
201
+ /** @internal */
202
+ unpublishTrack(sid: Track.SID, sendUnpublish?: boolean) {
203
+ const publication = <RemoteTrackPublication> this.tracks.get(sid);
204
+ if (!publication) {
205
+ return;
206
+ }
207
+
208
+ this.tracks.delete(sid);
209
+
210
+ // remove from the right type map
211
+ switch (publication.kind) {
212
+ case Track.Kind.Audio:
213
+ this.audioTracks.delete(sid);
214
+ break;
215
+ case Track.Kind.Video:
216
+ this.videoTracks.delete(sid);
217
+ break;
218
+ default:
219
+ break;
220
+ }
221
+
222
+ // also send unsubscribe, if track is actively subscribed
223
+ const { track } = publication;
224
+ if (track) {
225
+ const { isSubscribed } = publication;
226
+ track.stop();
227
+ publication.setTrack(undefined);
228
+ // always send unsubscribed, since apps may rely on this
229
+ if (isSubscribed) {
230
+ this.emit(ParticipantEvent.TrackUnsubscribed, track, publication);
231
+ }
232
+ }
233
+ if (sendUnpublish) { this.emit(ParticipantEvent.TrackUnpublished, publication); }
234
+ }
235
+
236
+ /** @internal */
237
+ emit(event: string | symbol, ...args: any[]): boolean {
238
+ log.trace('participant event', this.sid, event, ...args);
239
+ return super.emit(event, ...args);
240
+ }
241
+ }
@@ -0,0 +1,105 @@
1
+ import { VideoPresets, VideoPresets43 } from '../track/options';
2
+ import {
3
+ computeVideoEncodings,
4
+ determineAppropriateEncoding,
5
+ presets169,
6
+ presets43,
7
+ presetsForResolution,
8
+ presetsScreenShare,
9
+ } from './publishUtils';
10
+
11
+ describe('presetsForResolution', () => {
12
+ it('handles screenshare', () => {
13
+ expect(presetsForResolution(true, 600, 300)).toEqual(presetsScreenShare);
14
+ });
15
+
16
+ it('handles landscape', () => {
17
+ expect(presetsForResolution(false, 600, 300)).toEqual(presets169);
18
+ expect(presetsForResolution(false, 500, 500)).toEqual(presets43);
19
+ });
20
+
21
+ it('handles portrait', () => {
22
+ expect(presetsForResolution(false, 300, 600)).toEqual(presets169);
23
+ expect(presetsForResolution(false, 500, 500)).toEqual(presets43);
24
+ });
25
+ });
26
+
27
+ describe('determineAppropriateEncoding', () => {
28
+ it('uses higher encoding', () => {
29
+ expect(determineAppropriateEncoding(false, 600, 300))
30
+ .toEqual(VideoPresets.vga.encoding);
31
+ });
32
+
33
+ it('handles portrait', () => {
34
+ expect(determineAppropriateEncoding(false, 300, 600))
35
+ .toEqual(VideoPresets.vga.encoding);
36
+ });
37
+ });
38
+
39
+ describe('computeVideoEncodings', () => {
40
+ it('handles non-simulcast', () => {
41
+ const encodings = computeVideoEncodings(false, 640, 480, {
42
+ simulcast: false,
43
+ });
44
+ expect(encodings).toEqual([{}]);
45
+ });
46
+
47
+ it('respects client defined bitrate', () => {
48
+ const encodings = computeVideoEncodings(false, 640, 480, {
49
+ simulcast: false,
50
+ videoEncoding: {
51
+ maxBitrate: 1024,
52
+ },
53
+ });
54
+ expect(encodings).toHaveLength(1);
55
+ expect(encodings![0].maxBitrate).toBe(1024);
56
+ });
57
+
58
+ it('returns three encodings for high-res simulcast', () => {
59
+ const encodings = computeVideoEncodings(false, 960, 540, {
60
+ simulcast: true,
61
+ });
62
+ expect(encodings).toHaveLength(3);
63
+
64
+ // ensure they are what we expect
65
+ expect(encodings![0].rid).toBe('q');
66
+ expect(encodings![0].maxBitrate).toBe(VideoPresets.qvga.encoding.maxBitrate);
67
+ expect(encodings![0].scaleResolutionDownBy).toBe(3);
68
+ expect(encodings![1].rid).toBe('h');
69
+ expect(encodings![1].scaleResolutionDownBy).toBe(1.5);
70
+ expect(encodings![2].rid).toBe('f');
71
+ });
72
+
73
+ it('handles portrait simulcast', () => {
74
+ const encodings = computeVideoEncodings(false, 540, 960, {
75
+ simulcast: true,
76
+ });
77
+ expect(encodings).toHaveLength(3);
78
+ expect(encodings![0].scaleResolutionDownBy).toBe(3);
79
+ expect(encodings![1].scaleResolutionDownBy).toBe(1.5);
80
+ expect(encodings![2].maxBitrate).toBe(VideoPresets.qhd.encoding.maxBitrate);
81
+ });
82
+
83
+ it('returns two encodings for lower-res simulcast', () => {
84
+ const encodings = computeVideoEncodings(false, 640, 360, {
85
+ simulcast: true,
86
+ });
87
+ expect(encodings).toHaveLength(2);
88
+
89
+ // ensure they are what we expect
90
+ expect(encodings![0].rid).toBe('q');
91
+ expect(encodings![0].maxBitrate).toBe(VideoPresets.qvga.encoding.maxBitrate);
92
+ expect(encodings![1].rid).toBe('h');
93
+ expect(encodings![1].maxBitrate).toBe(VideoPresets.vga.encoding.maxBitrate);
94
+ });
95
+
96
+ it('respects provided min resolution', () => {
97
+ const encodings = computeVideoEncodings(false, 100, 120, {
98
+ simulcast: true,
99
+ });
100
+ expect(encodings).toHaveLength(1);
101
+ expect(encodings![0].rid).toBe('q');
102
+ expect(encodings![0].maxBitrate).toBe(VideoPresets43.qvga.encoding.maxBitrate);
103
+ expect(encodings![0].scaleResolutionDownBy).toBe(1);
104
+ });
105
+ });