livekit-client 2.21.0 → 2.22.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 +10 -10
- 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 +189 -2
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2243 -270
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +189 -2
- package/dist/livekit-client.fm.worker.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 +25 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/src/api/SignalClientStateMachine.d.ts.map +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +2 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +22 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +51 -3
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +27 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +10 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/src/room/statsSummary.d.ts +13 -0
- package/dist/src/room/statsSummary.d.ts.map +1 -0
- package/dist/src/room/token-source/TokenSource.d.ts +19 -10
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- package/dist/src/room/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts +3 -0
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +10 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +15 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/promiseState.d.ts +12 -0
- package/dist/src/test/promiseState.d.ts.map +1 -0
- package/dist/src/test/signalToken.d.ts.map +1 -1
- package/dist/src/utils/AsyncQueue.d.ts +3 -3
- package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
- package/dist/src/utils/machineInspector.d.ts +54 -0
- package/dist/src/utils/machineInspector.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +25 -1
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/ts4.2/index.d.ts +3 -2
- package/dist/ts4.2/logger.d.ts +2 -1
- package/dist/ts4.2/options.d.ts +22 -0
- package/dist/ts4.2/room/PCTransport.d.ts +51 -3
- package/dist/ts4.2/room/RTCEngine.d.ts +27 -3
- package/dist/ts4.2/room/Room.d.ts +10 -1
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/ts4.2/room/statsSummary.d.ts +13 -0
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
- package/dist/ts4.2/room/track/Track.d.ts +10 -0
- package/dist/ts4.2/room/utils.d.ts +15 -0
- package/dist/ts4.2/test/promiseState.d.ts +12 -0
- package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
- package/dist/ts4.2/utils/machineInspector.d.ts +54 -0
- package/package.json +19 -12
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/api/SignalClient.test.ts +320 -8
- package/src/api/SignalClient.ts +260 -82
- package/src/api/SignalClientStateMachine.test.ts +472 -0
- package/src/api/SignalClientStateMachine.ts +180 -0
- package/src/api/WebSocketStream.ts +19 -3
- package/src/api/utils.test.ts +20 -1
- package/src/api/utils.ts +5 -0
- package/src/e2ee/utils.ts +1 -1
- package/src/index.ts +5 -0
- package/src/logger.ts +1 -0
- package/src/options.ts +24 -0
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +183 -81
- package/src/room/RTCEngine.test.ts +339 -2
- package/src/room/RTCEngine.ts +168 -18
- package/src/room/Room.test.ts +134 -3
- package/src/room/Room.ts +122 -22
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +1 -1
- package/src/room/data-stream/incoming/StreamReader.ts +1 -1
- package/src/room/errors.ts +1 -2
- package/src/room/events.ts +1 -1
- package/src/room/statsSummary.ts +187 -0
- package/src/room/token-source/TokenSource.ts +25 -12
- package/src/room/token-source/test-tokens.ts +20 -0
- package/src/room/token-source/utils.test.ts +27 -0
- package/src/room/token-source/utils.ts +12 -5
- package/src/room/track/LocalAudioTrack.ts +9 -3
- package/src/room/track/LocalVideoTrack.test.ts +105 -2
- package/src/room/track/LocalVideoTrack.ts +36 -10
- package/src/room/track/RemoteTrack.test.ts +144 -0
- package/src/room/track/RemoteTrack.ts +39 -12
- package/src/room/track/Track.ts +29 -1
- package/src/room/utils.test.ts +94 -2
- package/src/room/utils.ts +53 -1
- package/src/test/promiseState.ts +23 -0
- package/src/test/signalServerSetup.ts +2 -1
- package/src/test/signalToken.ts +17 -13
- package/src/type-polyfills/header-extensions.d.ts +13 -0
- package/src/utils/AsyncQueue.ts +3 -3
- package/src/utils/machineInspector.ts +90 -0
package/src/room/Room.ts
CHANGED
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
type RpcInvocationData,
|
|
92
92
|
RpcServerManager,
|
|
93
93
|
} from './rpc';
|
|
94
|
+
import { summarizeStatsReport } from './statsSummary';
|
|
94
95
|
import CriticalTimers from './timers';
|
|
95
96
|
import LocalAudioTrack from './track/LocalAudioTrack';
|
|
96
97
|
import type LocalTrack from './track/LocalTrack';
|
|
@@ -113,6 +114,7 @@ import {
|
|
|
113
114
|
Future,
|
|
114
115
|
createDummyVideoStreamTrack,
|
|
115
116
|
extractChatMessage,
|
|
117
|
+
extractTrackSid,
|
|
116
118
|
extractTranscriptionSegments,
|
|
117
119
|
getDisconnectReasonFromConnectionError,
|
|
118
120
|
getEmptyAudioStreamTrack,
|
|
@@ -142,6 +144,7 @@ export enum ConnectionState {
|
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
const CONNECTION_RECONCILE_FREQUENCY_MS = 4 * 1000;
|
|
147
|
+
const STATS_LOG_FREQUENCY_MS = 30 * 1000;
|
|
145
148
|
|
|
146
149
|
/**
|
|
147
150
|
* In LiveKit, a room is the logical grouping for a list of participants.
|
|
@@ -206,6 +209,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
206
209
|
|
|
207
210
|
private connectionReconcileInterval?: ReturnType<typeof setInterval>;
|
|
208
211
|
|
|
212
|
+
private statsLogInterval?: ReturnType<typeof setInterval>;
|
|
213
|
+
|
|
209
214
|
private regionUrlProvider?: RegionUrlProvider;
|
|
210
215
|
|
|
211
216
|
private regionUrl?: string;
|
|
@@ -214,10 +219,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
214
219
|
|
|
215
220
|
private log = log;
|
|
216
221
|
|
|
222
|
+
private statsLog = log;
|
|
223
|
+
|
|
217
224
|
private bufferedEvents: Array<any> = [];
|
|
218
225
|
|
|
219
226
|
private isResuming: boolean = false;
|
|
220
227
|
|
|
228
|
+
private pendingTrackAddedCallbacks = new Map<Track.SID, Set<() => void>>();
|
|
229
|
+
|
|
221
230
|
/**
|
|
222
231
|
* map to store first point in time when a particular transcription segment was received
|
|
223
232
|
*/
|
|
@@ -251,6 +260,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
251
260
|
this.options = { ...roomOptionDefaults, ...options };
|
|
252
261
|
|
|
253
262
|
this.log = getLogger(this.options.loggerName ?? LoggerNames.Room, () => this.logContext);
|
|
263
|
+
// its own logger name, so the stats dumps can be silenced or routed
|
|
264
|
+
// separately from the rest of the room's logs
|
|
265
|
+
this.statsLog = getLogger(LoggerNames.Stats, () => this.logContext);
|
|
254
266
|
this.transcriptionReceivedTimes = new Map();
|
|
255
267
|
|
|
256
268
|
this.options.audioCaptureDefaults = {
|
|
@@ -268,7 +280,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
268
280
|
|
|
269
281
|
this.maybeCreateEngine();
|
|
270
282
|
|
|
271
|
-
this.incomingDataStreamManager = new IncomingDataStreamManager(
|
|
283
|
+
this.incomingDataStreamManager = new IncomingDataStreamManager(
|
|
284
|
+
this.options.dataStream?.maxPayloadByteLength,
|
|
285
|
+
);
|
|
272
286
|
this.outgoingDataStreamManager = new OutgoingDataStreamManager(
|
|
273
287
|
this.engine,
|
|
274
288
|
this.log,
|
|
@@ -636,10 +650,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
636
650
|
this.emitBufferedEvents();
|
|
637
651
|
})
|
|
638
652
|
.on(EngineEvent.SignalResumed, () => {
|
|
639
|
-
this.bufferedEvents = [];
|
|
640
653
|
if (this.state === ConnectionState.Reconnecting || this.isResuming) {
|
|
641
654
|
this.sendSyncState();
|
|
642
655
|
}
|
|
656
|
+
this.emitBufferedEvents();
|
|
643
657
|
})
|
|
644
658
|
.on(EngineEvent.Restarting, this.handleRestarting)
|
|
645
659
|
.on(EngineEvent.Restarted, this.handleRestarted)
|
|
@@ -1588,24 +1602,38 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1588
1602
|
// at that time, ICE connectivity has not been established so the track is not
|
|
1589
1603
|
// technically subscribed.
|
|
1590
1604
|
// We'll defer these events until when the room is connected or eventually disconnected.
|
|
1591
|
-
if (
|
|
1605
|
+
if ([ConnectionState.Connecting, ConnectionState.Reconnecting].includes(this.state)) {
|
|
1606
|
+
const pendingTrackSid = extractTrackSid(mediaTrack, stream);
|
|
1607
|
+
this.log.debug('deferring on track for later', {
|
|
1608
|
+
mediaTrackId: mediaTrack.id,
|
|
1609
|
+
mediaStreamId: stream.id,
|
|
1610
|
+
tracksInStream: stream.getTracks().map((track) => track.id),
|
|
1611
|
+
});
|
|
1592
1612
|
const reconnectedHandler = () => {
|
|
1593
|
-
this.log.debug('deferring on track for later', {
|
|
1594
|
-
mediaTrackId: mediaTrack.id,
|
|
1595
|
-
mediaStreamId: stream.id,
|
|
1596
|
-
tracksInStream: stream.getTracks().map((track) => track.id),
|
|
1597
|
-
});
|
|
1598
|
-
this.onTrackAdded(mediaTrack, stream, receiver);
|
|
1599
1613
|
cleanup();
|
|
1614
|
+
this.onTrackAdded(mediaTrack, stream, receiver);
|
|
1600
1615
|
};
|
|
1601
1616
|
const cleanup = () => {
|
|
1602
1617
|
this.off(RoomEvent.Reconnected, reconnectedHandler);
|
|
1603
1618
|
this.off(RoomEvent.Connected, reconnectedHandler);
|
|
1604
1619
|
this.off(RoomEvent.Disconnected, cleanup);
|
|
1620
|
+
if (pendingTrackSid) {
|
|
1621
|
+
const pendingCallbacks = this.pendingTrackAddedCallbacks.get(pendingTrackSid);
|
|
1622
|
+
pendingCallbacks?.delete(cleanup);
|
|
1623
|
+
if (pendingCallbacks?.size === 0) {
|
|
1624
|
+
this.pendingTrackAddedCallbacks.delete(pendingTrackSid);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1605
1627
|
};
|
|
1606
1628
|
this.once(RoomEvent.Reconnected, reconnectedHandler);
|
|
1607
1629
|
this.once(RoomEvent.Connected, reconnectedHandler);
|
|
1608
1630
|
this.once(RoomEvent.Disconnected, cleanup);
|
|
1631
|
+
if (pendingTrackSid) {
|
|
1632
|
+
const pendingCallbacks =
|
|
1633
|
+
this.pendingTrackAddedCallbacks.get(pendingTrackSid) ?? new Set<() => void>();
|
|
1634
|
+
pendingCallbacks.add(cleanup);
|
|
1635
|
+
this.pendingTrackAddedCallbacks.set(pendingTrackSid, pendingCallbacks);
|
|
1636
|
+
}
|
|
1609
1637
|
return;
|
|
1610
1638
|
}
|
|
1611
1639
|
if (this.state === ConnectionState.Disconnected) {
|
|
@@ -1618,11 +1646,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1618
1646
|
}
|
|
1619
1647
|
const parts = unpackStreamId(stream.id);
|
|
1620
1648
|
const participantSid = parts[0];
|
|
1621
|
-
|
|
1622
|
-
let trackId = mediaTrack.id;
|
|
1649
|
+
const streamId = parts[1];
|
|
1623
1650
|
// firefox will get streamId (pID|trackId) instead of (pID|streamId) as it doesn't support sync tracks by stream
|
|
1624
1651
|
// and generates its own track id instead of infer from sdp track id.
|
|
1625
|
-
|
|
1652
|
+
let trackId = extractTrackSid(mediaTrack, stream) ?? mediaTrack.id;
|
|
1626
1653
|
|
|
1627
1654
|
if (participantSid === this.localParticipant.sid) {
|
|
1628
1655
|
this.log.warn('tried to create RemoteParticipant for local participant');
|
|
@@ -1691,6 +1718,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1691
1718
|
}
|
|
1692
1719
|
}
|
|
1693
1720
|
|
|
1721
|
+
private cancelPendingTrackAdded(trackSid: Track.SID) {
|
|
1722
|
+
this.pendingTrackAddedCallbacks.get(trackSid)?.forEach((cleanup) => cleanup());
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1694
1725
|
private handleLocalTrackSubscribed(subscribedSid: string) {
|
|
1695
1726
|
const findPublication = () =>
|
|
1696
1727
|
this.localParticipant
|
|
@@ -1893,7 +1924,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1893
1924
|
|
|
1894
1925
|
// when it's disconnected, send updates
|
|
1895
1926
|
if (info.state === ParticipantInfo_State.DISCONNECTED) {
|
|
1896
|
-
this.handleParticipantDisconnected(
|
|
1927
|
+
this.handleParticipantDisconnected(
|
|
1928
|
+
info.identity,
|
|
1929
|
+
remoteParticipant,
|
|
1930
|
+
info.disconnectReason === DisconnectReason.UNKNOWN_REASON
|
|
1931
|
+
? undefined
|
|
1932
|
+
: info.disconnectReason,
|
|
1933
|
+
);
|
|
1897
1934
|
} else {
|
|
1898
1935
|
// create participant if doesn't exist
|
|
1899
1936
|
this.getOrCreateParticipant(info.identity, info);
|
|
@@ -1911,7 +1948,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1911
1948
|
this.incomingDataTrackManager.receiveSfuPublicationUpdates(mapped);
|
|
1912
1949
|
};
|
|
1913
1950
|
|
|
1914
|
-
private handleParticipantDisconnected(
|
|
1951
|
+
private handleParticipantDisconnected(
|
|
1952
|
+
identity: string,
|
|
1953
|
+
participant?: RemoteParticipant,
|
|
1954
|
+
disconnectReason?: DisconnectReason,
|
|
1955
|
+
) {
|
|
1915
1956
|
// remove and send event
|
|
1916
1957
|
this.remoteParticipants.delete(identity);
|
|
1917
1958
|
if (!participant) {
|
|
@@ -1924,7 +1965,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1924
1965
|
participant.trackPublications.forEach((publication) => {
|
|
1925
1966
|
participant.unpublishTrack(publication.trackSid, true);
|
|
1926
1967
|
});
|
|
1927
|
-
this.emit(RoomEvent.ParticipantDisconnected, participant);
|
|
1968
|
+
this.emit(RoomEvent.ParticipantDisconnected, participant, disconnectReason);
|
|
1928
1969
|
participant.setDisconnected();
|
|
1929
1970
|
this.rpcClientManager.handleParticipantDisconnected(participant.identity);
|
|
1930
1971
|
}
|
|
@@ -2008,8 +2049,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2008
2049
|
return;
|
|
2009
2050
|
}
|
|
2010
2051
|
const newStreamState = Track.streamStateFromProto(streamState.state);
|
|
2052
|
+
const prevStreamState = pub.track.streamState;
|
|
2011
2053
|
pub.track.setStreamState(newStreamState);
|
|
2012
|
-
if (newStreamState !==
|
|
2054
|
+
if (newStreamState !== prevStreamState) {
|
|
2013
2055
|
participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
|
|
2014
2056
|
this.emitWhenConnected(
|
|
2015
2057
|
RoomEvent.TrackStreamStateChanged,
|
|
@@ -2035,6 +2077,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2035
2077
|
};
|
|
2036
2078
|
|
|
2037
2079
|
private handleSubscriptionError = (update: SubscriptionResponse) => {
|
|
2080
|
+
this.cancelPendingTrackAdded(update.trackSid);
|
|
2081
|
+
|
|
2038
2082
|
const participant = Array.from(this.remoteParticipants.values()).find((p) =>
|
|
2039
2083
|
p.trackPublications.has(update.trackSid),
|
|
2040
2084
|
);
|
|
@@ -2409,6 +2453,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2409
2453
|
},
|
|
2410
2454
|
)
|
|
2411
2455
|
.on(ParticipantEvent.TrackUnpublished, (publication: RemoteTrackPublication) => {
|
|
2456
|
+
this.cancelPendingTrackAdded(publication.trackSid);
|
|
2412
2457
|
this.emit(RoomEvent.TrackUnpublished, publication, participant);
|
|
2413
2458
|
})
|
|
2414
2459
|
.on(
|
|
@@ -2558,6 +2603,46 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2558
2603
|
);
|
|
2559
2604
|
}
|
|
2560
2605
|
|
|
2606
|
+
private setStatsLogging(enabled: boolean) {
|
|
2607
|
+
if (enabled) {
|
|
2608
|
+
if (!this.statsLogInterval) {
|
|
2609
|
+
this.statsLogInterval = CriticalTimers.setInterval(() => {
|
|
2610
|
+
// logWebRTCStats handles its own errors, nothing to await here
|
|
2611
|
+
this.logWebRTCStats();
|
|
2612
|
+
}, STATS_LOG_FREQUENCY_MS);
|
|
2613
|
+
}
|
|
2614
|
+
} else if (this.statsLogInterval) {
|
|
2615
|
+
CriticalTimers.clearInterval(this.statsLogInterval);
|
|
2616
|
+
this.statsLogInterval = undefined;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
/**
|
|
2621
|
+
* Dumps stats of both peer connections.
|
|
2622
|
+
*/
|
|
2623
|
+
private logWebRTCStats = async () => {
|
|
2624
|
+
const pcManager = this.engine?.pcManager;
|
|
2625
|
+
if (!pcManager) {
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
try {
|
|
2629
|
+
const [publisher, subscriber] = await Promise.all([
|
|
2630
|
+
pcManager.publisher.getStats(),
|
|
2631
|
+
pcManager.subscriber?.getStats(),
|
|
2632
|
+
]);
|
|
2633
|
+
const publisherStats = publisher && summarizeStatsReport(publisher);
|
|
2634
|
+
const subscriberStats = subscriber && summarizeStatsReport(subscriber);
|
|
2635
|
+
this.statsLog.info(`webrtc stats`, {
|
|
2636
|
+
publisher: publisherStats?.connection,
|
|
2637
|
+
subscriber: subscriberStats?.connection,
|
|
2638
|
+
inbound: [...(publisherStats?.inbound ?? []), ...(subscriberStats?.inbound ?? [])],
|
|
2639
|
+
outbound: publisherStats?.outbound,
|
|
2640
|
+
});
|
|
2641
|
+
} catch (error) {
|
|
2642
|
+
this.statsLog.debug('could not collect webrtc stats', { error });
|
|
2643
|
+
}
|
|
2644
|
+
};
|
|
2645
|
+
|
|
2561
2646
|
private registerConnectionReconcile() {
|
|
2562
2647
|
this.clearConnectionReconcile();
|
|
2563
2648
|
let consecutiveFailures = 0;
|
|
@@ -2581,11 +2666,20 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2581
2666
|
: undefined,
|
|
2582
2667
|
});
|
|
2583
2668
|
if (consecutiveFailures >= 3) {
|
|
2584
|
-
this.
|
|
2585
|
-
this.
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2669
|
+
this.clearConnectionReconcile();
|
|
2670
|
+
if (this.engine && !this.engine.isClosed) {
|
|
2671
|
+
// The transport silently died while we still looked connected. Try a full reconnect
|
|
2672
|
+
// (keeps the room alive; the engine falls back to Disconnected if it ultimately fails).
|
|
2673
|
+
this.log.warn('detected connection state mismatch, attempting full reconnect');
|
|
2674
|
+
this.engine.reconnect();
|
|
2675
|
+
} else {
|
|
2676
|
+
// No usable engine to reconnect with; tear down.
|
|
2677
|
+
this.recreateEngine();
|
|
2678
|
+
this.handleDisconnect(
|
|
2679
|
+
this.options.stopLocalTrackOnUnpublish,
|
|
2680
|
+
DisconnectReason.STATE_MISMATCH,
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
2589
2683
|
}
|
|
2590
2684
|
} else {
|
|
2591
2685
|
consecutiveFailures = 0;
|
|
@@ -2607,7 +2701,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2607
2701
|
this.log.info(`connection state changed: ${this.state} -> ${state}`);
|
|
2608
2702
|
this.state = state;
|
|
2609
2703
|
this.incomingDataStreamManager.setConnected(state === ConnectionState.Connected);
|
|
2704
|
+
this.setStatsLogging(state === ConnectionState.Connected);
|
|
2705
|
+
|
|
2610
2706
|
this.emit(RoomEvent.ConnectionStateChanged, this.state);
|
|
2707
|
+
|
|
2611
2708
|
return true;
|
|
2612
2709
|
}
|
|
2613
2710
|
|
|
@@ -2913,7 +3010,10 @@ export type RoomEventCallbacks = {
|
|
|
2913
3010
|
moved: (name: string) => void;
|
|
2914
3011
|
mediaDevicesChanged: () => void;
|
|
2915
3012
|
participantConnected: (participant: RemoteParticipant) => void;
|
|
2916
|
-
participantDisconnected: (
|
|
3013
|
+
participantDisconnected: (
|
|
3014
|
+
participant: RemoteParticipant,
|
|
3015
|
+
disconnectReason?: DisconnectReason,
|
|
3016
|
+
) => void;
|
|
2917
3017
|
trackPublished: (publication: RemoteTrackPublication, participant: RemoteParticipant) => void;
|
|
2918
3018
|
trackSubscribed: (
|
|
2919
3019
|
track: RemoteTrack,
|
|
@@ -535,7 +535,7 @@ function bytesToChunks(streamId: string): TransformStream<Uint8Array, DataStream
|
|
|
535
535
|
* synthesized text chunk decodes independently. The `flush` emits the decoder's trailing bytes.
|
|
536
536
|
*/
|
|
537
537
|
function bytesToDecodedUtf8(streamId: string): TransformStream<Uint8Array, DataStream_Chunk> {
|
|
538
|
-
const decoder = new TextDecoder('utf-8'
|
|
538
|
+
const decoder = new TextDecoder('utf-8');
|
|
539
539
|
const encoder = new TextEncoder();
|
|
540
540
|
|
|
541
541
|
let outIndex = 0;
|
|
@@ -204,7 +204,7 @@ export class TextStreamReader extends BaseStreamReader<TextStreamInfo> {
|
|
|
204
204
|
// Suppress unhandled rejection on reader.closed — errors are
|
|
205
205
|
// already propagated through reader.read() to the consumer.
|
|
206
206
|
reader.closed.catch(() => {});
|
|
207
|
-
const decoder = new TextDecoder('utf-8'
|
|
207
|
+
const decoder = new TextDecoder('utf-8');
|
|
208
208
|
const signal = this.signal;
|
|
209
209
|
|
|
210
210
|
const cleanup = () => {
|
package/src/room/errors.ts
CHANGED
|
@@ -248,8 +248,7 @@ export class PublishTrackError extends LivekitError {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
export type RequestErrorReason =
|
|
251
|
-
|
|
252
|
-
| 'TimeoutError';
|
|
251
|
+
Exclude<RequestResponse_Reason, RequestResponse_Reason.OK> | 'TimeoutError';
|
|
253
252
|
|
|
254
253
|
export class SignalRequestError extends LivekitReasonedError<RequestErrorReason> {
|
|
255
254
|
readonly name = 'SignalRequestError';
|
package/src/room/events.ts
CHANGED
|
@@ -80,7 +80,7 @@ export enum RoomEvent {
|
|
|
80
80
|
* When a [[RemoteParticipant]] leaves *after* the local
|
|
81
81
|
* participant has joined.
|
|
82
82
|
*
|
|
83
|
-
* args: ([[RemoteParticipant]])
|
|
83
|
+
* args: ([[RemoteParticipant]], [[DisconnectReason]] | undefined)
|
|
84
84
|
*/
|
|
85
85
|
ParticipantDisconnected = 'participantDisconnected',
|
|
86
86
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** one summarised stats entry; keys without a value are dropped */
|
|
2
|
+
type Summary = Record<string, unknown>;
|
|
3
|
+
|
|
4
|
+
function compact<T extends Summary>(summary: T): Summary {
|
|
5
|
+
const compacted: Summary = {};
|
|
6
|
+
for (const [key, value] of Object.entries(summary)) {
|
|
7
|
+
if (value !== undefined) {
|
|
8
|
+
compacted[key] = value;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return compacted;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolution(width?: number, height?: number): string | undefined {
|
|
15
|
+
return width && height ? `${width}x${height}` : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** keeps the derived durations readable; reported values are logged as they are */
|
|
19
|
+
function round(seconds: number): number {
|
|
20
|
+
return Math.round(seconds * 10_000) / 10_000;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Average time media spent in the jitter buffer, in s. Both counters are
|
|
25
|
+
* cumulative, so this is the average over the lifetime of the stream.
|
|
26
|
+
*/
|
|
27
|
+
function jitterBuffer(stat: Summary): number | undefined {
|
|
28
|
+
const delay = stat.jitterBufferDelay as number | undefined;
|
|
29
|
+
const emitted = stat.jitterBufferEmittedCount as number | undefined;
|
|
30
|
+
return delay !== undefined && emitted ? round(delay / emitted) : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Playout delay in s. Video receive reports it directly, the audio path keeps it
|
|
35
|
+
* in the linked `media-playout` stats summed over the samples played out. Pairs
|
|
36
|
+
* with `RemoteTrack.setPlayoutDelay`.
|
|
37
|
+
*/
|
|
38
|
+
function playoutDelay(stat: Summary, playout?: Summary): number | undefined {
|
|
39
|
+
if (stat.playoutDelay !== undefined) {
|
|
40
|
+
return round(stat.playoutDelay as number);
|
|
41
|
+
}
|
|
42
|
+
const total = playout?.totalPlayoutDelay as number | undefined;
|
|
43
|
+
const samples = playout?.totalSamplesCount as number | undefined;
|
|
44
|
+
return total !== undefined && samples ? round(total / samples) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Picks the interesting fields out of a `getStats()` report and groups them by
|
|
49
|
+
* RTP stream, so a stats dump can be read without unfolding the raw report.
|
|
50
|
+
*/
|
|
51
|
+
export function summarizeStatsReport(report: RTCStatsReport) {
|
|
52
|
+
const byId = new Map<string, Summary>();
|
|
53
|
+
const candidatePairs: Summary[] = [];
|
|
54
|
+
const inbound: Summary[] = [];
|
|
55
|
+
const outbound: Summary[] = [];
|
|
56
|
+
let transport: Summary | undefined;
|
|
57
|
+
|
|
58
|
+
report.forEach((stat) => byId.set(stat.id, stat));
|
|
59
|
+
|
|
60
|
+
const codecOf = (stat: Summary) =>
|
|
61
|
+
stat.codecId ? byId.get(stat.codecId as string)?.mimeType : undefined;
|
|
62
|
+
const relatedOf = (stat: Summary, key: 'remoteId' | 'mediaSourceId' | 'playoutId') =>
|
|
63
|
+
stat[key] ? byId.get(stat[key] as string) : undefined;
|
|
64
|
+
|
|
65
|
+
report.forEach((stat) => {
|
|
66
|
+
switch (stat.type) {
|
|
67
|
+
case 'inbound-rtp': {
|
|
68
|
+
const playout = relatedOf(stat, 'playoutId');
|
|
69
|
+
inbound.push(
|
|
70
|
+
compact({
|
|
71
|
+
kind: stat.kind,
|
|
72
|
+
ssrc: stat.ssrc,
|
|
73
|
+
mid: stat.mid,
|
|
74
|
+
// matches `streamTrackID` in the track's own log context
|
|
75
|
+
trackId: stat.trackIdentifier,
|
|
76
|
+
codec: codecOf(stat),
|
|
77
|
+
decoder: stat.decoderImplementation,
|
|
78
|
+
resolution: resolution(stat.frameWidth, stat.frameHeight),
|
|
79
|
+
fps: stat.framesPerSecond,
|
|
80
|
+
bytesReceived: stat.bytesReceived,
|
|
81
|
+
packetsReceived: stat.packetsReceived,
|
|
82
|
+
packetsLost: stat.packetsLost,
|
|
83
|
+
packetsDiscarded: stat.packetsDiscarded,
|
|
84
|
+
// frames received without frames decoded is a decode failure
|
|
85
|
+
framesReceived: stat.framesReceived,
|
|
86
|
+
framesDecoded: stat.framesDecoded,
|
|
87
|
+
framesDropped: stat.framesDropped,
|
|
88
|
+
keyFramesDecoded: stat.keyFramesDecoded,
|
|
89
|
+
freezeCount: stat.freezeCount,
|
|
90
|
+
totalFreezesDuration: stat.totalFreezesDuration,
|
|
91
|
+
pauseCount: stat.pauseCount,
|
|
92
|
+
nackCount: stat.nackCount,
|
|
93
|
+
pliCount: stat.pliCount,
|
|
94
|
+
firCount: stat.firCount,
|
|
95
|
+
jitter: stat.jitter,
|
|
96
|
+
jitterBuffer: jitterBuffer(stat),
|
|
97
|
+
playoutDelay: playoutDelay(stat, playout),
|
|
98
|
+
audioLevel: stat.audioLevel,
|
|
99
|
+
totalSamplesReceived: stat.totalSamplesReceived,
|
|
100
|
+
concealedSamples: stat.concealedSamples,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case 'outbound-rtp': {
|
|
106
|
+
const remote = relatedOf(stat, 'remoteId');
|
|
107
|
+
const source = relatedOf(stat, 'mediaSourceId');
|
|
108
|
+
outbound.push(
|
|
109
|
+
compact({
|
|
110
|
+
kind: stat.kind,
|
|
111
|
+
ssrc: stat.ssrc,
|
|
112
|
+
mid: stat.mid,
|
|
113
|
+
rid: stat.rid,
|
|
114
|
+
trackId: source?.trackIdentifier,
|
|
115
|
+
active: stat.active,
|
|
116
|
+
codec: codecOf(stat),
|
|
117
|
+
encoder: stat.encoderImplementation,
|
|
118
|
+
resolution: resolution(stat.frameWidth, stat.frameHeight),
|
|
119
|
+
fps: stat.framesPerSecond,
|
|
120
|
+
// what the source produces, to tell a stalled capture from a stalled encoder
|
|
121
|
+
captureResolution: resolution(
|
|
122
|
+
source?.width as number | undefined,
|
|
123
|
+
source?.height as number | undefined,
|
|
124
|
+
),
|
|
125
|
+
captureFps: source?.framesPerSecond,
|
|
126
|
+
audioLevel: source?.audioLevel,
|
|
127
|
+
targetBitrate: stat.targetBitrate,
|
|
128
|
+
bytesSent: stat.bytesSent,
|
|
129
|
+
packetsSent: stat.packetsSent,
|
|
130
|
+
retransmittedPacketsSent: stat.retransmittedPacketsSent,
|
|
131
|
+
framesEncoded: stat.framesEncoded,
|
|
132
|
+
keyFramesEncoded: stat.keyFramesEncoded,
|
|
133
|
+
limitedBy:
|
|
134
|
+
stat.qualityLimitationReason === 'none' ? undefined : stat.qualityLimitationReason,
|
|
135
|
+
nackCount: stat.nackCount,
|
|
136
|
+
pliCount: stat.pliCount,
|
|
137
|
+
firCount: stat.firCount,
|
|
138
|
+
// loss, jitter and RTT are only known from what the remote reports
|
|
139
|
+
remotePacketsLost: remote?.packetsLost,
|
|
140
|
+
remoteFractionLost: remote?.fractionLost,
|
|
141
|
+
remoteJitter: remote?.jitter,
|
|
142
|
+
remoteRoundTripTime: remote?.roundTripTime,
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'transport':
|
|
148
|
+
transport = stat;
|
|
149
|
+
break;
|
|
150
|
+
case 'candidate-pair':
|
|
151
|
+
candidatePairs.push(stat);
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const selectedPairId = transport?.selectedCandidatePairId as string | undefined;
|
|
158
|
+
const pair =
|
|
159
|
+
(selectedPairId ? byId.get(selectedPairId) : undefined) ??
|
|
160
|
+
candidatePairs.find((candidate) => candidate.selected) ??
|
|
161
|
+
candidatePairs.find((candidate) => candidate.nominated);
|
|
162
|
+
const local = pair?.localCandidateId ? byId.get(pair.localCandidateId as string) : undefined;
|
|
163
|
+
const remote = pair?.remoteCandidateId ? byId.get(pair.remoteCandidateId as string) : undefined;
|
|
164
|
+
|
|
165
|
+
const connection = compact({
|
|
166
|
+
ice: transport?.iceState,
|
|
167
|
+
dtls: transport?.dtlsState,
|
|
168
|
+
route:
|
|
169
|
+
local && remote
|
|
170
|
+
? `${local.candidateType}/${local.protocol} -> ${remote.candidateType}`
|
|
171
|
+
: undefined,
|
|
172
|
+
network: local?.networkType,
|
|
173
|
+
currentRoundTripTime: pair?.currentRoundTripTime,
|
|
174
|
+
// the send bandwidth estimate; no RTP stream reports it
|
|
175
|
+
availableOutgoingBitrate: pair?.availableOutgoingBitrate,
|
|
176
|
+
availableIncomingBitrate: pair?.availableIncomingBitrate,
|
|
177
|
+
bytesSent: pair?.bytesSent,
|
|
178
|
+
bytesReceived: pair?.bytesReceived,
|
|
179
|
+
candidatePairChanges: transport?.selectedCandidatePairChanges,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
connection: Object.keys(connection).length > 0 ? connection : undefined,
|
|
184
|
+
outbound: outbound.length > 0 ? outbound : undefined,
|
|
185
|
+
inbound: inbound.length > 0 ? inbound : undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -213,29 +213,34 @@ class TokenSourceEndpoint extends TokenSourceCached {
|
|
|
213
213
|
const body = await response.json();
|
|
214
214
|
return TokenSourceResponse.fromJson(body, {
|
|
215
215
|
// NOTE: it could be possible that the response body could contain more fields than just
|
|
216
|
-
// what's in TokenSourceResponse depending on the implementation (ie,
|
|
216
|
+
// what's in TokenSourceResponse depending on the implementation (ie, DevelopmentTokenServer)
|
|
217
217
|
ignoreUnknownFields: true,
|
|
218
218
|
});
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
export type
|
|
222
|
+
export type DevelopmentTokenServerOptions = {
|
|
223
223
|
baseUrl?: string;
|
|
224
224
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
constructor(sandboxId: string, options: SandboxTokenServerOptions) {
|
|
225
|
+
class TokenSourceDevelopmentTokenServer extends TokenSourceEndpoint {
|
|
226
|
+
constructor(tokenServerId: string, options: DevelopmentTokenServerOptions) {
|
|
228
227
|
const { baseUrl = 'https://cloud-api.livekit.io', ...rest } = options;
|
|
229
228
|
|
|
230
229
|
super(`${baseUrl}/api/v2/sandbox/connection-details`, {
|
|
231
230
|
...rest,
|
|
232
231
|
headers: {
|
|
233
|
-
'X-Sandbox-ID':
|
|
232
|
+
'X-Sandbox-ID': tokenServerId,
|
|
234
233
|
},
|
|
235
234
|
});
|
|
236
235
|
}
|
|
237
236
|
}
|
|
238
237
|
|
|
238
|
+
/** @deprecated use {@link DevelopmentTokenServerOptions} instead */
|
|
239
|
+
export type SandboxTokenServerOptions = DevelopmentTokenServerOptions;
|
|
240
|
+
|
|
241
|
+
/** @deprecated Use {@link TokenSourceDevelopmentTokenServer} instead */
|
|
242
|
+
class TokenSourceSandboxTokenServer extends TokenSourceDevelopmentTokenServer {}
|
|
243
|
+
|
|
239
244
|
export {
|
|
240
245
|
/** The return type of {@link TokenSource.literal} */
|
|
241
246
|
type TokenSourceLiteral,
|
|
@@ -243,8 +248,9 @@ export {
|
|
|
243
248
|
type TokenSourceCustom,
|
|
244
249
|
/** The return type of {@link TokenSource.endpoint} */
|
|
245
250
|
type TokenSourceEndpoint,
|
|
246
|
-
/** The return type of {@link TokenSource.sandboxTokenServer} */
|
|
247
251
|
type TokenSourceSandboxTokenServer,
|
|
252
|
+
/** The return type of {@link TokenSource.developmentTokenServer} */
|
|
253
|
+
type TokenSourceDevelopmentTokenServer,
|
|
248
254
|
decodeTokenPayload,
|
|
249
255
|
areTokenSourceFetchOptionsEqual,
|
|
250
256
|
};
|
|
@@ -269,22 +275,29 @@ export const TokenSource = {
|
|
|
269
275
|
/**
|
|
270
276
|
* TokenSource.endpoint creates a token source that fetches credentials from a given URL using
|
|
271
277
|
* the standard endpoint format:
|
|
272
|
-
* @see https://
|
|
278
|
+
* @see https://docs.livekit.io/frontends/build/authentication/endpoint/
|
|
273
279
|
*/
|
|
274
280
|
endpoint(url: string, options: EndpointOptions = {}) {
|
|
275
281
|
return new TokenSourceEndpoint(url, options);
|
|
276
282
|
},
|
|
277
283
|
|
|
278
284
|
/**
|
|
279
|
-
*
|
|
285
|
+
* @deprecated Use {@link TokenSource.developmentTokenServer} instead
|
|
286
|
+
*/
|
|
287
|
+
sandboxTokenServer(sandboxId: string, options: SandboxTokenServerOptions = {}) {
|
|
288
|
+
return new TokenSourceSandboxTokenServer(sandboxId, options);
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* TokenSource.developmentTokenServer queries a development token server for credentials,
|
|
280
293
|
* which supports quick prototyping / getting started types of use cases.
|
|
281
294
|
*
|
|
282
295
|
* This token provider is INSECURE and should NOT be used in production.
|
|
283
296
|
*
|
|
284
297
|
* For more info:
|
|
285
|
-
* @see https://
|
|
298
|
+
* @see https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/
|
|
286
299
|
*/
|
|
287
|
-
|
|
288
|
-
return new
|
|
300
|
+
developmentTokenServer(tokenServerId: string, options: DevelopmentTokenServerOptions = {}) {
|
|
301
|
+
return new TokenSourceDevelopmentTokenServer(tokenServerId, options);
|
|
289
302
|
},
|
|
290
303
|
};
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// Builds an unsigned (`alg: none`) JWT. These aren't signed at all, so they can only be used in
|
|
2
|
+
// tests which don't care about the signature.
|
|
3
|
+
function unsignedToken(payload: Record<string, unknown>) {
|
|
4
|
+
const encode = (value: Record<string, unknown>) =>
|
|
5
|
+
btoa(JSON.stringify(value)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
6
|
+
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.`;
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
// Test JWTs created for test purposes only.
|
|
2
10
|
// None of these actually auth against anything.
|
|
3
11
|
export const TOKENS = {
|
|
@@ -25,4 +33,16 @@ export const TOKENS = {
|
|
|
25
33
|
// A dummy roomConfig value is also set, with room_config.name = "test room name", room_config.extraField = "extra field value", and room_config.agents = [{"agentName": "test agent name","metadata":"test agent metadata","extraField":"extra field value"}]
|
|
26
34
|
EXTRA_FIELDS:
|
|
27
35
|
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjo5ODc2NTQzMjEwLCJuYmYiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MCwicm9vbUNvbmZpZyI6eyJuYW1lIjoidGVzdCByb29tIG5hbWUiLCJlbXB0eVRpbWVvdXQiOjAsImRlcGFydHVyZVRpbWVvdXQiOjAsIm1heFBhcnRpY2lwYW50cyI6MCwibWluUGxheW91dERlbGF5IjowLCJtYXhQbGF5b3V0RGVsYXkiOjAsInN5bmNTdHJlYW1zIjpmYWxzZSwiYWdlbnRzIjpbeyJhZ2VudE5hbWUiOiJ0ZXN0IGFnZW50IG5hbWUiLCJtZXRhZGF0YSI6InRlc3QgYWdlbnQgbWV0YWRhdGEiLCJleHRyYUZpZWxkIjoiZXh0cmEgZmllbGQgdmFsdWUifV0sIm1ldGFkYXRhIjoiIiwiZXh0cmFGaWVsZCI6ImV4dHJhIGZpZWxkIHZhbHVlIn19Cg.EDetpHG8cSubaApzgWJaQrpCiSy9KDBlfCfVdIydbQ-_CHiNnXOK_f_mCJbTf9A-duT1jmvPOkLrkkWFT60XPQ',
|
|
36
|
+
|
|
37
|
+
// Nbf date set at 1234567890 seconds (Fri Feb 13 2009 23:31:30 GMT+0000)
|
|
38
|
+
// No exp date set at all
|
|
39
|
+
NO_EXP: unsignedToken({ sub: '1234567890', nbf: 1234567890, iat: 1234567890 }),
|
|
40
|
+
|
|
41
|
+
// No nbf date set at all
|
|
42
|
+
// Exp date set at 1234567891 seconds (Fri Feb 13 2009 23:31:31 GMT+0000)
|
|
43
|
+
EXP_IN_PAST_NO_NBF: unsignedToken({ sub: '1234567890', exp: 1234567891, iat: 1234567890 }),
|
|
44
|
+
|
|
45
|
+
// No nbf date set at all
|
|
46
|
+
// Exp date set at 9876543210 seconds (Fri Dec 22 2282 20:13:30 GMT+0000)
|
|
47
|
+
VALID_NO_NBF: unsignedToken({ sub: '1234567890', exp: 9876543210, iat: 1234567890 }),
|
|
28
48
|
};
|