livekit-client 0.18.4-RC8 → 0.18.6
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.
- package/README.md +2 -5
- package/dist/api/RequestQueue.d.ts +13 -12
- package/dist/api/RequestQueue.d.ts.map +1 -0
- package/dist/api/SignalClient.d.ts +67 -66
- package/dist/api/SignalClient.d.ts.map +1 -0
- package/dist/connect.d.ts +24 -23
- package/dist/connect.d.ts.map +1 -0
- package/dist/index.d.ts +27 -26
- package/dist/index.d.ts.map +1 -0
- package/dist/livekit-client.esm.mjs +593 -507
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/logger.d.ts +26 -25
- package/dist/logger.d.ts.map +1 -0
- package/dist/options.d.ts +128 -127
- package/dist/options.d.ts.map +1 -0
- package/dist/proto/google/protobuf/timestamp.d.ts +133 -132
- package/dist/proto/google/protobuf/timestamp.d.ts.map +1 -0
- package/dist/proto/livekit_models.d.ts +876 -875
- package/dist/proto/livekit_models.d.ts.map +1 -0
- package/dist/proto/livekit_rtc.d.ts +3904 -3903
- package/dist/proto/livekit_rtc.d.ts.map +1 -0
- package/dist/room/DeviceManager.d.ts +8 -7
- package/dist/room/DeviceManager.d.ts.map +1 -0
- package/dist/room/PCTransport.d.ts +16 -15
- package/dist/room/PCTransport.d.ts.map +1 -0
- package/dist/room/RTCEngine.d.ts +67 -66
- package/dist/room/RTCEngine.d.ts.map +1 -0
- package/dist/room/Room.d.ts +166 -165
- package/dist/room/Room.d.ts.map +1 -0
- package/dist/room/errors.d.ts +29 -28
- package/dist/room/errors.d.ts.map +1 -0
- package/dist/room/events.d.ts +391 -390
- package/dist/room/events.d.ts.map +1 -0
- package/dist/room/participant/LocalParticipant.d.ts +126 -125
- package/dist/room/participant/LocalParticipant.d.ts.map +1 -0
- package/dist/room/participant/Participant.d.ts +94 -93
- package/dist/room/participant/Participant.d.ts.map +1 -0
- package/dist/room/participant/ParticipantTrackPermission.d.ts +26 -25
- package/dist/room/participant/ParticipantTrackPermission.d.ts.map +1 -0
- package/dist/room/participant/RemoteParticipant.d.ts +40 -39
- package/dist/room/participant/RemoteParticipant.d.ts.map +1 -0
- package/dist/room/participant/publishUtils.d.ts +18 -17
- package/dist/room/participant/publishUtils.d.ts.map +1 -0
- package/dist/room/stats.d.ts +66 -65
- package/dist/room/stats.d.ts.map +1 -0
- package/dist/room/track/LocalAudioTrack.d.ts +20 -19
- package/dist/room/track/LocalAudioTrack.d.ts.map +1 -0
- package/dist/room/track/LocalTrack.d.ts +28 -27
- package/dist/room/track/LocalTrack.d.ts.map +1 -0
- package/dist/room/track/LocalTrackPublication.d.ts +38 -37
- package/dist/room/track/LocalTrackPublication.d.ts.map +1 -0
- package/dist/room/track/LocalVideoTrack.d.ts +31 -30
- package/dist/room/track/LocalVideoTrack.d.ts.map +1 -0
- package/dist/room/track/RemoteAudioTrack.d.ts +20 -19
- package/dist/room/track/RemoteAudioTrack.d.ts.map +1 -0
- package/dist/room/track/RemoteTrack.d.ts +16 -15
- package/dist/room/track/RemoteTrack.d.ts.map +1 -0
- package/dist/room/track/RemoteTrackPublication.d.ts +51 -50
- package/dist/room/track/RemoteTrackPublication.d.ts.map +1 -0
- package/dist/room/track/RemoteVideoTrack.d.ts +30 -27
- package/dist/room/track/RemoteVideoTrack.d.ts.map +1 -0
- package/dist/room/track/Track.d.ts +105 -100
- package/dist/room/track/Track.d.ts.map +1 -0
- package/dist/room/track/TrackPublication.d.ts +50 -49
- package/dist/room/track/TrackPublication.d.ts.map +1 -0
- package/dist/room/track/create.d.ts +24 -23
- package/dist/room/track/create.d.ts.map +1 -0
- package/dist/room/track/defaults.d.ts +5 -4
- package/dist/room/track/defaults.d.ts.map +1 -0
- package/dist/room/track/options.d.ts +232 -222
- package/dist/room/track/options.d.ts.map +1 -0
- package/dist/room/track/types.d.ts +19 -18
- package/dist/room/track/types.d.ts.map +1 -0
- package/dist/room/track/utils.d.ts +14 -13
- package/dist/room/track/utils.d.ts.map +1 -0
- package/dist/room/utils.d.ts +17 -16
- package/dist/room/utils.d.ts.map +1 -0
- package/dist/test/mocks.d.ts +12 -11
- package/dist/test/mocks.d.ts.map +1 -0
- package/dist/version.d.ts +3 -2
- package/dist/version.d.ts.map +1 -0
- package/package.json +6 -6
- package/src/api/RequestQueue.ts +53 -0
- package/src/api/SignalClient.ts +497 -0
- package/src/connect.ts +98 -0
- package/src/index.ts +49 -0
- package/src/logger.ts +56 -0
- package/src/options.ts +156 -0
- package/src/proto/google/protobuf/timestamp.ts +216 -0
- package/src/proto/livekit_models.ts +2456 -0
- package/src/proto/livekit_rtc.ts +2859 -0
- package/src/room/DeviceManager.ts +80 -0
- package/src/room/PCTransport.ts +88 -0
- package/src/room/RTCEngine.ts +695 -0
- package/src/room/Room.ts +970 -0
- package/src/room/errors.ts +65 -0
- package/src/room/events.ts +438 -0
- package/src/room/participant/LocalParticipant.ts +779 -0
- package/src/room/participant/Participant.ts +287 -0
- package/src/room/participant/ParticipantTrackPermission.ts +42 -0
- package/src/room/participant/RemoteParticipant.ts +263 -0
- package/src/room/participant/publishUtils.test.ts +144 -0
- package/src/room/participant/publishUtils.ts +258 -0
- package/src/room/stats.ts +134 -0
- package/src/room/track/LocalAudioTrack.ts +134 -0
- package/src/room/track/LocalTrack.ts +229 -0
- package/src/room/track/LocalTrackPublication.ts +87 -0
- package/src/room/track/LocalVideoTrack.test.ts +72 -0
- package/src/room/track/LocalVideoTrack.ts +295 -0
- package/src/room/track/RemoteAudioTrack.ts +86 -0
- package/src/room/track/RemoteTrack.ts +62 -0
- package/src/room/track/RemoteTrackPublication.ts +207 -0
- package/src/room/track/RemoteVideoTrack.ts +253 -0
- package/src/room/track/Track.ts +365 -0
- package/src/room/track/TrackPublication.ts +120 -0
- package/src/room/track/create.ts +122 -0
- package/src/room/track/defaults.ts +26 -0
- package/src/room/track/options.ts +292 -0
- package/src/room/track/types.ts +20 -0
- package/src/room/track/utils.test.ts +110 -0
- package/src/room/track/utils.ts +113 -0
- package/src/room/utils.ts +115 -0
- package/src/test/mocks.ts +17 -0
- package/src/version.ts +2 -0
- 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,253 @@
|
|
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
|
+
import log from '../../logger';
|
14
|
+
|
15
|
+
const REACTION_DELAY = 100;
|
16
|
+
|
17
|
+
export default class RemoteVideoTrack extends RemoteTrack {
|
18
|
+
/** @internal */
|
19
|
+
receiver?: RTCRtpReceiver;
|
20
|
+
|
21
|
+
private prevStats?: VideoReceiverStats;
|
22
|
+
|
23
|
+
private elementInfos: ElementInfo[] = [];
|
24
|
+
|
25
|
+
private adaptiveStreamSettings?: AdaptiveStreamSettings;
|
26
|
+
|
27
|
+
private lastVisible?: boolean;
|
28
|
+
|
29
|
+
private lastDimensions?: Track.Dimensions;
|
30
|
+
|
31
|
+
private hasUsedAttach: boolean = false;
|
32
|
+
|
33
|
+
constructor(
|
34
|
+
mediaTrack: MediaStreamTrack,
|
35
|
+
sid: string,
|
36
|
+
receiver?: RTCRtpReceiver,
|
37
|
+
adaptiveStreamSettings?: AdaptiveStreamSettings,
|
38
|
+
) {
|
39
|
+
super(mediaTrack, sid, Track.Kind.Video, receiver);
|
40
|
+
this.adaptiveStreamSettings = adaptiveStreamSettings;
|
41
|
+
if (this.isAdaptiveStream) {
|
42
|
+
this.streamState = Track.StreamState.Paused;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
get isAdaptiveStream(): boolean {
|
47
|
+
return this.adaptiveStreamSettings !== undefined;
|
48
|
+
}
|
49
|
+
|
50
|
+
get mediaStreamTrack() {
|
51
|
+
if (this.isAdaptiveStream && !this.hasUsedAttach) {
|
52
|
+
log.warn(
|
53
|
+
'When using adaptiveStream, you need to use remoteVideoTrack.attach() to add the track to a HTMLVideoElement, otherwise your video tracks might never start',
|
54
|
+
);
|
55
|
+
}
|
56
|
+
return this._mediaStreamTrack;
|
57
|
+
}
|
58
|
+
|
59
|
+
/** @internal */
|
60
|
+
setMuted(muted: boolean) {
|
61
|
+
super.setMuted(muted);
|
62
|
+
|
63
|
+
this.attachedElements.forEach((element) => {
|
64
|
+
// detach or attach
|
65
|
+
if (muted) {
|
66
|
+
detachTrack(this._mediaStreamTrack, element);
|
67
|
+
} else {
|
68
|
+
attachToElement(this._mediaStreamTrack, element);
|
69
|
+
}
|
70
|
+
});
|
71
|
+
}
|
72
|
+
|
73
|
+
attach(): HTMLMediaElement;
|
74
|
+
attach(element: HTMLMediaElement): HTMLMediaElement;
|
75
|
+
attach(element?: HTMLMediaElement): HTMLMediaElement {
|
76
|
+
if (!element) {
|
77
|
+
element = super.attach();
|
78
|
+
} else {
|
79
|
+
super.attach(element);
|
80
|
+
}
|
81
|
+
|
82
|
+
// It's possible attach is called multiple times on an element. When that's
|
83
|
+
// the case, we'd want to avoid adding duplicate elementInfos
|
84
|
+
if (
|
85
|
+
this.adaptiveStreamSettings &&
|
86
|
+
this.elementInfos.find((info) => info.element === element) === undefined
|
87
|
+
) {
|
88
|
+
this.elementInfos.push({
|
89
|
+
element,
|
90
|
+
visible: true, // default visible
|
91
|
+
});
|
92
|
+
|
93
|
+
(element as ObservableMediaElement).handleResize = this.debouncedHandleResize;
|
94
|
+
(element as ObservableMediaElement).handleVisibilityChanged = this.handleVisibilityChanged;
|
95
|
+
|
96
|
+
getIntersectionObserver().observe(element);
|
97
|
+
getResizeObserver().observe(element);
|
98
|
+
|
99
|
+
// trigger the first resize update cycle
|
100
|
+
// if the tab is backgrounded, the initial resize event does not fire until
|
101
|
+
// the tab comes into focus for the first time.
|
102
|
+
this.debouncedHandleResize();
|
103
|
+
}
|
104
|
+
this.hasUsedAttach = true;
|
105
|
+
return element;
|
106
|
+
}
|
107
|
+
|
108
|
+
detach(): HTMLMediaElement[];
|
109
|
+
detach(element: HTMLMediaElement): HTMLMediaElement;
|
110
|
+
detach(element?: HTMLMediaElement): HTMLMediaElement | HTMLMediaElement[] {
|
111
|
+
let detachedElements: HTMLMediaElement[] = [];
|
112
|
+
if (element) {
|
113
|
+
this.stopObservingElement(element);
|
114
|
+
return super.detach(element);
|
115
|
+
}
|
116
|
+
detachedElements = super.detach();
|
117
|
+
|
118
|
+
for (const e of detachedElements) {
|
119
|
+
this.stopObservingElement(e);
|
120
|
+
}
|
121
|
+
|
122
|
+
return detachedElements;
|
123
|
+
}
|
124
|
+
|
125
|
+
protected monitorReceiver = async () => {
|
126
|
+
if (!this.receiver) {
|
127
|
+
this._currentBitrate = 0;
|
128
|
+
return;
|
129
|
+
}
|
130
|
+
const stats = await this.getReceiverStats();
|
131
|
+
|
132
|
+
if (stats && this.prevStats && this.receiver) {
|
133
|
+
this._currentBitrate = computeBitrate(stats, this.prevStats);
|
134
|
+
}
|
135
|
+
|
136
|
+
this.prevStats = stats;
|
137
|
+
setTimeout(() => {
|
138
|
+
this.monitorReceiver();
|
139
|
+
}, monitorFrequency);
|
140
|
+
};
|
141
|
+
|
142
|
+
private async getReceiverStats(): Promise<VideoReceiverStats | undefined> {
|
143
|
+
if (!this.receiver) {
|
144
|
+
return;
|
145
|
+
}
|
146
|
+
|
147
|
+
const stats = await this.receiver.getStats();
|
148
|
+
let receiverStats: VideoReceiverStats | undefined;
|
149
|
+
stats.forEach((v) => {
|
150
|
+
if (v.type === 'inbound-rtp') {
|
151
|
+
receiverStats = {
|
152
|
+
type: 'video',
|
153
|
+
framesDecoded: v.framesDecoded,
|
154
|
+
framesDropped: v.framesDropped,
|
155
|
+
framesReceived: v.framesReceived,
|
156
|
+
packetsReceived: v.packetsReceived,
|
157
|
+
packetsLost: v.packetsLost,
|
158
|
+
frameWidth: v.frameWidth,
|
159
|
+
frameHeight: v.frameHeight,
|
160
|
+
pliCount: v.pliCount,
|
161
|
+
firCount: v.firCount,
|
162
|
+
nackCount: v.nackCount,
|
163
|
+
jitter: v.jitter,
|
164
|
+
timestamp: v.timestamp,
|
165
|
+
bytesReceived: v.bytesReceived,
|
166
|
+
};
|
167
|
+
}
|
168
|
+
});
|
169
|
+
return receiverStats;
|
170
|
+
}
|
171
|
+
|
172
|
+
private stopObservingElement(element: HTMLMediaElement) {
|
173
|
+
getIntersectionObserver()?.unobserve(element);
|
174
|
+
getResizeObserver()?.unobserve(element);
|
175
|
+
this.elementInfos = this.elementInfos.filter((info) => info.element !== element);
|
176
|
+
}
|
177
|
+
|
178
|
+
private handleVisibilityChanged = (entry: IntersectionObserverEntry) => {
|
179
|
+
const { target, isIntersecting } = entry;
|
180
|
+
const elementInfo = this.elementInfos.find((info) => info.element === target);
|
181
|
+
if (elementInfo) {
|
182
|
+
elementInfo.visible = isIntersecting;
|
183
|
+
elementInfo.visibilityChangedAt = Date.now();
|
184
|
+
}
|
185
|
+
this.updateVisibility();
|
186
|
+
};
|
187
|
+
|
188
|
+
protected async handleAppVisibilityChanged() {
|
189
|
+
await super.handleAppVisibilityChanged();
|
190
|
+
if (!this.isAdaptiveStream) return;
|
191
|
+
// on desktop don't pause when tab is backgrounded
|
192
|
+
if (!isMobile()) return;
|
193
|
+
this.updateVisibility();
|
194
|
+
}
|
195
|
+
|
196
|
+
private readonly debouncedHandleResize = debounce(() => {
|
197
|
+
this.updateDimensions();
|
198
|
+
}, REACTION_DELAY);
|
199
|
+
|
200
|
+
private updateVisibility() {
|
201
|
+
const lastVisibilityChange = this.elementInfos.reduce(
|
202
|
+
(prev, info) => Math.max(prev, info.visibilityChangedAt || 0),
|
203
|
+
0,
|
204
|
+
);
|
205
|
+
const isVisible = this.elementInfos.some((info) => info.visible) && !this.isInBackground;
|
206
|
+
|
207
|
+
if (this.lastVisible === isVisible) {
|
208
|
+
return;
|
209
|
+
}
|
210
|
+
|
211
|
+
if (!isVisible && Date.now() - lastVisibilityChange < REACTION_DELAY) {
|
212
|
+
// delay hidden events
|
213
|
+
setTimeout(() => {
|
214
|
+
this.updateVisibility();
|
215
|
+
}, REACTION_DELAY);
|
216
|
+
return;
|
217
|
+
}
|
218
|
+
|
219
|
+
this.lastVisible = isVisible;
|
220
|
+
this.emit(TrackEvent.VisibilityChanged, isVisible, this);
|
221
|
+
}
|
222
|
+
|
223
|
+
private updateDimensions() {
|
224
|
+
let maxWidth = 0;
|
225
|
+
let maxHeight = 0;
|
226
|
+
for (const info of this.elementInfos) {
|
227
|
+
const pixelDensity = this.adaptiveStreamSettings?.pixelDensity ?? 1;
|
228
|
+
const pixelDensityValue = pixelDensity === 'screen' ? window.devicePixelRatio : pixelDensity;
|
229
|
+
const currentElementWidth = info.element.clientWidth * pixelDensityValue;
|
230
|
+
const currentElementHeight = info.element.clientHeight * pixelDensityValue;
|
231
|
+
if (currentElementWidth + currentElementHeight > maxWidth + maxHeight) {
|
232
|
+
maxWidth = currentElementWidth;
|
233
|
+
maxHeight = currentElementHeight;
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
if (this.lastDimensions?.width === maxWidth && this.lastDimensions?.height === maxHeight) {
|
238
|
+
return;
|
239
|
+
}
|
240
|
+
|
241
|
+
this.lastDimensions = {
|
242
|
+
width: maxWidth,
|
243
|
+
height: maxHeight,
|
244
|
+
};
|
245
|
+
this.emit(TrackEvent.VideoDimensionsChanged, this.lastDimensions, this);
|
246
|
+
}
|
247
|
+
}
|
248
|
+
|
249
|
+
interface ElementInfo {
|
250
|
+
element: HTMLMediaElement;
|
251
|
+
visible: boolean;
|
252
|
+
visibilityChangedAt?: number;
|
253
|
+
}
|