livekit-client 0.15.2 → 0.16.1
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/dist/api/SignalClient.d.ts +11 -3
- package/dist/api/SignalClient.js +92 -28
- package/dist/api/SignalClient.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +5 -0
- package/dist/proto/livekit_models.d.ts +33 -0
- package/dist/proto/livekit_models.js +263 -4
- package/dist/proto/livekit_models.js.map +1 -1
- package/dist/proto/livekit_rtc.d.ts +48 -10
- package/dist/proto/livekit_rtc.js +273 -22
- package/dist/proto/livekit_rtc.js.map +1 -1
- package/dist/room/PCTransport.js +4 -0
- package/dist/room/PCTransport.js.map +1 -1
- package/dist/room/RTCEngine.d.ts +10 -2
- package/dist/room/RTCEngine.js +182 -42
- package/dist/room/RTCEngine.js.map +1 -1
- package/dist/room/Room.d.ts +15 -0
- package/dist/room/Room.js +165 -20
- package/dist/room/Room.js.map +1 -1
- package/dist/room/events.d.ts +42 -20
- package/dist/room/events.js +41 -19
- package/dist/room/events.js.map +1 -1
- package/dist/room/participant/LocalParticipant.d.ts +25 -4
- package/dist/room/participant/LocalParticipant.js +47 -20
- package/dist/room/participant/LocalParticipant.js.map +1 -1
- package/dist/room/participant/Participant.d.ts +1 -1
- package/dist/room/participant/ParticipantTrackPermission.d.ts +19 -0
- package/dist/room/participant/ParticipantTrackPermission.js +16 -0
- package/dist/room/participant/ParticipantTrackPermission.js.map +1 -0
- package/dist/room/participant/RemoteParticipant.d.ts +2 -2
- package/dist/room/participant/RemoteParticipant.js +11 -14
- package/dist/room/participant/RemoteParticipant.js.map +1 -1
- package/dist/room/participant/publishUtils.js +1 -1
- package/dist/room/participant/publishUtils.js.map +1 -1
- package/dist/room/participant/publishUtils.test.js +9 -0
- package/dist/room/participant/publishUtils.test.js.map +1 -1
- package/dist/room/track/LocalTrack.d.ts +0 -3
- package/dist/room/track/LocalTrack.js +1 -6
- package/dist/room/track/LocalTrack.js.map +1 -1
- package/dist/room/track/LocalTrackPublication.d.ts +5 -1
- package/dist/room/track/LocalTrackPublication.js +15 -5
- package/dist/room/track/LocalTrackPublication.js.map +1 -1
- package/dist/room/track/LocalVideoTrack.js +2 -0
- package/dist/room/track/LocalVideoTrack.js.map +1 -1
- package/dist/room/track/RemoteAudioTrack.d.ts +5 -14
- package/dist/room/track/RemoteAudioTrack.js +7 -32
- package/dist/room/track/RemoteAudioTrack.js.map +1 -1
- package/dist/room/track/RemoteTrack.d.ts +14 -0
- package/dist/room/track/RemoteTrack.js +47 -0
- package/dist/room/track/RemoteTrack.js.map +1 -0
- package/dist/room/track/RemoteTrackPublication.d.ts +10 -2
- package/dist/room/track/RemoteTrackPublication.js +51 -13
- package/dist/room/track/RemoteTrackPublication.js.map +1 -1
- package/dist/room/track/RemoteVideoTrack.d.ts +3 -9
- package/dist/room/track/RemoteVideoTrack.js +16 -36
- package/dist/room/track/RemoteVideoTrack.js.map +1 -1
- package/dist/room/track/Track.d.ts +3 -0
- package/dist/room/track/Track.js +14 -5
- package/dist/room/track/Track.js.map +1 -1
- package/dist/room/track/TrackPublication.d.ts +12 -1
- package/dist/room/track/TrackPublication.js +23 -7
- package/dist/room/track/TrackPublication.js.map +1 -1
- package/dist/room/track/create.js +5 -0
- package/dist/room/track/create.js.map +1 -1
- package/dist/room/utils.d.ts +2 -0
- package/dist/room/utils.js +12 -1
- package/dist/room/utils.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +3 -3
- package/src/api/SignalClient.ts +444 -0
- package/src/connect.ts +100 -0
- package/src/index.ts +47 -0
- package/src/logger.ts +22 -0
- package/src/options.ts +152 -0
- package/src/proto/livekit_models.ts +1863 -0
- package/src/proto/livekit_rtc.ts +3415 -0
- package/src/room/DeviceManager.ts +57 -0
- package/src/room/PCTransport.ts +86 -0
- package/src/room/RTCEngine.ts +582 -0
- package/src/room/Room.ts +840 -0
- package/src/room/errors.ts +65 -0
- package/src/room/events.ts +398 -0
- package/src/room/participant/LocalParticipant.ts +685 -0
- package/src/room/participant/Participant.ts +214 -0
- package/src/room/participant/ParticipantTrackPermission.ts +32 -0
- package/src/room/participant/RemoteParticipant.ts +241 -0
- package/src/room/participant/publishUtils.test.ts +105 -0
- package/src/room/participant/publishUtils.ts +180 -0
- package/src/room/stats.ts +130 -0
- package/src/room/track/LocalAudioTrack.ts +112 -0
- package/src/room/track/LocalTrack.ts +124 -0
- package/src/room/track/LocalTrackPublication.ts +66 -0
- package/src/room/track/LocalVideoTrack.test.ts +70 -0
- package/src/room/track/LocalVideoTrack.ts +416 -0
- package/src/room/track/RemoteAudioTrack.ts +58 -0
- package/src/room/track/RemoteTrack.ts +59 -0
- package/src/room/track/RemoteTrackPublication.ts +198 -0
- package/src/room/track/RemoteVideoTrack.ts +215 -0
- package/src/room/track/Track.ts +307 -0
- package/src/room/track/TrackPublication.ts +120 -0
- package/src/room/track/create.ts +120 -0
- package/src/room/track/defaults.ts +23 -0
- package/src/room/track/options.ts +229 -0
- package/src/room/track/types.ts +8 -0
- package/src/room/track/utils.test.ts +93 -0
- package/src/room/track/utils.ts +76 -0
- package/src/room/utils.ts +58 -0
- package/src/version.ts +2 -0
- package/.github/workflows/publish.yaml +0 -55
- package/.github/workflows/test.yaml +0 -36
- package/example/index.html +0 -248
- package/example/sample.ts +0 -621
- package/example/styles.css +0 -144
- 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
|
+
});
|