livekit-client 2.18.7 → 2.18.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +2 -2
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +408 -255
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts.map +1 -1
  10. package/dist/src/logger.d.ts +11 -1
  11. package/dist/src/logger.d.ts.map +1 -1
  12. package/dist/src/room/PCTransport.d.ts +13 -3
  13. package/dist/src/room/PCTransport.d.ts.map +1 -1
  14. package/dist/src/room/PCTransportManager.d.ts +3 -1
  15. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts.map +1 -1
  18. package/dist/src/room/data-track/LocalDataTrack.d.ts +32 -0
  19. package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -1
  20. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  21. package/dist/src/room/data-track/handle.d.ts +1 -0
  22. package/dist/src/room/data-track/handle.d.ts.map +1 -1
  23. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
  24. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  25. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
  26. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  27. package/dist/src/room/data-track/outgoing/types.d.ts +7 -0
  28. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  29. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  30. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  31. package/dist/src/utils/subscribeToEvents.d.ts.map +1 -1
  32. package/dist/ts4.2/logger.d.ts +11 -1
  33. package/dist/ts4.2/room/PCTransport.d.ts +13 -3
  34. package/dist/ts4.2/room/PCTransportManager.d.ts +3 -1
  35. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +32 -0
  36. package/dist/ts4.2/room/data-track/handle.d.ts +1 -0
  37. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
  38. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
  39. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +7 -0
  40. package/package.json +1 -1
  41. package/src/api/SignalClient.ts +19 -31
  42. package/src/logger.test.ts +61 -0
  43. package/src/logger.ts +38 -4
  44. package/src/room/PCTransport.ts +26 -3
  45. package/src/room/PCTransportManager.test.ts +281 -0
  46. package/src/room/PCTransportManager.ts +45 -31
  47. package/src/room/RTCEngine.ts +34 -52
  48. package/src/room/Room.ts +37 -59
  49. package/src/room/data-track/LocalDataTrack.ts +60 -1
  50. package/src/room/data-track/RemoteDataTrack.ts +4 -1
  51. package/src/room/data-track/handle.ts +4 -0
  52. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +72 -2
  53. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +5 -3
  54. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +387 -1
  55. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +51 -3
  56. package/src/room/data-track/outgoing/types.ts +5 -0
  57. package/src/room/participant/LocalParticipant.ts +59 -144
  58. package/src/room/participant/Participant.ts +4 -1
  59. 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,13 @@ 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>;
21
+ protected isFlushed: boolean;
18
22
  /** @internal */
19
23
  constructor(options: DataTrackOptions, manager: OutgoingDataTrackManager);
24
+ private handleManagerReset;
25
+ private handleManagerPacketsFlushedChange;
20
26
  /** @internal */
21
27
  static withExplicitHandle(options: DataTrackOptions, manager: OutgoingDataTrackManager, handle: DataTrackHandle): LocalDataTrack;
22
28
  /** Metrics about the data track publication. */
@@ -39,6 +45,32 @@ export default class LocalDataTrack implements ILocalTrack, IDataTrack {
39
45
  * - The room is no longer connected
40
46
  */
41
47
  tryPush(frame: DataTrackFrame): Promise<import("@livekit/throws-transformer/throws").Throws<void, DataTrackPushFrameError<import("./outgoing/errors").DataTrackPushFrameErrorReason.TrackUnpublished> | DataTrackPushFrameError<import("./outgoing/errors").DataTrackPushFrameErrorReason.Dropped>>>;
48
+ /**
49
+ * When called, waits for all in flight packets to be sent before resolving.
50
+ *
51
+ * Use this to:
52
+ *
53
+ * 1. Send frames exactly in order:
54
+ * ```ts
55
+ * await track.tryPush(/* ... *\/);
56
+ * await track.flush();
57
+ * await track.tryPush(/* ... *\/);
58
+ * await track.flush();
59
+ * // ... etc ...
60
+ * ```
61
+ *
62
+ * 2. Wait for frames to all be delivered before unpublishing a local data track:
63
+ *
64
+ * ```ts
65
+ * await track.tryPush(/* ... *\/);
66
+ * await track.tryPush(/* ... *\/);
67
+ * await track.tryPush(/* ... *\/);
68
+ * // ... etc ...
69
+ * await track.flush();
70
+ * await track.unpublish();
71
+ * ```
72
+ **/
73
+ flush(): Promise<void>;
42
74
  /**
43
75
  * Unpublish the track from the SFU. Once this is called, any further calls to {@link tryPush}
44
76
  * will fail.
@@ -22,5 +22,6 @@ export declare class DataTrackHandleAllocator {
22
22
  value: number;
23
23
  /** Returns a unique track handle for the next publication, if one can be obtained. */
24
24
  get(): DataTrackHandle | null;
25
+ reset(): void;
25
26
  }
26
27
  //# sourceMappingURL=handle.d.ts.map
@@ -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 (hot code path) faster.
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
- /** Shutdown the manager, ending any subscriptions. */
111
- shutdown(): void;
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, EventPacketsFlushedChange, 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
+ packetsFlushedChange: (event: EventPacketsFlushedChange) => 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
- * Shuts down the manager and all associated tracks.
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
- shutdown(): Promise<void>;
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,9 @@ 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 EventPacketsFlushedChange = {
44
+ handle: DataTrackHandle;
45
+ isFlushed: boolean;
46
+ };
40
47
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.18.7",
3
+ "version": "2.18.9",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -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.debug(`connecting to ${redactedUrl}`, {
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', { ...this.logContext, error: e });
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', { ...this.logContext, offerSdp: offer.sdp });
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', { ...this.logContext, answerSdp: answer.sdp });
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', { ...this.logContext, 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', { ...this.logContext, error: e });
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', this.logContext);
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', { ...this.logContext, msgCase: msg.case });
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.debug(`websocket connection closed: ${reason}`, { ...this.logContext, reason });
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', { ...this.logContext, 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', this.logContext);
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', this.logContext);
957
+ this.log.warn('ping interval duration not set');
968
958
  return;
969
959
  }
970
- this.log.debug('start ping interval', this.logContext);
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', this.logContext);
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
- let livekitLogger = log.getLogger('livekit');
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
- return logger as StructuredLogger;
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('lk-e2ee') as StructuredLogger;
130
+ export const workerLogger = log.getLogger(LoggerNames.E2EE) as StructuredLogger;
@@ -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
- private latestOfferId: number = 0;
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
+ };