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,198 @@
1
+ import log from '../../logger';
2
+ import { TrackInfo, VideoQuality } from '../../proto/livekit_models';
3
+ import {
4
+ UpdateSubscription,
5
+ UpdateTrackSettings,
6
+ } from '../../proto/livekit_rtc';
7
+ import { TrackEvent } from '../events';
8
+ import RemoteVideoTrack from './RemoteVideoTrack';
9
+ import { Track } from './Track';
10
+ import { TrackPublication } from './TrackPublication';
11
+ import { RemoteTrack } from './types';
12
+
13
+ export default class RemoteTrackPublication extends TrackPublication {
14
+ track?: RemoteTrack;
15
+
16
+ /** @internal */
17
+ _allowed = true;
18
+
19
+ // keeps track of client's desire to subscribe to a track
20
+ protected subscribed?: boolean;
21
+
22
+ protected disabled: boolean = false;
23
+
24
+ protected currentVideoQuality?: VideoQuality = VideoQuality.HIGH;
25
+
26
+ protected videoDimensions?: Track.Dimensions;
27
+
28
+ /**
29
+ * Subscribe or unsubscribe to this remote track
30
+ * @param subscribed true to subscribe to a track, false to unsubscribe
31
+ */
32
+ setSubscribed(subscribed: boolean) {
33
+ this.subscribed = subscribed;
34
+
35
+ const sub: UpdateSubscription = {
36
+ trackSids: [this.trackSid],
37
+ subscribe: this.subscribed,
38
+ participantTracks: [{
39
+ // sending an empty participant id since TrackPublication doesn't keep it
40
+ // this is filled in by the participant that receives this message
41
+ participantSid: '',
42
+ trackSids: [this.trackSid],
43
+ }],
44
+ };
45
+ this.emit(TrackEvent.UpdateSubscription, sub);
46
+ }
47
+
48
+ get subscriptionStatus(): TrackPublication.SubscriptionStatus {
49
+ if (this.subscribed === false || !super.isSubscribed) {
50
+ return TrackPublication.SubscriptionStatus.Unsubscribed;
51
+ }
52
+ if (!this._allowed) {
53
+ return TrackPublication.SubscriptionStatus.NotAllowed;
54
+ }
55
+ return TrackPublication.SubscriptionStatus.Subscribed;
56
+ }
57
+
58
+ /**
59
+ * Returns true if track is subscribed, and ready for playback
60
+ */
61
+ get isSubscribed(): boolean {
62
+ if (this.subscribed === false) {
63
+ return false;
64
+ }
65
+ if (!this._allowed) {
66
+ return false;
67
+ }
68
+ return super.isSubscribed;
69
+ }
70
+
71
+ get isEnabled(): boolean {
72
+ return !this.disabled;
73
+ }
74
+
75
+ /**
76
+ * disable server from sending down data for this track. this is useful when
77
+ * the participant is off screen, you may disable streaming down their video
78
+ * to reduce bandwidth requirements
79
+ * @param enabled
80
+ */
81
+ setEnabled(enabled: boolean) {
82
+ if (!this.isManualOperationAllowed() || this.disabled === !enabled) {
83
+ return;
84
+ }
85
+ this.disabled = !enabled;
86
+
87
+ this.emitTrackUpdate();
88
+ }
89
+
90
+ /**
91
+ * for tracks that support simulcasting, adjust subscribed quality
92
+ *
93
+ * This indicates the highest quality the client can accept. if network
94
+ * bandwidth does not allow, server will automatically reduce quality to
95
+ * optimize for uninterrupted video
96
+ */
97
+ setVideoQuality(quality: VideoQuality) {
98
+ if (!this.isManualOperationAllowed() || this.currentVideoQuality === quality) {
99
+ return;
100
+ }
101
+ this.currentVideoQuality = quality;
102
+ this.videoDimensions = undefined;
103
+
104
+ this.emitTrackUpdate();
105
+ }
106
+
107
+ setVideoDimensions(dimensions: Track.Dimensions) {
108
+ if (!this.isManualOperationAllowed()) {
109
+ return;
110
+ }
111
+ if (this.videoDimensions?.width === dimensions.width
112
+ && this.videoDimensions?.height === dimensions.height) {
113
+ return;
114
+ }
115
+ if (this.track instanceof RemoteVideoTrack) { this.videoDimensions = dimensions; }
116
+ this.currentVideoQuality = undefined;
117
+
118
+ this.emitTrackUpdate();
119
+ }
120
+
121
+ get videoQuality(): VideoQuality | undefined {
122
+ return this.currentVideoQuality;
123
+ }
124
+
125
+ setTrack(track?: Track) {
126
+ if (this.track) {
127
+ // unregister listener
128
+ this.track.off(TrackEvent.VideoDimensionsChanged, this.handleVideoDimensionsChange);
129
+ this.track.off(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
130
+ this.track.off(TrackEvent.Ended, this.handleEnded);
131
+ }
132
+ super.setTrack(track);
133
+ if (track) {
134
+ track.sid = this.trackSid;
135
+ track.on(TrackEvent.VideoDimensionsChanged, this.handleVideoDimensionsChange);
136
+ track.on(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
137
+ track.on(TrackEvent.Ended, this.handleEnded);
138
+ }
139
+ }
140
+
141
+ /** @internal */
142
+ updateInfo(info: TrackInfo) {
143
+ super.updateInfo(info);
144
+ this.metadataMuted = info.muted;
145
+ this.track?.setMuted(info.muted);
146
+ }
147
+
148
+ private isManualOperationAllowed(): boolean {
149
+ if (this.isAdaptiveStream) {
150
+ log.warn('adaptive stream is enabled, cannot change track settings', this.trackSid);
151
+ return false;
152
+ }
153
+ if (!this.isSubscribed) {
154
+ log.warn('cannot update track settings when not subscribed', this.trackSid);
155
+ return false;
156
+ }
157
+ return true;
158
+ }
159
+
160
+ protected handleEnded = (track: RemoteTrack) => {
161
+ this.emit(TrackEvent.Ended, track);
162
+ };
163
+
164
+ protected get isAdaptiveStream(): boolean {
165
+ return this.track instanceof RemoteVideoTrack && this.track.isAdaptiveStream;
166
+ }
167
+
168
+ protected handleVisibilityChange = (visible: boolean) => {
169
+ log.debug('adaptivestream video visibility', this.trackSid, `visible=${visible}`);
170
+ this.disabled = !visible;
171
+ this.emitTrackUpdate();
172
+ };
173
+
174
+ protected handleVideoDimensionsChange = (dimensions: Track.Dimensions) => {
175
+ log.debug('adaptivestream video dimensions', this.trackSid, `${dimensions.width}x${dimensions.height}`);
176
+ this.videoDimensions = dimensions;
177
+ this.emitTrackUpdate();
178
+ };
179
+
180
+ /* @internal */
181
+ emitTrackUpdate() {
182
+ const settings: UpdateTrackSettings = UpdateTrackSettings.fromPartial({
183
+ trackSids: [this.trackSid],
184
+ disabled: this.disabled,
185
+ });
186
+ if (this.videoDimensions) {
187
+ settings.width = this.videoDimensions.width;
188
+ settings.height = this.videoDimensions.height;
189
+ } else if (this.currentVideoQuality !== undefined) {
190
+ settings.quality = this.currentVideoQuality;
191
+ } else {
192
+ // defaults to high quality
193
+ settings.quality = VideoQuality.HIGH;
194
+ }
195
+
196
+ this.emit(TrackEvent.UpdateSettings, settings);
197
+ }
198
+ }
@@ -0,0 +1,220 @@
1
+ import { debounce } from 'ts-debounce';
2
+ import { TrackEvent } from '../events';
3
+ import { computeBitrate, monitorFrequency, VideoReceiverStats } from '../stats';
4
+ import { getIntersectionObserver, getResizeObserver, ObservableMediaElement } from '../utils';
5
+ import RemoteTrack from './RemoteTrack';
6
+ import { attachToElement, detachTrack, Track } from './Track';
7
+
8
+ const REACTION_DELAY = 100;
9
+
10
+ export default class RemoteVideoTrack extends RemoteTrack {
11
+ /** @internal */
12
+ receiver?: RTCRtpReceiver;
13
+
14
+ private prevStats?: VideoReceiverStats;
15
+
16
+ private elementInfos: ElementInfo[] = [];
17
+
18
+ private adaptiveStream?: boolean;
19
+
20
+ private lastVisible?: boolean;
21
+
22
+ private lastDimensions?: Track.Dimensions;
23
+
24
+ constructor(
25
+ mediaTrack: MediaStreamTrack,
26
+ sid: string,
27
+ receiver?: RTCRtpReceiver,
28
+ adaptiveStream?: boolean,
29
+ ) {
30
+ super(mediaTrack, sid, Track.Kind.Video, receiver);
31
+ this.adaptiveStream = adaptiveStream;
32
+ }
33
+
34
+ get isAdaptiveStream(): boolean {
35
+ return this.adaptiveStream ?? false;
36
+ }
37
+
38
+ /** @internal */
39
+ setMuted(muted: boolean) {
40
+ super.setMuted(muted);
41
+
42
+ this.attachedElements.forEach((element) => {
43
+ // detach or attach
44
+ if (muted) {
45
+ detachTrack(this.mediaStreamTrack, element);
46
+ } else {
47
+ attachToElement(this.mediaStreamTrack, element);
48
+ }
49
+ });
50
+ }
51
+
52
+ attach(): HTMLMediaElement;
53
+ attach(element: HTMLMediaElement): HTMLMediaElement;
54
+ attach(element?: HTMLMediaElement): HTMLMediaElement {
55
+ if (!element) {
56
+ element = super.attach();
57
+ } else {
58
+ super.attach(element);
59
+ }
60
+
61
+ // It's possible attach is called multiple times on an element. When that's
62
+ // the case, we'd want to avoid adding duplicate elementInfos
63
+ if (this.adaptiveStream
64
+ && this.elementInfos.find((info) => info.element === element) === undefined
65
+ ) {
66
+ this.elementInfos.push({
67
+ element,
68
+ visible: true, // default visible
69
+ });
70
+
71
+ (element as ObservableMediaElement)
72
+ .handleResize = this.debouncedHandleResize;
73
+ (element as ObservableMediaElement)
74
+ .handleVisibilityChanged = this.handleVisibilityChanged;
75
+
76
+ getIntersectionObserver().observe(element);
77
+ getResizeObserver().observe(element);
78
+
79
+ // trigger the first resize update cycle
80
+ // if the tab is backgrounded, the initial resize event does not fire until
81
+ // the tab comes into focus for the first time.
82
+ this.debouncedHandleResize();
83
+ }
84
+ return element;
85
+ }
86
+
87
+ detach(): HTMLMediaElement[];
88
+ detach(element: HTMLMediaElement): HTMLMediaElement;
89
+ detach(element?: HTMLMediaElement): HTMLMediaElement | HTMLMediaElement[] {
90
+ let detachedElements: HTMLMediaElement[] = [];
91
+ if (element) {
92
+ this.stopObservingElement(element);
93
+ return super.detach(element);
94
+ }
95
+ detachedElements = super.detach();
96
+
97
+ for (const e of detachedElements) {
98
+ this.stopObservingElement(e);
99
+ }
100
+
101
+ return detachedElements;
102
+ }
103
+
104
+ protected monitorReceiver = async () => {
105
+ if (!this.receiver) {
106
+ this._currentBitrate = 0;
107
+ return;
108
+ }
109
+ const stats = await this.getReceiverStats();
110
+
111
+ if (stats && this.prevStats && this.receiver) {
112
+ this._currentBitrate = computeBitrate(stats, this.prevStats);
113
+ }
114
+
115
+ this.prevStats = stats;
116
+ setTimeout(() => {
117
+ this.monitorReceiver();
118
+ }, monitorFrequency);
119
+ };
120
+
121
+ private async getReceiverStats(): Promise<VideoReceiverStats | undefined> {
122
+ if (!this.receiver) {
123
+ return;
124
+ }
125
+
126
+ const stats = await this.receiver.getStats();
127
+ let receiverStats: VideoReceiverStats | undefined;
128
+ stats.forEach((v) => {
129
+ if (v.type === 'inbound-rtp') {
130
+ receiverStats = {
131
+ type: 'video',
132
+ framesDecoded: v.framesDecoded,
133
+ framesDropped: v.framesDropped,
134
+ framesReceived: v.framesReceived,
135
+ packetsReceived: v.packetsReceived,
136
+ packetsLost: v.packetsLost,
137
+ frameWidth: v.frameWidth,
138
+ frameHeight: v.frameHeight,
139
+ pliCount: v.pliCount,
140
+ firCount: v.firCount,
141
+ nackCount: v.nackCount,
142
+ jitter: v.jitter,
143
+ timestamp: v.timestamp,
144
+ bytesReceived: v.bytesReceived,
145
+ };
146
+ }
147
+ });
148
+ return receiverStats;
149
+ }
150
+
151
+ private stopObservingElement(element: HTMLMediaElement) {
152
+ getIntersectionObserver()?.unobserve(element);
153
+ getResizeObserver()?.unobserve(element);
154
+ this.elementInfos = this.elementInfos.filter((info) => info.element !== element);
155
+ }
156
+
157
+ private handleVisibilityChanged = (entry: IntersectionObserverEntry) => {
158
+ const { target, isIntersecting } = entry;
159
+ const elementInfo = this.elementInfos.find((info) => info.element === target);
160
+ if (elementInfo) {
161
+ elementInfo.visible = isIntersecting;
162
+ elementInfo.visibilityChangedAt = Date.now();
163
+ }
164
+ this.updateVisibility();
165
+ };
166
+
167
+ private readonly debouncedHandleResize = debounce(() => {
168
+ this.updateDimensions();
169
+ }, REACTION_DELAY);
170
+
171
+ private updateVisibility() {
172
+ const lastVisibilityChange = this.elementInfos.reduce(
173
+ (prev, info) => Math.max(prev, info.visibilityChangedAt || 0),
174
+ 0,
175
+ );
176
+ const isVisible = this.elementInfos.some((info) => info.visible);
177
+
178
+ if (this.lastVisible === isVisible) {
179
+ return;
180
+ }
181
+
182
+ if (!isVisible && Date.now() - lastVisibilityChange < REACTION_DELAY) {
183
+ // delay hidden events
184
+ setTimeout(() => {
185
+ this.updateVisibility();
186
+ }, REACTION_DELAY);
187
+ return;
188
+ }
189
+
190
+ this.lastVisible = isVisible;
191
+ this.emit(TrackEvent.VisibilityChanged, isVisible, this);
192
+ }
193
+
194
+ private updateDimensions() {
195
+ let maxWidth = 0;
196
+ let maxHeight = 0;
197
+ for (const info of this.elementInfos) {
198
+ if (info.element.clientWidth + info.element.clientHeight > maxWidth + maxHeight) {
199
+ maxWidth = info.element.clientWidth;
200
+ maxHeight = info.element.clientHeight;
201
+ }
202
+ }
203
+
204
+ if (this.lastDimensions?.width === maxWidth && this.lastDimensions?.height === maxHeight) {
205
+ return;
206
+ }
207
+
208
+ this.lastDimensions = {
209
+ width: maxWidth,
210
+ height: maxHeight,
211
+ };
212
+ this.emit(TrackEvent.VideoDimensionsChanged, this.lastDimensions, this);
213
+ }
214
+ }
215
+
216
+ interface ElementInfo {
217
+ element: HTMLMediaElement;
218
+ visible: boolean;
219
+ visibilityChangedAt?: number;
220
+ }