@fishjam-cloud/ts-client 0.5.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/README.md +134 -0
- package/dist/src/FishjamClient.d.ts +523 -0
- package/dist/src/FishjamClient.js +666 -0
- package/dist/src/auth.d.ts +3 -0
- package/dist/src/auth.js +8 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +3 -0
- package/dist/src/protos/fishjam/peer_notifications.d.ts +78 -0
- package/dist/src/protos/fishjam/peer_notifications.js +302 -0
- package/dist/src/protos/index.d.ts +1 -0
- package/dist/src/protos/index.js +1 -0
- package/dist/src/reconnection.d.ts +27 -0
- package/dist/src/reconnection.js +121 -0
- package/dist/src/webrtc/CommandsQueue.d.ts +23 -0
- package/dist/src/webrtc/CommandsQueue.js +102 -0
- package/dist/src/webrtc/ConnectionManager.d.ts +29 -0
- package/dist/src/webrtc/ConnectionManager.js +82 -0
- package/dist/src/webrtc/bitrate.d.ts +12 -0
- package/dist/src/webrtc/bitrate.js +12 -0
- package/dist/src/webrtc/deferred.d.ts +8 -0
- package/dist/src/webrtc/deferred.js +17 -0
- package/dist/src/webrtc/index.d.ts +3 -0
- package/dist/src/webrtc/index.js +1 -0
- package/dist/src/webrtc/internal.d.ts +26 -0
- package/dist/src/webrtc/internal.js +34 -0
- package/dist/src/webrtc/mediaEvent.d.ts +10 -0
- package/dist/src/webrtc/mediaEvent.js +16 -0
- package/dist/src/webrtc/tracks/Local.d.ts +51 -0
- package/dist/src/webrtc/tracks/Local.js +289 -0
- package/dist/src/webrtc/tracks/LocalTrack.d.ts +62 -0
- package/dist/src/webrtc/tracks/LocalTrack.js +235 -0
- package/dist/src/webrtc/tracks/LocalTrackManager.d.ts +38 -0
- package/dist/src/webrtc/tracks/LocalTrackManager.js +83 -0
- package/dist/src/webrtc/tracks/Remote.d.ts +38 -0
- package/dist/src/webrtc/tracks/Remote.js +200 -0
- package/dist/src/webrtc/tracks/RemoteTrack.d.ts +39 -0
- package/dist/src/webrtc/tracks/RemoteTrack.js +64 -0
- package/dist/src/webrtc/tracks/TrackCommon.d.ts +8 -0
- package/dist/src/webrtc/tracks/TrackCommon.js +1 -0
- package/dist/src/webrtc/tracks/bandwidth.d.ts +1 -0
- package/dist/src/webrtc/tracks/bandwidth.js +27 -0
- package/dist/src/webrtc/tracks/encodings.d.ts +1 -0
- package/dist/src/webrtc/tracks/encodings.js +7 -0
- package/dist/src/webrtc/tracks/transceivers.d.ts +2 -0
- package/dist/src/webrtc/tracks/transceivers.js +78 -0
- package/dist/src/webrtc/types.d.ts +306 -0
- package/dist/src/webrtc/types.js +2 -0
- package/dist/src/webrtc/voiceActivityDetection.d.ts +2 -0
- package/dist/src/webrtc/voiceActivityDetection.js +2 -0
- package/dist/src/webrtc/webRTCEndpoint.d.ts +299 -0
- package/dist/src/webrtc/webRTCEndpoint.js +730 -0
- package/package.json +82 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { generateCustomEvent, generateMediaEvent } from '../mediaEvent';
|
|
2
|
+
import { defaultBitrates, defaultSimulcastBitrates, UNLIMITED_BANDWIDTH, } from '../bitrate';
|
|
3
|
+
import { getEncodingParameters } from './encodings';
|
|
4
|
+
import { createTransceiverConfig } from './transceivers';
|
|
5
|
+
/**
|
|
6
|
+
* This is a wrapper over `TrackContext` that adds additional properties such as:
|
|
7
|
+
* - `MLineId`: required to generate sdpOffer
|
|
8
|
+
* - `MediaStreamTrackId`: required to generate sdpOffer and match RTCRtpSender
|
|
9
|
+
* - `RTCRtpSender`: required to manage RTCPeerConnection track, e.g.:
|
|
10
|
+
* - enable/disable encoding
|
|
11
|
+
* - remove from connection
|
|
12
|
+
* - replace track
|
|
13
|
+
* - set track bandwidth
|
|
14
|
+
*
|
|
15
|
+
* In the future, this object could potentially be merged with `TrackContextImpl`.
|
|
16
|
+
*
|
|
17
|
+
* # Lifecycle, state transitions
|
|
18
|
+
*
|
|
19
|
+
* I identified the following states
|
|
20
|
+
* - Before creating `Connection` object
|
|
21
|
+
* - connection === null
|
|
22
|
+
* - sender === null
|
|
23
|
+
* - mLineId === null
|
|
24
|
+
* - After creating `Connection` object, during `onOfferData` handler
|
|
25
|
+
* // TODO: Verify if the track acquires a sender at this point or if it's only for past tracks
|
|
26
|
+
* - connection !== null
|
|
27
|
+
* - sender !== null
|
|
28
|
+
* - mLineId === null
|
|
29
|
+
* - After establishing connection, during `onSdpAnswer`, track is being sent
|
|
30
|
+
* - connection !== null
|
|
31
|
+
* - sender !== null
|
|
32
|
+
* - mLineId !== null
|
|
33
|
+
*/
|
|
34
|
+
export class LocalTrack {
|
|
35
|
+
id;
|
|
36
|
+
mediaStreamTrackId = null;
|
|
37
|
+
mLineId = null;
|
|
38
|
+
trackContext;
|
|
39
|
+
sender = null;
|
|
40
|
+
encodings;
|
|
41
|
+
metadataParser;
|
|
42
|
+
connection;
|
|
43
|
+
constructor(connection, id, trackContext, metadataParser) {
|
|
44
|
+
this.connection = connection;
|
|
45
|
+
this.id = id;
|
|
46
|
+
this.trackContext = trackContext;
|
|
47
|
+
// todo maybe we could remove this object and use sender.getParameters().encodings.encodingParameter.active instead
|
|
48
|
+
this.encodings = { h: true, m: true, l: true };
|
|
49
|
+
if (trackContext.track?.id) {
|
|
50
|
+
this.mediaStreamTrackId = trackContext.track?.id;
|
|
51
|
+
}
|
|
52
|
+
this.metadataParser = metadataParser;
|
|
53
|
+
}
|
|
54
|
+
updateSender = () => {
|
|
55
|
+
if (this.mediaStreamTrackId && this.connection) {
|
|
56
|
+
this.sender = this.connection.findSender(this.mediaStreamTrackId);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
updateConnection = (connection) => {
|
|
60
|
+
this.connection = connection;
|
|
61
|
+
};
|
|
62
|
+
disableTrackEncoding = async (encoding) => {
|
|
63
|
+
if (!this.sender)
|
|
64
|
+
throw new Error(`RTCRtpSender for track ${this.id} not found`);
|
|
65
|
+
const params = this.sender.getParameters();
|
|
66
|
+
const encodings = params.encodings;
|
|
67
|
+
const encodingParameter = encodings.find((en) => en.rid == encoding);
|
|
68
|
+
if (!encodingParameter)
|
|
69
|
+
throw new Error(`RTCRtpEncodingParameters for track ${this.id} not found`);
|
|
70
|
+
encodingParameter.active = false;
|
|
71
|
+
this.encodings[encoding] = false;
|
|
72
|
+
await this.sender.setParameters(params);
|
|
73
|
+
};
|
|
74
|
+
addTrackToConnection = () => {
|
|
75
|
+
if (!this.trackContext.track)
|
|
76
|
+
throw Error(`MediaStreamTrack for track ${this.id} does not exist`);
|
|
77
|
+
if (!this.connection)
|
|
78
|
+
throw new Error(`There is no active RTCPeerConnection`);
|
|
79
|
+
const transceiverConfig = createTransceiverConfig(this.trackContext);
|
|
80
|
+
this.updateEncodings();
|
|
81
|
+
this.connection.addTransceiver(this.trackContext.track, transceiverConfig);
|
|
82
|
+
};
|
|
83
|
+
// 1
|
|
84
|
+
updateEncodings = () => {
|
|
85
|
+
if (this.trackContext?.track?.kind === 'video' &&
|
|
86
|
+
this.trackContext.simulcastConfig?.activeEncodings) {
|
|
87
|
+
const activeEncodings = this.trackContext.simulcastConfig.activeEncodings;
|
|
88
|
+
this.encodings.l = activeEncodings.some((e) => e === 'l');
|
|
89
|
+
this.encodings.m = activeEncodings.some((e) => e === 'm');
|
|
90
|
+
this.encodings.h = activeEncodings.some((e) => e === 'h');
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
removeFromConnection = () => {
|
|
94
|
+
if (!this.sender)
|
|
95
|
+
throw new Error(`RTCRtpSender for track ${this.id} not found`);
|
|
96
|
+
if (!this.connection)
|
|
97
|
+
throw new Error(`There is no active RTCPeerConnection`);
|
|
98
|
+
this.connection.removeTrack(this.sender);
|
|
99
|
+
};
|
|
100
|
+
// TODO: Remove `newTrackMetadata` parameter because this function should be an atomic operation.
|
|
101
|
+
// Metadata are updated after `await this.sender.replaceTrack(newTrack)`,
|
|
102
|
+
// so it could be chained by the user.
|
|
103
|
+
replaceTrack = async (newTrack,
|
|
104
|
+
// todo remove webrtc with newTrackMetadata
|
|
105
|
+
newTrackMetadata,
|
|
106
|
+
// todo remove webrtc with newTrackMetadata
|
|
107
|
+
webrtc) => {
|
|
108
|
+
const trackId = this.id;
|
|
109
|
+
const stream = this.trackContext.stream;
|
|
110
|
+
if (!this.sender)
|
|
111
|
+
throw Error('There is no RTCRtpSender for this track id!');
|
|
112
|
+
stream?.getTracks().forEach((track) => {
|
|
113
|
+
stream?.removeTrack(track);
|
|
114
|
+
});
|
|
115
|
+
if (newTrack) {
|
|
116
|
+
stream?.addTrack(newTrack);
|
|
117
|
+
}
|
|
118
|
+
if (this.trackContext.track && !newTrack) {
|
|
119
|
+
const mediaEvent = generateMediaEvent('muteTrack', { trackId: trackId });
|
|
120
|
+
webrtc.sendMediaEvent(mediaEvent);
|
|
121
|
+
webrtc.emit('localTrackMuted', { trackId: trackId });
|
|
122
|
+
}
|
|
123
|
+
else if (!this.trackContext.track && newTrack) {
|
|
124
|
+
const mediaEvent = generateMediaEvent('unmuteTrack', {
|
|
125
|
+
trackId: trackId,
|
|
126
|
+
});
|
|
127
|
+
webrtc.sendMediaEvent(mediaEvent);
|
|
128
|
+
webrtc.emit('localTrackUnmuted', { trackId: trackId });
|
|
129
|
+
}
|
|
130
|
+
this.mediaStreamTrackId = newTrack?.id ?? null;
|
|
131
|
+
try {
|
|
132
|
+
await this.sender.replaceTrack(newTrack);
|
|
133
|
+
this.trackContext.track = newTrack;
|
|
134
|
+
if (newTrackMetadata) {
|
|
135
|
+
webrtc.updateTrackMetadata(trackId, newTrackMetadata);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
// ignore
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
setTrackBandwidth = (bandwidth) => {
|
|
143
|
+
if (!this.sender)
|
|
144
|
+
throw new Error(`RTCRtpSender for track ${this.id} not found`);
|
|
145
|
+
const parameters = this.sender.getParameters();
|
|
146
|
+
parameters.encodings = getEncodingParameters(parameters, bandwidth);
|
|
147
|
+
return this.sender.setParameters(parameters);
|
|
148
|
+
};
|
|
149
|
+
setEncodingBandwidth(rid, bandwidth) {
|
|
150
|
+
if (!this.sender)
|
|
151
|
+
throw new Error(`RTCRtpSender for track ${this.id} not found`);
|
|
152
|
+
const parameters = this.sender.getParameters();
|
|
153
|
+
const encoding = parameters.encodings.find((encoding) => encoding.rid === rid);
|
|
154
|
+
if (!encoding) {
|
|
155
|
+
return Promise.reject(`Encoding with rid '${rid}' doesn't exist`);
|
|
156
|
+
}
|
|
157
|
+
else if (bandwidth === 0) {
|
|
158
|
+
delete encoding.maxBitrate;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
encoding.maxBitrate = bandwidth * 1024;
|
|
162
|
+
}
|
|
163
|
+
return this.sender.setParameters(parameters);
|
|
164
|
+
}
|
|
165
|
+
updateTrackMetadata = (metadata) => {
|
|
166
|
+
const trackContext = this.trackContext;
|
|
167
|
+
try {
|
|
168
|
+
trackContext.metadata = this.metadataParser(metadata);
|
|
169
|
+
trackContext.rawMetadata = metadata;
|
|
170
|
+
trackContext.metadataParsingError = undefined;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
trackContext.metadata = undefined;
|
|
174
|
+
trackContext.metadataParsingError = error;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
enableTrackEncoding = (encoding) => {
|
|
179
|
+
if (!this.sender)
|
|
180
|
+
throw new Error(`RTCRtpSender for track ${this.id} not found`);
|
|
181
|
+
const params = this.sender.getParameters();
|
|
182
|
+
const encodingParameters = params.encodings.find((en) => en.rid == encoding);
|
|
183
|
+
if (!encodingParameters)
|
|
184
|
+
throw new Error(`RTCRtEncodingParameters ${encoding} for track ${this.id} not found`);
|
|
185
|
+
encodingParameters.active = true;
|
|
186
|
+
this.encodings[encoding] = true;
|
|
187
|
+
return this.sender.setParameters(params);
|
|
188
|
+
};
|
|
189
|
+
getDisabledEncodings = () => {
|
|
190
|
+
return Object.entries(this.encodings)
|
|
191
|
+
.filter(([_, value]) => !value)
|
|
192
|
+
.map(([encoding]) => encoding)
|
|
193
|
+
.reduce((acc, encoding) => [...acc, encoding], []);
|
|
194
|
+
};
|
|
195
|
+
setMLineId = (mLineId) => {
|
|
196
|
+
this.mLineId = mLineId;
|
|
197
|
+
};
|
|
198
|
+
isNotSimulcastTrack = (encodings) => encodings.length === 1 && !encodings[0].rid;
|
|
199
|
+
getTrackBitrates = () => {
|
|
200
|
+
const trackContext = this.trackContext;
|
|
201
|
+
const kind = this.trackContext.track?.kind;
|
|
202
|
+
if (!trackContext.track) {
|
|
203
|
+
if (!trackContext.trackKind) {
|
|
204
|
+
throw new Error('trackContext.trackKind is empty');
|
|
205
|
+
}
|
|
206
|
+
return defaultBitrates[trackContext.trackKind];
|
|
207
|
+
}
|
|
208
|
+
if (!this.sender)
|
|
209
|
+
throw new Error(`RTCRtpSender for track ${this.id} not found`);
|
|
210
|
+
const encodings = this.sender.getParameters().encodings;
|
|
211
|
+
if (this.isNotSimulcastTrack(encodings)) {
|
|
212
|
+
return (encodings[0].maxBitrate ||
|
|
213
|
+
(kind ? defaultBitrates[kind] : UNLIMITED_BANDWIDTH));
|
|
214
|
+
}
|
|
215
|
+
else if (kind === 'audio') {
|
|
216
|
+
throw 'Audio track cannot have multiple encodings';
|
|
217
|
+
}
|
|
218
|
+
return encodings
|
|
219
|
+
.filter((encoding) => encoding.rid)
|
|
220
|
+
.reduce((acc, encoding) => {
|
|
221
|
+
const rid = encoding.rid;
|
|
222
|
+
acc[rid] = encoding.maxBitrate || defaultSimulcastBitrates[rid];
|
|
223
|
+
return acc;
|
|
224
|
+
}, {});
|
|
225
|
+
};
|
|
226
|
+
createTrackVariantBitratesEvent = () => {
|
|
227
|
+
return generateCustomEvent({
|
|
228
|
+
type: 'trackVariantBitrates',
|
|
229
|
+
data: {
|
|
230
|
+
trackId: this.id,
|
|
231
|
+
variantBitrates: this.getTrackBitrates(),
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SimulcastConfig, TrackBandwidthLimit } from '../types';
|
|
2
|
+
import { type MediaEvent } from '../mediaEvent';
|
|
3
|
+
import type { ConnectionManager } from '../ConnectionManager';
|
|
4
|
+
import type { Local } from './Local';
|
|
5
|
+
import type { WebRTCEndpoint } from '../webRTCEndpoint';
|
|
6
|
+
/**
|
|
7
|
+
* This class is responsible for handling asynchronous operations related to track management.
|
|
8
|
+
* It contains methods and state associated with handling a single operation
|
|
9
|
+
* (adding, removing, and replacing a track).
|
|
10
|
+
* It facilitates the preparation of events that will later be handled by the `CommandsQueue`.
|
|
11
|
+
* It informs the `CommandsQueue` whether the previous task has been completed
|
|
12
|
+
*
|
|
13
|
+
* Adding and removing tracks requires renegotiation.
|
|
14
|
+
*
|
|
15
|
+
* Replacing a track relies on `ongoingTrackReplacement`, which probably could be removed
|
|
16
|
+
* and replaced with a Promise, because it does not require renegotiation.
|
|
17
|
+
*/
|
|
18
|
+
export declare class LocalTrackManager<EndpointMetadata, TrackMetadata> {
|
|
19
|
+
connection?: ConnectionManager;
|
|
20
|
+
private readonly local;
|
|
21
|
+
ongoingTrackReplacement: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Indicates if an ongoing renegotiation is active.
|
|
24
|
+
* During renegotiation, both parties are expected to actively exchange events: renegotiateTracks, offerData, sdpOffer, sdpAnswer.
|
|
25
|
+
*/
|
|
26
|
+
ongoingRenegotiation: boolean;
|
|
27
|
+
private readonly sendMediaEvent;
|
|
28
|
+
constructor(local: Local<EndpointMetadata, TrackMetadata>, sendMediaEvent: (mediaEvent: MediaEvent) => void);
|
|
29
|
+
isNegotiationInProgress: () => boolean;
|
|
30
|
+
cleanUp: () => void;
|
|
31
|
+
parseAddTrack: (track: MediaStreamTrack, simulcastConfig: SimulcastConfig, maxBandwidth: TrackBandwidthLimit) => void;
|
|
32
|
+
addTrackHandler: (trackId: string, track: MediaStreamTrack, stream: MediaStream, trackMetadata: TrackMetadata | undefined, simulcastConfig: SimulcastConfig, maxBandwidth: TrackBandwidthLimit) => void;
|
|
33
|
+
removeTrackHandler: (trackId: string) => void;
|
|
34
|
+
replaceTrackHandler: (webrtc: WebRTCEndpoint<EndpointMetadata, TrackMetadata>, trackId: string, newTrack: MediaStreamTrack | null, newTrackMetadata?: TrackMetadata) => Promise<void>;
|
|
35
|
+
getEndpointId: () => string;
|
|
36
|
+
updateConnection: (connection: ConnectionManager) => void;
|
|
37
|
+
updateSenders: () => void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { generateCustomEvent } from '../mediaEvent';
|
|
2
|
+
/**
|
|
3
|
+
* This class is responsible for handling asynchronous operations related to track management.
|
|
4
|
+
* It contains methods and state associated with handling a single operation
|
|
5
|
+
* (adding, removing, and replacing a track).
|
|
6
|
+
* It facilitates the preparation of events that will later be handled by the `CommandsQueue`.
|
|
7
|
+
* It informs the `CommandsQueue` whether the previous task has been completed
|
|
8
|
+
*
|
|
9
|
+
* Adding and removing tracks requires renegotiation.
|
|
10
|
+
*
|
|
11
|
+
* Replacing a track relies on `ongoingTrackReplacement`, which probably could be removed
|
|
12
|
+
* and replaced with a Promise, because it does not require renegotiation.
|
|
13
|
+
*/
|
|
14
|
+
export class LocalTrackManager {
|
|
15
|
+
connection;
|
|
16
|
+
local;
|
|
17
|
+
ongoingTrackReplacement = false;
|
|
18
|
+
/**
|
|
19
|
+
* Indicates if an ongoing renegotiation is active.
|
|
20
|
+
* During renegotiation, both parties are expected to actively exchange events: renegotiateTracks, offerData, sdpOffer, sdpAnswer.
|
|
21
|
+
*/
|
|
22
|
+
ongoingRenegotiation = false;
|
|
23
|
+
sendMediaEvent;
|
|
24
|
+
constructor(local, sendMediaEvent) {
|
|
25
|
+
this.local = local;
|
|
26
|
+
this.sendMediaEvent = sendMediaEvent;
|
|
27
|
+
}
|
|
28
|
+
isNegotiationInProgress = () => {
|
|
29
|
+
return this.ongoingRenegotiation || this.ongoingTrackReplacement;
|
|
30
|
+
};
|
|
31
|
+
cleanUp = () => {
|
|
32
|
+
this.ongoingTrackReplacement = false;
|
|
33
|
+
this.ongoingRenegotiation = false;
|
|
34
|
+
};
|
|
35
|
+
parseAddTrack = (track, simulcastConfig, maxBandwidth) => {
|
|
36
|
+
if (this.getEndpointId() === '') {
|
|
37
|
+
throw new Error('Cannot add tracks before being accepted by the server');
|
|
38
|
+
}
|
|
39
|
+
if (!simulcastConfig.enabled && !(typeof maxBandwidth === 'number')) {
|
|
40
|
+
throw new Error('Invalid type of `maxBandwidth` argument for a non-simulcast track, expected: number');
|
|
41
|
+
}
|
|
42
|
+
if (this.connection?.isTrackInUse(track)) {
|
|
43
|
+
throw new Error("This track was already added to peerConnection, it can't be added again!");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
addTrackHandler = (trackId, track, stream, trackMetadata, simulcastConfig, maxBandwidth) => {
|
|
47
|
+
this.ongoingRenegotiation = true;
|
|
48
|
+
const trackManager = this.local.addTrack(this.connection, trackId, track, stream, trackMetadata, simulcastConfig, maxBandwidth);
|
|
49
|
+
if (this.connection) {
|
|
50
|
+
trackManager.addTrackToConnection();
|
|
51
|
+
this.connection.setTransceiverDirection();
|
|
52
|
+
}
|
|
53
|
+
const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' });
|
|
54
|
+
this.sendMediaEvent(mediaEvent);
|
|
55
|
+
};
|
|
56
|
+
removeTrackHandler = (trackId) => {
|
|
57
|
+
if (!this.connection)
|
|
58
|
+
throw new Error(`There is no active RTCPeerConnection`);
|
|
59
|
+
this.ongoingRenegotiation = true;
|
|
60
|
+
this.local.removeTrack(trackId);
|
|
61
|
+
};
|
|
62
|
+
replaceTrackHandler = async (
|
|
63
|
+
// todo remove webrtc with newTrackMetadata
|
|
64
|
+
webrtc, trackId, newTrack,
|
|
65
|
+
// todo remove webrtc with newTrackMetadata
|
|
66
|
+
newTrackMetadata) => {
|
|
67
|
+
this.ongoingTrackReplacement = true;
|
|
68
|
+
try {
|
|
69
|
+
await this.local.replaceTrack(webrtc, trackId, newTrack, newTrackMetadata);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
this.ongoingTrackReplacement = false;
|
|
73
|
+
}
|
|
74
|
+
this.ongoingTrackReplacement = false;
|
|
75
|
+
};
|
|
76
|
+
getEndpointId = () => this.local.getEndpoint().id;
|
|
77
|
+
updateConnection = (connection) => {
|
|
78
|
+
this.connection = connection;
|
|
79
|
+
};
|
|
80
|
+
updateSenders = () => {
|
|
81
|
+
this.local.updateSenders();
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Encoding, EncodingReason, MetadataParser, MLineId, SimulcastConfig, TrackContext, WebRTCEndpointEvents } from '../types';
|
|
2
|
+
import { RemoteTrack } from './RemoteTrack';
|
|
3
|
+
import type { EndpointWithTrackContext } from '../internal';
|
|
4
|
+
import { type MediaEvent } from '../mediaEvent';
|
|
5
|
+
import type { EndpointId, TrackId } from './TrackCommon';
|
|
6
|
+
type SDPTrack = {
|
|
7
|
+
metadata: null;
|
|
8
|
+
simulcastConfig: SimulcastConfig;
|
|
9
|
+
};
|
|
10
|
+
export declare class Remote<EndpointMetadata, TrackMetadata> {
|
|
11
|
+
private readonly remoteTracks;
|
|
12
|
+
private readonly remoteEndpoints;
|
|
13
|
+
private readonly endpointMetadataParser;
|
|
14
|
+
private readonly trackMetadataParser;
|
|
15
|
+
private readonly emit;
|
|
16
|
+
private readonly sendMediaEvent;
|
|
17
|
+
constructor(emit: <E extends keyof Required<WebRTCEndpointEvents<EndpointMetadata, TrackMetadata>>>(event: E, ...args: Parameters<Required<WebRTCEndpointEvents<EndpointMetadata, TrackMetadata>>[E]>) => void, sendMediaEvent: (mediaEvent: MediaEvent) => void, endpointMetadataParser: MetadataParser<EndpointMetadata>, trackMetadataParser: MetadataParser<TrackMetadata>);
|
|
18
|
+
getTrackByMid: (mid: string) => RemoteTrack<EndpointMetadata, TrackMetadata>;
|
|
19
|
+
addTracks: (endpointId: EndpointId, tracks: Record<TrackId, SDPTrack>, trackIdToMetadata: Record<TrackId, any>) => void;
|
|
20
|
+
removeTracks: (trackIds: TrackId[]) => void;
|
|
21
|
+
removeRemoteTrack: (trackId: TrackId) => void;
|
|
22
|
+
addRemoteEndpoint: (endpoint: any, sendNotification?: boolean) => void;
|
|
23
|
+
private addEndpoint;
|
|
24
|
+
updateRemoteEndpoint: (data: any) => void;
|
|
25
|
+
private updateEndpointMetadata;
|
|
26
|
+
removeRemoteEndpoint: (endpointId: EndpointId) => void;
|
|
27
|
+
updateRemoteTrack: (data: any) => void;
|
|
28
|
+
private updateTrackMetadata;
|
|
29
|
+
disableRemoteTrackEncoding: (trackId: TrackId, encoding: Encoding) => void;
|
|
30
|
+
enableRemoteTrackEncoding: (trackId: TrackId, encoding: Encoding) => void;
|
|
31
|
+
setRemoteTrackEncoding: (trackId: TrackId, encoding: Encoding, reason: EncodingReason) => void;
|
|
32
|
+
setRemoteTrackVadStatus: (trackId: TrackId, vadStatus: string) => void;
|
|
33
|
+
getRemoteTrackContexts: () => Record<string, TrackContext<EndpointMetadata, TrackMetadata>>;
|
|
34
|
+
getRemoteEndpoints: () => Record<string, EndpointWithTrackContext<EndpointMetadata, TrackMetadata>>;
|
|
35
|
+
setTargetRemoteTrackEncoding: (trackId: TrackId, variant: Encoding) => void;
|
|
36
|
+
updateMLineIds: (midToTrackId: Record<MLineId, TrackId>) => void;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { RemoteTrack } from './RemoteTrack';
|
|
2
|
+
import { TrackContextImpl } from '../internal';
|
|
3
|
+
import { isVadStatus } from '../voiceActivityDetection';
|
|
4
|
+
import { generateCustomEvent } from '../mediaEvent';
|
|
5
|
+
export class Remote {
|
|
6
|
+
remoteTracks = {};
|
|
7
|
+
remoteEndpoints = {};
|
|
8
|
+
endpointMetadataParser;
|
|
9
|
+
trackMetadataParser;
|
|
10
|
+
emit;
|
|
11
|
+
sendMediaEvent;
|
|
12
|
+
constructor(emit, sendMediaEvent, endpointMetadataParser, trackMetadataParser) {
|
|
13
|
+
this.emit = emit;
|
|
14
|
+
this.sendMediaEvent = sendMediaEvent;
|
|
15
|
+
this.endpointMetadataParser = endpointMetadataParser;
|
|
16
|
+
this.trackMetadataParser = trackMetadataParser;
|
|
17
|
+
}
|
|
18
|
+
getTrackByMid = (mid) => {
|
|
19
|
+
const remoteTrack = Object.values(this.remoteTracks).find((remote) => remote.mLineId === mid);
|
|
20
|
+
if (!remoteTrack)
|
|
21
|
+
throw new Error(`Remote track with ${mid} not found`);
|
|
22
|
+
return remoteTrack;
|
|
23
|
+
};
|
|
24
|
+
addTracks = (endpointId, tracks, trackIdToMetadata) => {
|
|
25
|
+
const endpoint = this.remoteEndpoints[endpointId];
|
|
26
|
+
if (!endpoint)
|
|
27
|
+
throw new Error(`Endpoint ${endpointId} not found`);
|
|
28
|
+
Object.entries(tracks || {})
|
|
29
|
+
.map(([trackId, { simulcastConfig }]) => {
|
|
30
|
+
const trackContext = new TrackContextImpl(endpoint, trackId, trackIdToMetadata[trackId], simulcastConfig, this.trackMetadataParser);
|
|
31
|
+
return new RemoteTrack(trackId, trackContext);
|
|
32
|
+
})
|
|
33
|
+
.forEach((remoteTrack) => {
|
|
34
|
+
this.remoteTracks[remoteTrack.id] = remoteTrack;
|
|
35
|
+
endpoint.tracks.set(remoteTrack.id, remoteTrack.trackContext);
|
|
36
|
+
this.emit('trackAdded', remoteTrack.trackContext);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
removeTracks = (trackIds) => {
|
|
40
|
+
trackIds.forEach((trackId) => {
|
|
41
|
+
this.removeRemoteTrack(trackId);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
removeRemoteTrack = (trackId) => {
|
|
45
|
+
const remoteTrack = this.remoteTracks[trackId];
|
|
46
|
+
if (!remoteTrack)
|
|
47
|
+
throw new Error(`Track ${trackId} not found`);
|
|
48
|
+
const remoteEndpoint = this.remoteEndpoints[remoteTrack.trackContext.endpoint.id];
|
|
49
|
+
if (!remoteEndpoint)
|
|
50
|
+
throw new Error(`Endpoint ${remoteTrack.trackContext.endpoint.id} not found`);
|
|
51
|
+
remoteEndpoint.tracks.delete(trackId);
|
|
52
|
+
delete this.remoteTracks[trackId];
|
|
53
|
+
this.emit('trackRemoved', remoteTrack.trackContext);
|
|
54
|
+
};
|
|
55
|
+
addRemoteEndpoint = (endpoint, sendNotification = true) => {
|
|
56
|
+
const newEndpoint = {
|
|
57
|
+
id: endpoint.id,
|
|
58
|
+
type: endpoint.type,
|
|
59
|
+
metadata: undefined,
|
|
60
|
+
rawMetadata: undefined,
|
|
61
|
+
metadataParsingError: undefined,
|
|
62
|
+
tracks: new Map(),
|
|
63
|
+
};
|
|
64
|
+
// mutation in place
|
|
65
|
+
this.updateEndpointMetadata(newEndpoint, endpoint.metadata);
|
|
66
|
+
this.addEndpoint(newEndpoint);
|
|
67
|
+
this.addTracks(newEndpoint.id, endpoint.tracks, endpoint.trackIdToMetadata);
|
|
68
|
+
if (sendNotification) {
|
|
69
|
+
this.emit('endpointAdded', endpoint);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
addEndpoint = (endpoint) => {
|
|
73
|
+
this.remoteEndpoints[endpoint.id] = endpoint;
|
|
74
|
+
};
|
|
75
|
+
updateRemoteEndpoint = (data) => {
|
|
76
|
+
const endpoint = this.remoteEndpoints[data.id];
|
|
77
|
+
if (!endpoint)
|
|
78
|
+
throw new Error(`Endpoint ${data.id} not found`);
|
|
79
|
+
// mutation in place
|
|
80
|
+
this.updateEndpointMetadata(endpoint, data.metadata);
|
|
81
|
+
this.emit('endpointUpdated', endpoint);
|
|
82
|
+
};
|
|
83
|
+
updateEndpointMetadata = (endpoint, metadata) => {
|
|
84
|
+
try {
|
|
85
|
+
endpoint.metadata = this.endpointMetadataParser(metadata);
|
|
86
|
+
endpoint.metadataParsingError = undefined;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
endpoint.metadata = undefined;
|
|
90
|
+
endpoint.metadataParsingError = error;
|
|
91
|
+
}
|
|
92
|
+
endpoint.rawMetadata = metadata;
|
|
93
|
+
};
|
|
94
|
+
removeRemoteEndpoint = (endpointId) => {
|
|
95
|
+
const endpoint = this.remoteEndpoints[endpointId];
|
|
96
|
+
if (!endpoint)
|
|
97
|
+
throw new Error(`Endpoint ${endpointId} not found`);
|
|
98
|
+
const trackIds = [...endpoint.tracks.values()].map(({ trackId }) => trackId);
|
|
99
|
+
this.removeTracks(trackIds);
|
|
100
|
+
delete this.remoteEndpoints[endpointId];
|
|
101
|
+
this.emit('endpointRemoved', endpoint);
|
|
102
|
+
};
|
|
103
|
+
updateRemoteTrack = (data) => {
|
|
104
|
+
const endpointId = data.endpointId;
|
|
105
|
+
const endpoint = this.remoteEndpoints[endpointId];
|
|
106
|
+
if (!endpoint)
|
|
107
|
+
throw new Error(`Endpoint ${endpointId} not found`);
|
|
108
|
+
const trackId = data.trackId;
|
|
109
|
+
const remoteTrack = this.remoteTracks[trackId];
|
|
110
|
+
if (!remoteTrack)
|
|
111
|
+
throw new Error(`Track ${trackId} not found`);
|
|
112
|
+
this.updateTrackMetadata(remoteTrack.trackContext, data.metadata);
|
|
113
|
+
this.emit('trackUpdated', remoteTrack.trackContext);
|
|
114
|
+
};
|
|
115
|
+
updateTrackMetadata = (trackContext, trackMetadata) => {
|
|
116
|
+
try {
|
|
117
|
+
trackContext.metadata = this.trackMetadataParser(trackMetadata);
|
|
118
|
+
trackContext.metadataParsingError = undefined;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
trackContext.metadataParsingError = error;
|
|
122
|
+
trackContext.metadata = undefined;
|
|
123
|
+
}
|
|
124
|
+
trackContext.rawMetadata = trackMetadata;
|
|
125
|
+
};
|
|
126
|
+
disableRemoteTrackEncoding = (trackId, encoding) => {
|
|
127
|
+
const remoteTrack = this.remoteTracks[trackId];
|
|
128
|
+
if (!remoteTrack)
|
|
129
|
+
throw new Error(`Track ${trackId} not found`);
|
|
130
|
+
remoteTrack.disableTrackEncoding(encoding);
|
|
131
|
+
this.emit('trackEncodingDisabled', remoteTrack.trackContext, encoding);
|
|
132
|
+
};
|
|
133
|
+
enableRemoteTrackEncoding = (trackId, encoding) => {
|
|
134
|
+
const remoteTrack = this.remoteTracks[trackId];
|
|
135
|
+
if (!remoteTrack)
|
|
136
|
+
throw new Error(`Track ${trackId} not found`);
|
|
137
|
+
remoteTrack.enableTrackEncoding(encoding);
|
|
138
|
+
this.emit('trackEncodingEnabled', remoteTrack.trackContext, encoding);
|
|
139
|
+
};
|
|
140
|
+
setRemoteTrackEncoding = (trackId, encoding, reason) => {
|
|
141
|
+
const remoteTrack = this.remoteTracks[trackId];
|
|
142
|
+
if (!remoteTrack)
|
|
143
|
+
throw new Error(`Track ${trackId} not found`);
|
|
144
|
+
remoteTrack.trackContext.encoding = encoding;
|
|
145
|
+
remoteTrack.trackContext.encodingReason = reason;
|
|
146
|
+
remoteTrack.trackContext.emit('encodingChanged', remoteTrack.trackContext);
|
|
147
|
+
};
|
|
148
|
+
setRemoteTrackVadStatus = (trackId, vadStatus) => {
|
|
149
|
+
const remoteTrack = this.remoteTracks[trackId];
|
|
150
|
+
if (!remoteTrack)
|
|
151
|
+
throw new Error(`Track ${trackId} not found`);
|
|
152
|
+
if (isVadStatus(vadStatus)) {
|
|
153
|
+
remoteTrack.trackContext.vadStatus = vadStatus;
|
|
154
|
+
remoteTrack.trackContext.emit('voiceActivityChanged', remoteTrack.trackContext);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.warn('Received unknown vad status: ', vadStatus);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
getRemoteTrackContexts = () => {
|
|
161
|
+
return Object.values(this.remoteTracks).reduce((acc, current) => ({
|
|
162
|
+
...acc,
|
|
163
|
+
[current.trackContext.trackId]: current.trackContext,
|
|
164
|
+
}), {});
|
|
165
|
+
};
|
|
166
|
+
getRemoteEndpoints = () => {
|
|
167
|
+
return Object.values(this.remoteEndpoints).reduce((acc, current) => ({ ...acc, [current.id]: current }), {});
|
|
168
|
+
};
|
|
169
|
+
setTargetRemoteTrackEncoding = (trackId, variant) => {
|
|
170
|
+
const remoteTrack = this.remoteTracks[trackId];
|
|
171
|
+
if (!remoteTrack)
|
|
172
|
+
throw new Error(`Track ${trackId} not found`);
|
|
173
|
+
try {
|
|
174
|
+
remoteTrack.setTargetTrackEncoding(variant);
|
|
175
|
+
const mediaEvent = generateCustomEvent({
|
|
176
|
+
type: 'setTargetTrackVariant',
|
|
177
|
+
data: {
|
|
178
|
+
trackId: trackId,
|
|
179
|
+
variant,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
this.sendMediaEvent(mediaEvent);
|
|
183
|
+
this.emit('targetTrackEncodingRequested', {
|
|
184
|
+
trackId,
|
|
185
|
+
variant,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
console.warn(e);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
updateMLineIds = (midToTrackId) => {
|
|
193
|
+
Object.entries(midToTrackId).forEach(([mLineId, trackId]) => {
|
|
194
|
+
const remoteTrack = this.remoteTracks[trackId];
|
|
195
|
+
if (remoteTrack) {
|
|
196
|
+
remoteTrack.setMLineId(mLineId);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TrackCommon, TrackEncodings, TrackId } from './TrackCommon';
|
|
2
|
+
import type { LocalTrackId, MLineId, Encoding } from '../types';
|
|
3
|
+
import type { TrackContextImpl } from '../internal';
|
|
4
|
+
/**
|
|
5
|
+
* This is a wrapper over `TrackContext` that adds additional properties such as:
|
|
6
|
+
* - `MLineId`: required to generate sdpOffer
|
|
7
|
+
*
|
|
8
|
+
* # Lifecycle, state transitions
|
|
9
|
+
*
|
|
10
|
+
* I identified the following states
|
|
11
|
+
* - on `trackAdded`
|
|
12
|
+
* - trackContext.stream === null
|
|
13
|
+
* - trackContext.track === null
|
|
14
|
+
* - trackContext.trackKind === null
|
|
15
|
+
* - mLineId === null
|
|
16
|
+
* - on `onSdpAnswer`
|
|
17
|
+
* - trackContext.stream === null
|
|
18
|
+
* - trackContext.track === null
|
|
19
|
+
* - trackContext.trackKind === null
|
|
20
|
+
* - mLineId !== null
|
|
21
|
+
* - on `trackReady`
|
|
22
|
+
* - trackContext.stream !== null
|
|
23
|
+
* - trackContext.track !== null
|
|
24
|
+
* - trackContext.trackKind !== null
|
|
25
|
+
* - mLineId !== null
|
|
26
|
+
*/
|
|
27
|
+
export declare class RemoteTrack<EndpointMetadata, TrackMetadata> implements TrackCommon {
|
|
28
|
+
id: TrackId;
|
|
29
|
+
mLineId: MLineId | null;
|
|
30
|
+
readonly trackContext: TrackContextImpl<EndpointMetadata, TrackMetadata>;
|
|
31
|
+
readonly encodings: TrackEncodings;
|
|
32
|
+
private targetEncoding;
|
|
33
|
+
constructor(id: LocalTrackId, trackContext: TrackContextImpl<EndpointMetadata, TrackMetadata>);
|
|
34
|
+
setMLineId: (mLineId: MLineId) => void;
|
|
35
|
+
setReady: (stream: MediaStream, track: MediaStreamTrack) => void;
|
|
36
|
+
disableTrackEncoding: (encoding: Encoding) => void;
|
|
37
|
+
enableTrackEncoding: (encoding: Encoding) => void;
|
|
38
|
+
setTargetTrackEncoding: (variant: Encoding) => void;
|
|
39
|
+
}
|