livekit-client 2.18.7 → 2.18.8
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/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +2 -2
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +391 -255
- 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/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/logger.d.ts +11 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +13 -3
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +3 -1
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/LocalDataTrack.d.ts +31 -0
- package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -1
- package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
- package/dist/src/room/data-track/handle.d.ts +1 -0
- package/dist/src/room/data-track/handle.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/types.d.ts +6 -0
- package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/utils/subscribeToEvents.d.ts.map +1 -1
- package/dist/ts4.2/logger.d.ts +11 -1
- package/dist/ts4.2/room/PCTransport.d.ts +13 -3
- package/dist/ts4.2/room/PCTransportManager.d.ts +3 -1
- package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +31 -0
- package/dist/ts4.2/room/data-track/handle.d.ts +1 -0
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
- package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/api/SignalClient.ts +19 -31
- package/src/logger.test.ts +61 -0
- package/src/logger.ts +38 -4
- package/src/room/PCTransport.ts +26 -3
- package/src/room/PCTransportManager.test.ts +281 -0
- package/src/room/PCTransportManager.ts +45 -31
- package/src/room/RTCEngine.ts +34 -52
- package/src/room/Room.ts +37 -59
- package/src/room/data-track/LocalDataTrack.ts +51 -0
- package/src/room/data-track/RemoteDataTrack.ts +4 -1
- package/src/room/data-track/handle.ts +4 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +72 -2
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +5 -3
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +235 -1
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +45 -3
- package/src/room/data-track/outgoing/types.ts +5 -0
- package/src/room/participant/LocalParticipant.ts +59 -144
- package/src/room/participant/Participant.ts +4 -1
- package/src/utils/subscribeToEvents.ts +11 -8
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { SignalTarget } from '@livekit/protocol';
|
|
2
|
+
import type { Throws } from '@livekit/throws-transformer/throws';
|
|
2
3
|
import PCTransport from './PCTransport';
|
|
4
|
+
import { NegotiationError } from './errors';
|
|
3
5
|
import type { LoggerOptions } from './types';
|
|
4
6
|
export declare enum PCTransportState {
|
|
5
7
|
NEW = 0,
|
|
@@ -43,7 +45,7 @@ export declare class PCTransportManager {
|
|
|
43
45
|
createSubscriberAnswerFromOffer(sd: RTCSessionDescriptionInit, offerId: number): Promise<RTCSessionDescriptionInit | undefined>;
|
|
44
46
|
updateConfiguration(config: RTCConfiguration, iceRestart?: boolean): void;
|
|
45
47
|
ensurePCTransportConnection(abortController?: AbortController, timeout?: number): Promise<void>;
|
|
46
|
-
negotiate(abortController: AbortController): Promise<void
|
|
48
|
+
negotiate(abortController: AbortController): Promise<Throws<void, NegotiationError | Error>>;
|
|
47
49
|
addPublisherTransceiver(track: MediaStreamTrack, transceiverInit: RTCRtpTransceiverInit): RTCRtpTransceiver;
|
|
48
50
|
addPublisherTransceiverOfKind(kind: 'audio' | 'video', transceiverInit: RTCRtpTransceiverInit): RTCRtpTransceiver;
|
|
49
51
|
getMidForReceiver(receiver: RTCRtpReceiver): string | null | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StructuredLogger } from '../../logger';
|
|
2
|
+
import { Future } from '../utils';
|
|
2
3
|
import type { DataTrackFrame } from './frame';
|
|
3
4
|
import type { DataTrackHandle } from './handle';
|
|
4
5
|
import type OutgoingDataTrackManager from './outgoing/OutgoingDataTrackManager';
|
|
@@ -15,8 +16,12 @@ export default class LocalDataTrack implements ILocalTrack, IDataTrack {
|
|
|
15
16
|
protected handle: DataTrackHandle | null;
|
|
16
17
|
protected manager: OutgoingDataTrackManager;
|
|
17
18
|
protected log: StructuredLogger;
|
|
19
|
+
/** Resolves once the data track has sent all pending packets the rtc data channel buffer. */
|
|
20
|
+
protected flushedFuture: Future<void, never>;
|
|
18
21
|
/** @internal */
|
|
19
22
|
constructor(options: DataTrackOptions, manager: OutgoingDataTrackManager);
|
|
23
|
+
private handleManagerReset;
|
|
24
|
+
private handleManagerPacketsFlushed;
|
|
20
25
|
/** @internal */
|
|
21
26
|
static withExplicitHandle(options: DataTrackOptions, manager: OutgoingDataTrackManager, handle: DataTrackHandle): LocalDataTrack;
|
|
22
27
|
/** Metrics about the data track publication. */
|
|
@@ -39,6 +44,32 @@ export default class LocalDataTrack implements ILocalTrack, IDataTrack {
|
|
|
39
44
|
* - The room is no longer connected
|
|
40
45
|
*/
|
|
41
46
|
tryPush(frame: DataTrackFrame): Promise<import("@livekit/throws-transformer/throws").Throws<void, DataTrackPushFrameError<import("./outgoing/errors").DataTrackPushFrameErrorReason.TrackUnpublished> | DataTrackPushFrameError<import("./outgoing/errors").DataTrackPushFrameErrorReason.Dropped>>>;
|
|
47
|
+
/**
|
|
48
|
+
* When called, waits for all in flight packets to be sent before resolving.
|
|
49
|
+
*
|
|
50
|
+
* Use this to:
|
|
51
|
+
*
|
|
52
|
+
* 1. Send frames exactly in order:
|
|
53
|
+
* ```ts
|
|
54
|
+
* await track.tryPush(/* ... *\/);
|
|
55
|
+
* await track.flush();
|
|
56
|
+
* await track.tryPush(/* ... *\/);
|
|
57
|
+
* await track.flush();
|
|
58
|
+
* // ... etc ...
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* 2. Wait for frames to all be delivered before unpublishing a local data track:
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* await track.tryPush(/* ... *\/);
|
|
65
|
+
* await track.tryPush(/* ... *\/);
|
|
66
|
+
* await track.tryPush(/* ... *\/);
|
|
67
|
+
* // ... etc ...
|
|
68
|
+
* await track.flush();
|
|
69
|
+
* await track.unpublish();
|
|
70
|
+
* ```
|
|
71
|
+
**/
|
|
72
|
+
flush(): Promise<void>;
|
|
42
73
|
/**
|
|
43
74
|
* Unpublish the track from the SFU. Once this is called, any further calls to {@link tryPush}
|
|
44
75
|
* will fail.
|
|
@@ -34,7 +34,7 @@ export default class IncomingDataTrackManager extends IncomingDataTrackManager_b
|
|
|
34
34
|
*
|
|
35
35
|
* This is an index that allows track descriptors to be looked up
|
|
36
36
|
* by subscriber handle in O(1) time, to make routing incoming packets
|
|
37
|
-
* a
|
|
37
|
+
* (a hot code path) faster.
|
|
38
38
|
*/
|
|
39
39
|
private subscriptionHandles;
|
|
40
40
|
constructor(options?: IncomingDataTrackManagerOptions);
|
|
@@ -107,8 +107,9 @@ export default class IncomingDataTrackManager extends IncomingDataTrackManager_b
|
|
|
107
107
|
/** Called when a remote participant is disconnected so that any pending data tracks can be
|
|
108
108
|
* cancelled. */
|
|
109
109
|
handleRemoteParticipantDisconnected(remoteParticipantIdentity: RemoteParticipant['identity']): void;
|
|
110
|
-
/**
|
|
111
|
-
|
|
110
|
+
/** Resets the manager, ending any subscriptions, and getting it ready for the next room
|
|
111
|
+
* connection. */
|
|
112
|
+
reset(): void;
|
|
112
113
|
}
|
|
113
114
|
export {};
|
|
114
115
|
//# sourceMappingURL=IncomingDataTrackManager.d.ts.map
|
|
@@ -7,7 +7,7 @@ import { DataTrackHandle } from '../handle';
|
|
|
7
7
|
import type { DataTrackInfo } from '../types';
|
|
8
8
|
import { DataTrackPublishError, DataTrackPushFrameError, DataTrackPushFrameErrorReason } from './errors';
|
|
9
9
|
import DataTrackOutgoingPipeline from './pipeline';
|
|
10
|
-
import type { DataTrackOptions, EventPacketAvailable, EventSfuPublishRequest, EventSfuUnpublishRequest, EventTrackPublished, EventTrackUnpublished, SfuPublishResponseResult } from './types';
|
|
10
|
+
import type { DataTrackOptions, EventPacketAvailable, EventPacketsFlushed, EventSfuPublishRequest, EventSfuUnpublishRequest, EventTrackPublished, EventTrackUnpublished, SfuPublishResponseResult } from './types';
|
|
11
11
|
export type PendingDescriptor = {
|
|
12
12
|
type: 'pending';
|
|
13
13
|
/** Resolves when the descriptor is fully published. */
|
|
@@ -37,6 +37,11 @@ export type DataTrackOutgoingManagerCallbacks = {
|
|
|
37
37
|
trackPublished: (event: EventTrackPublished) => void;
|
|
38
38
|
/** A {@link LocalDataTrack} has been unpublished */
|
|
39
39
|
trackUnpublished: (event: EventTrackUnpublished) => void;
|
|
40
|
+
/** A {@link LocalDataTrack} has had all of its in flight packets sent via the rtc data channel. */
|
|
41
|
+
packetsFlushed: (event: EventPacketsFlushed) => void;
|
|
42
|
+
/** The manager has been reset and all state has been cleared in preparation for the next room
|
|
43
|
+
* connection. */
|
|
44
|
+
reset: () => void;
|
|
40
45
|
};
|
|
41
46
|
type OutgoingDataTrackManagerOptions = {
|
|
42
47
|
/**
|
|
@@ -51,6 +56,10 @@ export default class OutgoingDataTrackManager extends OutgoingDataTrackManager_b
|
|
|
51
56
|
private e2eeManager;
|
|
52
57
|
private handleAllocator;
|
|
53
58
|
private descriptors;
|
|
59
|
+
/** Number of packets for each data track which have been emitted via the `packetAvailable` event
|
|
60
|
+
* and which have not yet been sent via the rtc data channel yet. Once this goes to 0, then
|
|
61
|
+
* all in flight packets have been delivered, and the data tracks is "flushed". */
|
|
62
|
+
private inFlightPacketCounter;
|
|
54
63
|
constructor(options?: OutgoingDataTrackManagerOptions);
|
|
55
64
|
static withDescriptors(descriptors: Map<DataTrackHandle, Descriptor>): OutgoingDataTrackManager;
|
|
56
65
|
/** @internal */
|
|
@@ -65,6 +74,11 @@ export default class OutgoingDataTrackManager extends OutgoingDataTrackManager_b
|
|
|
65
74
|
* @internal
|
|
66
75
|
*/
|
|
67
76
|
tryProcessAndSend(handle: DataTrackHandle, frame: DataTrackFrameInternal): Promise<Throws<void, DataTrackPushFrameError<DataTrackPushFrameErrorReason.Dropped> | DataTrackPushFrameError<DataTrackPushFrameErrorReason.TrackUnpublished>>>;
|
|
77
|
+
/** The client has sent a packet over the rtc data channel. This signal is used for determining
|
|
78
|
+
* once all packets are sent and a data track has been "flushed".
|
|
79
|
+
*
|
|
80
|
+
* @internal */
|
|
81
|
+
handlePacketSendComplete(handle: DataTrackHandle): void;
|
|
68
82
|
/**
|
|
69
83
|
* Client requested to publish a track.
|
|
70
84
|
*
|
|
@@ -102,10 +116,11 @@ export default class OutgoingDataTrackManager extends OutgoingDataTrackManager_b
|
|
|
102
116
|
*/
|
|
103
117
|
sfuWillRepublishTracks(): void;
|
|
104
118
|
/**
|
|
105
|
-
*
|
|
119
|
+
* Reset's the state of the manager and all associated tracks. Run on room disconnect to get
|
|
120
|
+
* the manager ready for the next room connection.
|
|
106
121
|
* @internal
|
|
107
122
|
**/
|
|
108
|
-
|
|
123
|
+
reset(): Promise<void>;
|
|
109
124
|
}
|
|
110
125
|
export {};
|
|
111
126
|
//# sourceMappingURL=OutgoingDataTrackManager.d.ts.map
|
|
@@ -26,6 +26,8 @@ export type EventSfuUnpublishRequest = {
|
|
|
26
26
|
};
|
|
27
27
|
/** A serialized packet is ready to be sent over the transport. */
|
|
28
28
|
export type EventPacketAvailable = {
|
|
29
|
+
/** The handle associated with the data track which this packet bytes belong to. */
|
|
30
|
+
handle: DataTrackHandle;
|
|
29
31
|
bytes: Uint8Array;
|
|
30
32
|
};
|
|
31
33
|
/** A track has been created by a local participant and is available to be
|
|
@@ -37,4 +39,8 @@ export type EventTrackPublished = {
|
|
|
37
39
|
export type EventTrackUnpublished = {
|
|
38
40
|
sid: DataTrackSid;
|
|
39
41
|
};
|
|
42
|
+
/** A track has had all of its in flight packets sent via the rtc data channel. */
|
|
43
|
+
export type EventPacketsFlushed = {
|
|
44
|
+
handle: DataTrackHandle;
|
|
45
|
+
};
|
|
40
46
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
package/src/api/SignalClient.ts
CHANGED
|
@@ -247,8 +247,8 @@ export class SignalClient {
|
|
|
247
247
|
private useV0SignalPath = false;
|
|
248
248
|
|
|
249
249
|
constructor(useJSON: boolean = false, loggerOptions: LoggerOptions = {}) {
|
|
250
|
-
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.Signal);
|
|
251
250
|
this.loggerContextCb = loggerOptions.loggerContextCb;
|
|
251
|
+
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.Signal, () => this.logContext);
|
|
252
252
|
this.useJSON = useJSON;
|
|
253
253
|
this.requestQueue = new AsyncQueue();
|
|
254
254
|
this.queuedRequests = [];
|
|
@@ -284,10 +284,7 @@ export class SignalClient {
|
|
|
284
284
|
reason?: ReconnectReason,
|
|
285
285
|
): Promise<ReconnectResponse | undefined> {
|
|
286
286
|
if (!this.options) {
|
|
287
|
-
this.log.warn(
|
|
288
|
-
'attempted to reconnect without signal options being set, ignoring',
|
|
289
|
-
this.logContext,
|
|
290
|
-
);
|
|
287
|
+
this.log.warn('attempted to reconnect without signal options being set, ignoring');
|
|
291
288
|
return;
|
|
292
289
|
}
|
|
293
290
|
this.state = SignalConnectionState.RECONNECTING;
|
|
@@ -377,10 +374,9 @@ export class SignalClient {
|
|
|
377
374
|
if (redactedUrl.searchParams.has('access_token')) {
|
|
378
375
|
redactedUrl.searchParams.set('access_token', '<redacted>');
|
|
379
376
|
}
|
|
380
|
-
this.log.
|
|
377
|
+
this.log.info(`signal connecting to ${redactedUrl}`, {
|
|
381
378
|
reconnect: opts.reconnect,
|
|
382
379
|
reconnectReason: opts.reconnectReason,
|
|
383
|
-
...this.logContext,
|
|
384
380
|
});
|
|
385
381
|
if (this.ws) {
|
|
386
382
|
await this.close(false);
|
|
@@ -399,7 +395,6 @@ export class SignalClient {
|
|
|
399
395
|
}
|
|
400
396
|
if (closeInfo.closeCode !== 1000) {
|
|
401
397
|
this.log.warn(`websocket closed`, {
|
|
402
|
-
...this.logContext,
|
|
403
398
|
reason: closeInfo.reason,
|
|
404
399
|
code: closeInfo.closeCode,
|
|
405
400
|
wasClean: closeInfo.closeCode === 1000,
|
|
@@ -466,7 +461,6 @@ export class SignalClient {
|
|
|
466
461
|
|
|
467
462
|
if (this.pingTimeoutDuration && this.pingTimeoutDuration > 0) {
|
|
468
463
|
this.log.debug('ping config', {
|
|
469
|
-
...this.logContext,
|
|
470
464
|
timeout: this.pingTimeoutDuration,
|
|
471
465
|
interval: this.pingIntervalDuration,
|
|
472
466
|
});
|
|
@@ -555,7 +549,7 @@ export class SignalClient {
|
|
|
555
549
|
await Promise.race([closePromise, sleep(MAX_WS_CLOSE_TIME)]);
|
|
556
550
|
}
|
|
557
551
|
} catch (e) {
|
|
558
|
-
this.log.debug('websocket error while closing', {
|
|
552
|
+
this.log.debug('websocket error while closing', { error: e });
|
|
559
553
|
} finally {
|
|
560
554
|
if (updateState) {
|
|
561
555
|
this.state = SignalConnectionState.DISCONNECTED;
|
|
@@ -566,7 +560,7 @@ export class SignalClient {
|
|
|
566
560
|
|
|
567
561
|
// initial offer after joining
|
|
568
562
|
sendOffer(offer: RTCSessionDescriptionInit, offerId: number) {
|
|
569
|
-
this.log.debug('sending offer', {
|
|
563
|
+
this.log.debug('sending offer', { offerSdp: offer.sdp });
|
|
570
564
|
this.sendRequest({
|
|
571
565
|
case: 'offer',
|
|
572
566
|
value: toProtoSessionDescription(offer, offerId),
|
|
@@ -575,7 +569,7 @@ export class SignalClient {
|
|
|
575
569
|
|
|
576
570
|
// answer a server-initiated offer
|
|
577
571
|
sendAnswer(answer: RTCSessionDescriptionInit, offerId: number) {
|
|
578
|
-
this.log.debug('sending answer', {
|
|
572
|
+
this.log.debug('sending answer', { answerSdp: answer.sdp });
|
|
579
573
|
return this.sendRequest({
|
|
580
574
|
case: 'answer',
|
|
581
575
|
value: toProtoSessionDescription(answer, offerId),
|
|
@@ -583,7 +577,7 @@ export class SignalClient {
|
|
|
583
577
|
}
|
|
584
578
|
|
|
585
579
|
sendIceCandidate(candidate: RTCIceCandidateInit, target: SignalTarget) {
|
|
586
|
-
this.log.debug('sending ice candidate', {
|
|
580
|
+
this.log.debug('sending ice candidate', { candidate });
|
|
587
581
|
return this.sendRequest({
|
|
588
582
|
case: 'trickle',
|
|
589
583
|
value: new TrickleRequest({
|
|
@@ -768,10 +762,7 @@ export class SignalClient {
|
|
|
768
762
|
return;
|
|
769
763
|
}
|
|
770
764
|
if (!this.streamWriter) {
|
|
771
|
-
this.log.error(
|
|
772
|
-
`cannot send signal request before connected, type: ${message?.case}`,
|
|
773
|
-
this.logContext,
|
|
774
|
-
);
|
|
765
|
+
this.log.error(`cannot send signal request before connected, type: ${message?.case}`);
|
|
775
766
|
return;
|
|
776
767
|
}
|
|
777
768
|
const req = new SignalRequest({ message });
|
|
@@ -783,14 +774,14 @@ export class SignalClient {
|
|
|
783
774
|
await this.streamWriter.write(req.toBinary());
|
|
784
775
|
}
|
|
785
776
|
} catch (e) {
|
|
786
|
-
this.log.error('error sending signal message', {
|
|
777
|
+
this.log.error('error sending signal message', { error: e });
|
|
787
778
|
}
|
|
788
779
|
}
|
|
789
780
|
|
|
790
781
|
private handleSignalResponse(res: SignalResponse) {
|
|
791
782
|
const msg = res.message;
|
|
792
783
|
if (msg == undefined) {
|
|
793
|
-
this.log.debug('received unsupported message'
|
|
784
|
+
this.log.debug('received unsupported message');
|
|
794
785
|
return;
|
|
795
786
|
}
|
|
796
787
|
|
|
@@ -899,7 +890,7 @@ export class SignalClient {
|
|
|
899
890
|
this.onDataTrackSubscriberHandles(msg.value);
|
|
900
891
|
}
|
|
901
892
|
} else {
|
|
902
|
-
this.log.debug('unsupported message', {
|
|
893
|
+
this.log.debug('unsupported message', { msgCase: msg.case });
|
|
903
894
|
}
|
|
904
895
|
|
|
905
896
|
if (!pingHandled) {
|
|
@@ -920,14 +911,14 @@ export class SignalClient {
|
|
|
920
911
|
if (this.state === SignalConnectionState.DISCONNECTED) return;
|
|
921
912
|
const onCloseCallback = this.onClose;
|
|
922
913
|
await this.close(undefined, reason);
|
|
923
|
-
this.log.
|
|
914
|
+
this.log.info(`websocket connection closed: ${reason}`, { reason });
|
|
924
915
|
if (onCloseCallback) {
|
|
925
916
|
onCloseCallback(reason);
|
|
926
917
|
}
|
|
927
918
|
}
|
|
928
919
|
|
|
929
920
|
private handleWSError(error: unknown) {
|
|
930
|
-
this.log.error('websocket error', {
|
|
921
|
+
this.log.error('websocket error', { error });
|
|
931
922
|
}
|
|
932
923
|
|
|
933
924
|
/**
|
|
@@ -937,7 +928,7 @@ export class SignalClient {
|
|
|
937
928
|
private resetPingTimeout() {
|
|
938
929
|
this.clearPingTimeout();
|
|
939
930
|
if (!this.pingTimeoutDuration) {
|
|
940
|
-
this.log.warn('ping timeout duration not set'
|
|
931
|
+
this.log.warn('ping timeout duration not set');
|
|
941
932
|
return;
|
|
942
933
|
}
|
|
943
934
|
this.pingTimeout = CriticalTimers.setTimeout(() => {
|
|
@@ -945,7 +936,6 @@ export class SignalClient {
|
|
|
945
936
|
`ping timeout triggered. last pong received at: ${new Date(
|
|
946
937
|
Date.now() - this.pingTimeoutDuration! * 1000,
|
|
947
938
|
).toUTCString()}`,
|
|
948
|
-
this.logContext,
|
|
949
939
|
);
|
|
950
940
|
this.handleOnClose('ping timeout');
|
|
951
941
|
}, this.pingTimeoutDuration * 1000);
|
|
@@ -964,17 +954,17 @@ export class SignalClient {
|
|
|
964
954
|
this.clearPingInterval();
|
|
965
955
|
this.resetPingTimeout();
|
|
966
956
|
if (!this.pingIntervalDuration) {
|
|
967
|
-
this.log.warn('ping interval duration not set'
|
|
957
|
+
this.log.warn('ping interval duration not set');
|
|
968
958
|
return;
|
|
969
959
|
}
|
|
970
|
-
this.log.debug('start ping interval'
|
|
960
|
+
this.log.debug('start ping interval');
|
|
971
961
|
this.pingInterval = CriticalTimers.setInterval(() => {
|
|
972
962
|
this.sendPing();
|
|
973
963
|
}, this.pingIntervalDuration * 1000);
|
|
974
964
|
}
|
|
975
965
|
|
|
976
966
|
private clearPingInterval() {
|
|
977
|
-
this.log.debug('clearing ping interval'
|
|
967
|
+
this.log.debug('clearing ping interval');
|
|
978
968
|
this.clearPingTimeout();
|
|
979
969
|
if (this.pingInterval) {
|
|
980
970
|
CriticalTimers.clearInterval(this.pingInterval);
|
|
@@ -994,6 +984,7 @@ export class SignalClient {
|
|
|
994
984
|
firstMessage?: SignalResponse,
|
|
995
985
|
) {
|
|
996
986
|
this.state = SignalConnectionState.CONNECTED;
|
|
987
|
+
this.log.info('signal connected');
|
|
997
988
|
clearTimeout(timeoutHandle);
|
|
998
989
|
this.startPingInterval();
|
|
999
990
|
this.startReadingLoop(connection.readable.getReader(), firstMessage);
|
|
@@ -1031,10 +1022,7 @@ export class SignalClient {
|
|
|
1031
1022
|
};
|
|
1032
1023
|
} else {
|
|
1033
1024
|
// in reconnecting, any message received means signal reconnected and we still need to process it
|
|
1034
|
-
this.log.debug(
|
|
1035
|
-
'declaring signal reconnected without reconnect response received',
|
|
1036
|
-
this.logContext,
|
|
1037
|
-
);
|
|
1025
|
+
this.log.debug('declaring signal reconnected without reconnect response received');
|
|
1038
1026
|
return {
|
|
1039
1027
|
isValid: true,
|
|
1040
1028
|
response: undefined,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as loglevel from 'loglevel';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
type LogExtension,
|
|
5
|
+
LogLevel,
|
|
6
|
+
LoggerNames,
|
|
7
|
+
type StructuredLogger,
|
|
8
|
+
getLogger,
|
|
9
|
+
setLogExtension,
|
|
10
|
+
setLogLevel,
|
|
11
|
+
} from './logger';
|
|
12
|
+
|
|
13
|
+
describe('getLogger with context provider', () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
setLogLevel(LogLevel.info);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const hookBase = (name: LoggerNames, extension: LogExtension) => {
|
|
19
|
+
const base = loglevel.getLogger(name) as StructuredLogger;
|
|
20
|
+
setLogExtension(extension, base);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
it('omits the prefix when the bound context has no display keys', () => {
|
|
24
|
+
const extension = vi.fn<LogExtension>();
|
|
25
|
+
hookBase(LoggerNames.Room, extension);
|
|
26
|
+
setLogLevel(LogLevel.info, LoggerNames.Room);
|
|
27
|
+
|
|
28
|
+
const log = getLogger(LoggerNames.Room, () => ({ irrelevant: 'x' }));
|
|
29
|
+
log.info('plain');
|
|
30
|
+
|
|
31
|
+
expect(extension).toHaveBeenCalledWith(LogLevel.info, 'plain', { irrelevant: 'x' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('reflects dynamic changes to the bound context on every call', () => {
|
|
35
|
+
const extension = vi.fn<LogExtension>();
|
|
36
|
+
hookBase(LoggerNames.Engine, extension);
|
|
37
|
+
setLogLevel(LogLevel.info, LoggerNames.Engine);
|
|
38
|
+
|
|
39
|
+
let current: Record<string, string> = { room: 'r1' };
|
|
40
|
+
const log = getLogger(LoggerNames.Engine, () => current);
|
|
41
|
+
|
|
42
|
+
log.info('first');
|
|
43
|
+
current = { room: 'r2', participant: 'bob' };
|
|
44
|
+
log.info('second');
|
|
45
|
+
|
|
46
|
+
const infos = extension.mock.calls.filter((c) => c[0] === LogLevel.info);
|
|
47
|
+
expect(infos[0][2]).toEqual({ room: 'r1' });
|
|
48
|
+
expect(infos[1][2]).toEqual({ room: 'r2', participant: 'bob' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns an unwrapped logger when no context provider is supplied', () => {
|
|
52
|
+
const extension = vi.fn<LogExtension>();
|
|
53
|
+
hookBase(LoggerNames.Signal, extension);
|
|
54
|
+
setLogLevel(LogLevel.info, LoggerNames.Signal);
|
|
55
|
+
|
|
56
|
+
const log = getLogger(LoggerNames.Signal);
|
|
57
|
+
log.info('raw');
|
|
58
|
+
|
|
59
|
+
expect(extension).toHaveBeenCalledWith(LogLevel.info, 'raw', undefined);
|
|
60
|
+
});
|
|
61
|
+
});
|
package/src/logger.ts
CHANGED
|
@@ -37,7 +37,9 @@ export type StructuredLogger = log.Logger & {
|
|
|
37
37
|
getLevel: () => number;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
export type ContextProvider = () => object | undefined;
|
|
41
|
+
|
|
42
|
+
let livekitLogger = log.getLogger(LoggerNames.Default);
|
|
41
43
|
const livekitLoggers = Object.values(LoggerNames).map((name) => log.getLogger(name));
|
|
42
44
|
|
|
43
45
|
livekitLogger.setDefaultLevel(LogLevel.info);
|
|
@@ -46,11 +48,43 @@ export default livekitLogger as StructuredLogger;
|
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
50
|
* @internal
|
|
51
|
+
*
|
|
52
|
+
* Get a named logger. When `ctxFn` is supplied, every log call
|
|
53
|
+
* automatically:
|
|
54
|
+
* 1. prepends a `[key=value ...]` prefix derived from `ctxFn()` to the
|
|
55
|
+
* message string, so identifiers are visible in browser devtools
|
|
56
|
+
* without expanding the structured context object, and
|
|
57
|
+
* 2. merges `ctxFn()` into the structured context passed to any
|
|
58
|
+
* `setLogExtension` consumer, so ingestion pipelines continue to
|
|
59
|
+
* receive the full metadata unchanged.
|
|
49
60
|
*/
|
|
50
|
-
export function getLogger(name: string) {
|
|
61
|
+
export function getLogger(name: string, ctxFn?: ContextProvider) {
|
|
51
62
|
const logger = log.getLogger(name);
|
|
52
63
|
logger.setDefaultLevel(livekitLogger.getLevel());
|
|
53
|
-
|
|
64
|
+
if (!ctxFn) {
|
|
65
|
+
return logger as StructuredLogger;
|
|
66
|
+
}
|
|
67
|
+
return wrapWithContext(logger as StructuredLogger, ctxFn);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function wrapWithContext(base: StructuredLogger, ctxFn: ContextProvider): StructuredLogger {
|
|
71
|
+
type LogMethod = 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
|
72
|
+
// Resolve the underlying method on every call so that later
|
|
73
|
+
// setLogExtension installations (which replace the base logger's
|
|
74
|
+
// methods via loglevel's methodFactory) are picked up.
|
|
75
|
+
const wrap = (method: LogMethod) => (msg: string, extra?: object) => {
|
|
76
|
+
const ctx = ctxFn();
|
|
77
|
+
const merged = ctx || extra ? { ...ctx, ...extra } : undefined;
|
|
78
|
+
base[method](msg, merged);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const proxy = Object.create(base) as StructuredLogger;
|
|
82
|
+
proxy.trace = wrap('trace');
|
|
83
|
+
proxy.debug = wrap('debug');
|
|
84
|
+
proxy.info = wrap('info');
|
|
85
|
+
proxy.warn = wrap('warn');
|
|
86
|
+
proxy.error = wrap('error');
|
|
87
|
+
return proxy;
|
|
54
88
|
}
|
|
55
89
|
|
|
56
90
|
export function setLogLevel(level: LogLevel | LogLevelString, loggerName?: LoggerNames) {
|
|
@@ -93,4 +127,4 @@ export function setLogExtension(extension: LogExtension, logger?: StructuredLogg
|
|
|
93
127
|
});
|
|
94
128
|
}
|
|
95
129
|
|
|
96
|
-
export const workerLogger = log.getLogger(
|
|
130
|
+
export const workerLogger = log.getLogger(LoggerNames.E2EE) as StructuredLogger;
|
package/src/room/PCTransport.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Mutex } from '@livekit/mutex';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
3
|
import { parse, write } from 'sdp-transform';
|
|
4
|
-
import type { MediaDescription, SessionDescription } from 'sdp-transform';
|
|
4
|
+
import type { MediaAttributes, MediaDescription, SessionDescription } from 'sdp-transform';
|
|
5
|
+
import type TypedEmitter from 'typed-emitter';
|
|
5
6
|
import log, { LoggerNames, getLogger } from '../logger';
|
|
6
7
|
import { debounce } from './debounce';
|
|
7
8
|
import { NegotiationError, UnexpectedConnectionState } from './errors';
|
|
@@ -29,11 +30,16 @@ const debounceInterval = 20;
|
|
|
29
30
|
export const PCEvents = {
|
|
30
31
|
NegotiationStarted: 'negotiationStarted',
|
|
31
32
|
NegotiationComplete: 'negotiationComplete',
|
|
33
|
+
// Fired with the offerId for every successful publisher answer application,
|
|
34
|
+
// including answers that immediately recurse into another offer via
|
|
35
|
+
// `renegotiate`. Use this rather than NegotiationComplete to know that a
|
|
36
|
+
// specific offer has been negotiated end-to-end.
|
|
37
|
+
OfferAnswered: 'offerAnswered',
|
|
32
38
|
RTPVideoPayloadTypes: 'rtpVideoPayloadTypes',
|
|
33
39
|
} as const;
|
|
34
40
|
|
|
35
41
|
/** @internal */
|
|
36
|
-
export default class PCTransport extends EventEmitter {
|
|
42
|
+
export default class PCTransport extends (EventEmitter as new () => TypedEmitter<PCTransportEventCallbacks>) {
|
|
37
43
|
private _pc: RTCPeerConnection | null;
|
|
38
44
|
|
|
39
45
|
private get pc() {
|
|
@@ -51,7 +57,9 @@ export default class PCTransport extends EventEmitter {
|
|
|
51
57
|
|
|
52
58
|
private ddExtID = 0;
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
latestOfferId: number = 0;
|
|
61
|
+
|
|
62
|
+
latestAcknowledgedOfferId: number = 0;
|
|
55
63
|
|
|
56
64
|
private offerLock: Mutex;
|
|
57
65
|
|
|
@@ -236,6 +244,14 @@ export default class PCTransport extends EventEmitter {
|
|
|
236
244
|
this.pendingCandidates = [];
|
|
237
245
|
this.restartingIce = false;
|
|
238
246
|
|
|
247
|
+
// Fire OfferAnswered for every successfully applied answer, including the
|
|
248
|
+
// ones that recurse into another offer via `renegotiate`. Callers waiting
|
|
249
|
+
// on a specific offerId can resolve as soon as their offer's answer is in.
|
|
250
|
+
if (sd.type === 'answer') {
|
|
251
|
+
this.latestAcknowledgedOfferId = offerId;
|
|
252
|
+
this.emit(PCEvents.OfferAnswered, offerId);
|
|
253
|
+
}
|
|
254
|
+
|
|
239
255
|
if (this.renegotiate) {
|
|
240
256
|
this.renegotiate = false;
|
|
241
257
|
await this.createAndSendOffer();
|
|
@@ -737,3 +753,10 @@ function ensureIPAddrMatchVersion(media: MediaDescription) {
|
|
|
737
753
|
function getMidString(mid: string | number) {
|
|
738
754
|
return typeof mid === 'number' ? mid.toFixed(0) : mid;
|
|
739
755
|
}
|
|
756
|
+
|
|
757
|
+
type PCTransportEventCallbacks = {
|
|
758
|
+
negotiationStarted: () => void;
|
|
759
|
+
negotiationComplete: () => void;
|
|
760
|
+
offerAnswered: (offerId: number) => void;
|
|
761
|
+
rtpVideoPayloadTypes: (attributes: MediaAttributes['rtp']) => void;
|
|
762
|
+
};
|