livekit-client 0.18.4-RC6 → 0.18.4

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