livekit-client 2.20.1 → 2.20.2
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 +33 -7
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +589 -163
- 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 +33 -7
- 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.map +1 -1
- package/dist/src/logger.d.ts +3 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +2 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +1 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +45 -4
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.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/room/participant/publishUtils.d.ts +7 -0
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +11 -1
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/test/signalToken.d.ts +26 -0
- package/dist/src/test/signalToken.d.ts.map +1 -0
- package/dist/src/utils/dataPacketBuffer.d.ts +18 -0
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
- package/dist/ts4.2/logger.d.ts +3 -1
- package/dist/ts4.2/room/PCTransport.d.ts +2 -0
- package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +45 -4
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/ts4.2/room/track/options.d.ts +11 -1
- package/dist/ts4.2/test/signalToken.d.ts +26 -0
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +18 -0
- package/package.json +7 -3
- package/src/api/SignalClient.e2e.test.ts +264 -0
- package/src/api/SignalClient.ts +34 -2
- package/src/logger.ts +2 -0
- package/src/room/PCTransport.ts +66 -31
- package/src/room/PCTransportManager.ts +10 -6
- package/src/room/RTCEngine.test.ts +275 -2
- package/src/room/RTCEngine.ts +276 -71
- package/src/room/RegionUrlProvider.ts +4 -2
- package/src/room/Room.ts +7 -1
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
- package/src/room/errors.ts +4 -4
- package/src/room/participant/LocalParticipant.ts +16 -6
- package/src/room/participant/Participant.ts +5 -0
- package/src/room/participant/publishUtils.ts +14 -11
- package/src/room/track/LocalAudioTrack.ts +16 -0
- package/src/room/track/Track.ts +10 -6
- package/src/room/track/options.ts +11 -1
- package/src/test/signalServerSetup.ts +151 -0
- package/src/test/signalToken.ts +59 -0
- package/src/test/vitest-context.d.ts +10 -0
- package/src/utils/dataPacketBuffer.test.ts +100 -0
- package/src/utils/dataPacketBuffer.ts +46 -1
package/src/room/RTCEngine.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
type UserPacket,
|
|
45
45
|
} from '@livekit/protocol';
|
|
46
46
|
import { EventEmitter } from 'events';
|
|
47
|
+
import type { Throws } from '@livekit/throws-transformer/throws';
|
|
47
48
|
import type { MediaAttributes } from 'sdp-transform';
|
|
48
49
|
import type TypedEventEmitter from 'typed-emitter';
|
|
49
50
|
import type { SignalOptions } from '../api/SignalClient';
|
|
@@ -91,7 +92,6 @@ import type { TrackPublishOptions, VideoCodec } from './track/options';
|
|
|
91
92
|
import { getTrackPublicationInfo } from './track/utils';
|
|
92
93
|
import type { LoggerOptions } from './types';
|
|
93
94
|
import {
|
|
94
|
-
Future,
|
|
95
95
|
isPublisherOfferWithJoinSupported,
|
|
96
96
|
isReactNative,
|
|
97
97
|
isVideoCodec,
|
|
@@ -109,8 +109,17 @@ const dataTrackDataChannel = '_data_track';
|
|
|
109
109
|
const minReconnectWait = 2 * 1000;
|
|
110
110
|
const leaveReconnect = 'leave-reconnect';
|
|
111
111
|
const reliabeReceiveStateTTL = 30_000;
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
|
|
113
|
+
// Two-watermark flow control for the reliable and data-track channels. Senders fill the buffer
|
|
114
|
+
// freely up to the high-water mark; once it's exceeded they block until the browser's
|
|
115
|
+
// `bufferedamountlow` event (which we arm at the low-water mark) signals the buffer has drained.
|
|
116
|
+
// The gap between the marks keeps the SCTP send buffer saturated while we refill, so throughput
|
|
117
|
+
// isn't starved, while the high-water mark bounds the buffer well below the level that would abort
|
|
118
|
+
// the channel (see livekit/client-sdk-js#1995).
|
|
119
|
+
const reliableDataChannelWaterMarkLow = 64 * 1024;
|
|
120
|
+
const reliableDataChannelWaterMarkHigh = 1024 * 1024;
|
|
121
|
+
const lossyDataChannelWaterMarkLow = 8 * 1024;
|
|
122
|
+
const lossyDataChannelWaterMarkHigh = 256 * 1024;
|
|
114
123
|
|
|
115
124
|
const initialMediaSectionsAudio = 3;
|
|
116
125
|
const initialMediaSectionsVideo = 3;
|
|
@@ -262,8 +271,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
262
271
|
/** used to indicate whether the browser is currently waiting to reconnect */
|
|
263
272
|
private isWaitingForNetworkReconnect: boolean = false;
|
|
264
273
|
|
|
265
|
-
private bufferStatusLowClosingFuture = new Future<never, UnexpectedConnectionState>();
|
|
266
|
-
|
|
267
274
|
constructor(private options: InternalRoomOptions) {
|
|
268
275
|
super();
|
|
269
276
|
this.log = getLogger(options.loggerName ?? LoggerNames.Engine, () => this.logContext);
|
|
@@ -297,13 +304,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
297
304
|
this.client.onParticipantUpdate = (updates) =>
|
|
298
305
|
this.emit(EngineEvent.ParticipantUpdate, updates);
|
|
299
306
|
this.client.onJoined = (joinResponse) => this.emit(EngineEvent.Joined, joinResponse);
|
|
300
|
-
|
|
301
|
-
this.on(EngineEvent.Closing, () => {
|
|
302
|
-
this.bufferStatusLowClosingFuture.reject?.(new UnexpectedConnectionState('engine closed'));
|
|
303
|
-
});
|
|
304
|
-
// Swallow the rejection at the source so it doesn't surface as an unhandled promise rejection
|
|
305
|
-
// when no waitForBufferStatusLow callers are attached.
|
|
306
|
-
this.bufferStatusLowClosingFuture.promise.catch(() => {});
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
/** @internal */
|
|
@@ -458,6 +458,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
async cleanupPeerConnections() {
|
|
461
|
+
// Reject parked headroom waiters up front: closing the peer connection is allowed (per spec)
|
|
462
|
+
// to transition data channels to 'closed' without firing events, which would otherwise strand
|
|
463
|
+
// a waiter holding the headroom lock.
|
|
464
|
+
this.invalidateDataChannelWaiters('peer connections cleaned up');
|
|
465
|
+
|
|
461
466
|
const dcCleanup = (dc: RTCDataChannel | undefined) => {
|
|
462
467
|
if (!dc) {
|
|
463
468
|
return;
|
|
@@ -885,6 +890,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
885
890
|
return;
|
|
886
891
|
}
|
|
887
892
|
|
|
893
|
+
// Waiters parked on the old channel objects would never see another event from them once they
|
|
894
|
+
// are replaced below — reject them so the headroom lock is released and queued senders
|
|
895
|
+
// re-check against the new channels.
|
|
896
|
+
this.invalidateDataChannelWaiters('data channels recreated');
|
|
897
|
+
|
|
888
898
|
// clear old data channel callbacks if recreate
|
|
889
899
|
if (this.lossyDC) {
|
|
890
900
|
this.lossyDC.onmessage = null;
|
|
@@ -930,10 +940,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
930
940
|
this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
|
|
931
941
|
this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
|
|
932
942
|
|
|
933
|
-
// set up dc buffer threshold
|
|
934
|
-
this.lossyDC.bufferedAmountLowThreshold =
|
|
935
|
-
this.reliableDC.bufferedAmountLowThreshold =
|
|
936
|
-
this.dataTrackDC.bufferedAmountLowThreshold =
|
|
943
|
+
// set up dc buffer threshold - if this is not set, it will default to 0
|
|
944
|
+
this.lossyDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
|
|
945
|
+
this.reliableDC.bufferedAmountLowThreshold = reliableDataChannelWaterMarkLow;
|
|
946
|
+
this.dataTrackDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
|
|
937
947
|
|
|
938
948
|
// handle buffer amount low events
|
|
939
949
|
this.lossyDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.LOSSY);
|
|
@@ -952,8 +962,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
952
962
|
// control buffered latency to ~100ms
|
|
953
963
|
const threshold = this.lossyDataStatByterate / 10;
|
|
954
964
|
dc.bufferedAmountLowThreshold = Math.min(
|
|
955
|
-
Math.max(threshold,
|
|
956
|
-
|
|
965
|
+
Math.max(threshold, lossyDataChannelWaterMarkLow),
|
|
966
|
+
lossyDataChannelWaterMarkHigh,
|
|
957
967
|
);
|
|
958
968
|
}
|
|
959
969
|
}, 1000);
|
|
@@ -1505,7 +1515,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1505
1515
|
}
|
|
1506
1516
|
|
|
1507
1517
|
if (res?.lastMessageSeq) {
|
|
1508
|
-
this.resendReliableMessagesForResume(res.lastMessageSeq)
|
|
1518
|
+
this.resendReliableMessagesForResume(res.lastMessageSeq).catch((error) => {
|
|
1519
|
+
this.log.warn('failed to resend reliable messages after resume', {
|
|
1520
|
+
...this.logContext,
|
|
1521
|
+
error,
|
|
1522
|
+
});
|
|
1523
|
+
});
|
|
1509
1524
|
}
|
|
1510
1525
|
|
|
1511
1526
|
// resume success
|
|
@@ -1621,21 +1636,43 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1621
1636
|
case DataChannelKind.DATA_TRACK_LOSSY:
|
|
1622
1637
|
return this.sendLossyBytes(msg, kind);
|
|
1623
1638
|
|
|
1624
|
-
case DataChannelKind.RELIABLE:
|
|
1639
|
+
case DataChannelKind.RELIABLE: {
|
|
1640
|
+
if (this.attemptingReconnect) {
|
|
1641
|
+
// A reconnect is already underway — queue for the resume replay instead of parking on a
|
|
1642
|
+
// channel that is being torn down. The send resolves; delivery is deferred to the replay.
|
|
1643
|
+
this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: false });
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1625
1647
|
const dc = this.dataChannelForKind(kind);
|
|
1626
1648
|
if (dc) {
|
|
1627
|
-
|
|
1628
|
-
|
|
1649
|
+
try {
|
|
1650
|
+
await this.waitForBufferHeadroomWithLock(kind);
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
if (this.isClosed) {
|
|
1653
|
+
// No replay is coming after an engine close — surface the failure.
|
|
1654
|
+
throw error;
|
|
1655
|
+
}
|
|
1656
|
+
// Transient teardown (the channel closed or was replaced while we waited): the
|
|
1657
|
+
// reliable channel promises delivery across resume, so queue the packet for the
|
|
1658
|
+
// replay instead of rejecting a send the app can't meaningfully retry.
|
|
1659
|
+
this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: false });
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1629
1662
|
|
|
1630
1663
|
if (this.attemptingReconnect) {
|
|
1664
|
+
// A reconnect began while we waited for headroom — same deal as above.
|
|
1665
|
+
this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: false });
|
|
1631
1666
|
return;
|
|
1632
1667
|
}
|
|
1633
1668
|
|
|
1669
|
+
this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: true });
|
|
1634
1670
|
dc.send(msg);
|
|
1635
1671
|
}
|
|
1636
1672
|
|
|
1637
1673
|
this.updateAndEmitDCBufferStatus(kind);
|
|
1638
1674
|
break;
|
|
1675
|
+
}
|
|
1639
1676
|
}
|
|
1640
1677
|
}
|
|
1641
1678
|
|
|
@@ -1643,42 +1680,62 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1643
1680
|
async sendLossyBytes(
|
|
1644
1681
|
bytes: NonSharedUint8Array,
|
|
1645
1682
|
kind: Exclude<DataChannelKind, DataChannelKind.RELIABLE>,
|
|
1646
|
-
|
|
1683
|
+
bufferStatusFullBehavior: 'drop' | 'wait' = 'drop',
|
|
1647
1684
|
) {
|
|
1648
|
-
//
|
|
1685
|
+
// Make sure we do have a data connection. This matters most for the direct data-track path
|
|
1686
|
+
// (Room's packetAvailable handler), which doesn't go through sendDataPacket: it both waits for
|
|
1687
|
+
// the channel to be open and — on lazily negotiated publisher connections — is what kicks the
|
|
1688
|
+
// negotiation off in the first place. The call is memoized, so the steady-state cost is one
|
|
1689
|
+
// await on an already-resolved promise.
|
|
1649
1690
|
await this.ensurePublisherConnected(kind);
|
|
1650
1691
|
|
|
1651
1692
|
const dc = this.dataChannelForKind(kind);
|
|
1652
|
-
|
|
1653
|
-
if (
|
|
1693
|
+
try {
|
|
1694
|
+
if (dc) {
|
|
1654
1695
|
// Depending on the exact circumstance that data is being sent, either drop or wait for the
|
|
1655
|
-
// buffer
|
|
1656
|
-
switch (
|
|
1696
|
+
// buffer to drain below the high-water mark before continuing.
|
|
1697
|
+
switch (bufferStatusFullBehavior) {
|
|
1657
1698
|
case 'wait':
|
|
1658
|
-
|
|
1699
|
+
if (!this.isBelowHighWaterMark(kind)) {
|
|
1700
|
+
await this.waitForBufferHeadroomWithLock(kind);
|
|
1701
|
+
}
|
|
1659
1702
|
break;
|
|
1660
1703
|
case 'drop':
|
|
1661
|
-
//
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
this.
|
|
1666
|
-
|
|
1667
|
-
|
|
1704
|
+
// we check against the actual threshold on the DC here, as it is dynamic for the lossy DC
|
|
1705
|
+
if (!this.isBelowLowWaterMark(kind)) {
|
|
1706
|
+
// Drop messages to reduce latency
|
|
1707
|
+
this.lossyDataDropCount += 1;
|
|
1708
|
+
if (this.lossyDataDropCount % 100 === 0) {
|
|
1709
|
+
this.log.warn(
|
|
1710
|
+
`dropping lossy data channel messages, total dropped: ${this.lossyDataDropCount}`,
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
return;
|
|
1668
1714
|
}
|
|
1669
|
-
return;
|
|
1670
1715
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1716
|
+
if (kind === DataChannelKind.LOSSY) {
|
|
1717
|
+
// The byterate stat tunes the LOSSY channel's dynamic drop threshold; counting data-track
|
|
1718
|
+
// bytes here would inflate it and let the lossy channel buffer far more latency than the
|
|
1719
|
+
// ~100ms the tuning targets.
|
|
1720
|
+
this.lossyDataStatCurrentBytes += bytes.byteLength;
|
|
1721
|
+
}
|
|
1673
1722
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1723
|
+
if (this.attemptingReconnect) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
dc.send(bytes);
|
|
1676
1728
|
}
|
|
1677
1729
|
|
|
1678
|
-
|
|
1730
|
+
this.updateAndEmitDCBufferStatus(kind);
|
|
1731
|
+
} catch (error: unknown) {
|
|
1732
|
+
// ensure same surface behaviour as before with missing data channel being silently ignored, just log an error message for clarity
|
|
1733
|
+
if (error instanceof TypeError) {
|
|
1734
|
+
this.log.error(error);
|
|
1735
|
+
} else {
|
|
1736
|
+
throw error;
|
|
1737
|
+
}
|
|
1679
1738
|
}
|
|
1680
|
-
|
|
1681
|
-
this.updateAndEmitDCBufferStatus(kind);
|
|
1682
1739
|
}
|
|
1683
1740
|
|
|
1684
1741
|
private async resendReliableMessagesForResume(lastMessageSeq: number) {
|
|
@@ -1686,9 +1743,36 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1686
1743
|
const dc = this.dataChannelForKind(DataChannelKind.RELIABLE);
|
|
1687
1744
|
if (dc) {
|
|
1688
1745
|
this.reliableMessageBuffer.popToSequence(lastMessageSeq);
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1746
|
+
// Hold the headroom lock across the whole replay. Releasing it between messages would let a
|
|
1747
|
+
// concurrent send — whose (newer) sequence was already assigned in sendDataPacket before it
|
|
1748
|
+
// queued on the lock — hit the wire mid-replay, and receivers would then discard the
|
|
1749
|
+
// remaining lower-sequence resent messages as duplicates.
|
|
1750
|
+
const unlock = await this.getBufferHeadroomLock(DataChannelKind.RELIABLE).lock();
|
|
1751
|
+
try {
|
|
1752
|
+
// Everything left after the ack cutoff must be re-handed to the current channel.
|
|
1753
|
+
this.reliableMessageBuffer.markAllUnsent();
|
|
1754
|
+
// Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
|
|
1755
|
+
// sent:false) during our own awaits appends after this pass started, so we pick it up on
|
|
1756
|
+
// the next one. We mark each packet only once we've actually handed it to the channel — a
|
|
1757
|
+
// blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
|
|
1758
|
+
// and a later alignBufferedAmount would then drop it for good. If the loop throws
|
|
1759
|
+
// mid-drain, unsent entries keep their flag and the next replay picks them up.
|
|
1760
|
+
for (
|
|
1761
|
+
let batch = this.reliableMessageBuffer.getUnsent();
|
|
1762
|
+
batch.length > 0;
|
|
1763
|
+
batch = this.reliableMessageBuffer.getUnsent()
|
|
1764
|
+
) {
|
|
1765
|
+
for (const item of batch) {
|
|
1766
|
+
// Respect flow control on resume too, so a large resend doesn't overflow the buffer.
|
|
1767
|
+
// We already hold the lock across the whole replay, so use the lock-free wait.
|
|
1768
|
+
await this.waitForBufferHeadroomWithoutLock(DataChannelKind.RELIABLE);
|
|
1769
|
+
dc.send(item.data);
|
|
1770
|
+
this.reliableMessageBuffer.markSent(item);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
} finally {
|
|
1774
|
+
unlock();
|
|
1775
|
+
}
|
|
1692
1776
|
}
|
|
1693
1777
|
this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
|
|
1694
1778
|
}
|
|
@@ -1700,39 +1784,160 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1700
1784
|
this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
|
|
1701
1785
|
}
|
|
1702
1786
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1787
|
+
try {
|
|
1788
|
+
const status = this.isBelowLowWaterMark(kind);
|
|
1789
|
+
if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
|
|
1790
|
+
this.dcBufferStatus.set(kind, status);
|
|
1791
|
+
this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
|
|
1792
|
+
}
|
|
1793
|
+
} catch (e) {
|
|
1794
|
+
this.log.warn('could not update buffer status', { error: e });
|
|
1708
1795
|
}
|
|
1709
1796
|
};
|
|
1710
1797
|
|
|
1711
|
-
|
|
1798
|
+
/**
|
|
1799
|
+
* Whether the send buffer has room to accept more data (the send gate). Senders proceed while
|
|
1800
|
+
* this is true and block once it goes false.
|
|
1801
|
+
*/
|
|
1802
|
+
private isBelowHighWaterMark = (kind: DataChannelKind): Throws<boolean, TypeError> => {
|
|
1712
1803
|
const dc = this.dataChannelForKind(kind);
|
|
1713
|
-
if (dc) {
|
|
1714
|
-
|
|
1804
|
+
if (!dc) {
|
|
1805
|
+
throw new TypeError(`Could not get data channel for kind ${kind}`);
|
|
1806
|
+
}
|
|
1807
|
+
// because RTCDatachannel has no high water mark built in we read the statically defined versions as constants
|
|
1808
|
+
const highMark =
|
|
1809
|
+
kind === DataChannelKind.RELIABLE
|
|
1810
|
+
? reliableDataChannelWaterMarkHigh
|
|
1811
|
+
: lossyDataChannelWaterMarkHigh;
|
|
1812
|
+
return dc.bufferedAmount <= highMark;
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
/**
|
|
1816
|
+
* Whether the send buffer has drained to its low-water mark. Drives the public
|
|
1817
|
+
* {@link EngineEvent.DCBufferStatusChanged} event.
|
|
1818
|
+
*/
|
|
1819
|
+
private isBelowLowWaterMark = (kind: DataChannelKind): Throws<boolean, TypeError> => {
|
|
1820
|
+
const dc = this.dataChannelForKind(kind);
|
|
1821
|
+
if (!dc) {
|
|
1822
|
+
throw new TypeError(`Could not get data channel for kind ${kind}`);
|
|
1715
1823
|
}
|
|
1824
|
+
// because RTCDatachannel has the threshold built in we can read it dynamically to account for changing thresholds over time
|
|
1825
|
+
return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
|
|
1716
1826
|
};
|
|
1717
1827
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1828
|
+
/** Per-kind lock serializing senders that have to wait for the buffer to drain. */
|
|
1829
|
+
private waitForBufferHeadroomLocks = new Map<DataChannelKind, Mutex>();
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* Per-kind AbortController that cancels parked headroom waiters whenever a kind's channel object
|
|
1833
|
+
* stops being current (replaced by createDataChannels or torn down by cleanupPeerConnections). A
|
|
1834
|
+
* waiter is parked on the channel object it captured at wait entry; if that object is abandoned
|
|
1835
|
+
* its events may never fire again, so the abort is what rejects the waiter and releases the
|
|
1836
|
+
* headroom lock instead of stranding every future sender behind it.
|
|
1837
|
+
*/
|
|
1838
|
+
private waiterAbortControllers = new Map<DataChannelKind, AbortController>();
|
|
1839
|
+
|
|
1840
|
+
private waiterAbortSignal(kind: DataChannelKind): AbortSignal {
|
|
1841
|
+
let controller = this.waiterAbortControllers.get(kind);
|
|
1842
|
+
if (!controller) {
|
|
1843
|
+
controller = new AbortController();
|
|
1844
|
+
this.waiterAbortControllers.set(kind, controller);
|
|
1845
|
+
}
|
|
1846
|
+
return controller.signal;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/** Rejects all parked headroom waiters (all kinds); the next waiter gets a fresh controller. */
|
|
1850
|
+
private invalidateDataChannelWaiters(reason: string) {
|
|
1851
|
+
for (const controller of this.waiterAbortControllers.values()) {
|
|
1852
|
+
controller.abort(reason);
|
|
1853
|
+
}
|
|
1854
|
+
this.waiterAbortControllers.clear();
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/** Acquires the per-kind headroom lock, resolving with the unlock function. */
|
|
1858
|
+
private getBufferHeadroomLock(kind: DataChannelKind): Mutex {
|
|
1859
|
+
let lock = this.waitForBufferHeadroomLocks.get(kind);
|
|
1860
|
+
if (!lock) {
|
|
1861
|
+
lock = new Mutex();
|
|
1862
|
+
this.waitForBufferHeadroomLocks.set(kind, lock);
|
|
1863
|
+
}
|
|
1864
|
+
return lock;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/**
|
|
1868
|
+
* Acquires the `kind` headroom lock, waits until the send buffer has room, then releases. The
|
|
1869
|
+
* common single-send entry point. Callers that need to hold the lock across several sends (the
|
|
1870
|
+
* resume replay) acquire it via {@link getBufferHeadroomLock} and call
|
|
1871
|
+
* {@link waitForBufferHeadroomWithoutLock} per message instead.
|
|
1872
|
+
*/
|
|
1873
|
+
async waitForBufferHeadroomWithLock(kind: DataChannelKind) {
|
|
1874
|
+
const unlock = await this.getBufferHeadroomLock(kind).lock();
|
|
1875
|
+
try {
|
|
1876
|
+
await this.waitForBufferHeadroomWithoutLock(kind);
|
|
1877
|
+
} finally {
|
|
1878
|
+
unlock();
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
/**
|
|
1883
|
+
* Waits until the send buffer for `kind` is at or below the high-water mark (draining, if
|
|
1884
|
+
* needed, via the `bufferedamountlow` event). Does no locking of its own — the caller must
|
|
1885
|
+
* already hold the kind's headroom lock (via {@link getBufferHeadroomLock}). The resume replay
|
|
1886
|
+
* holds the lock across its whole batch and calls this per message so no other sender can
|
|
1887
|
+
* interleave; the single-send path goes through {@link waitForBufferHeadroomWithLock}.
|
|
1888
|
+
*
|
|
1889
|
+
* Serializing through the per-kind lock means that, once the buffer drains, queued callers
|
|
1890
|
+
* refill it one at a time (up to the high-water mark) rather than all at once and overflowing
|
|
1891
|
+
* the SCTP send buffer (see livekit/client-sdk-js#1995).
|
|
1892
|
+
*/
|
|
1893
|
+
private async waitForBufferHeadroomWithoutLock(
|
|
1894
|
+
kind: DataChannelKind,
|
|
1895
|
+
): Promise<Throws<void, UnexpectedConnectionState | TypeError>> {
|
|
1896
|
+
if (this.isClosed) {
|
|
1897
|
+
throw new UnexpectedConnectionState('engine closed');
|
|
1898
|
+
}
|
|
1899
|
+
if (this.isBelowHighWaterMark(kind)) {
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
const dc = this.dataChannelForKind(kind);
|
|
1903
|
+
if (!dc) {
|
|
1904
|
+
throw new UnexpectedConnectionState(`DataChannel not found, kind: ${kind}`);
|
|
1905
|
+
}
|
|
1906
|
+
const abortSignal = this.waiterAbortSignal(kind);
|
|
1907
|
+
await new TypedPromise<void, UnexpectedConnectionState>((resolve, reject) => {
|
|
1908
|
+
const onBufferedAmountLow = () => {
|
|
1909
|
+
cleanup();
|
|
1724
1910
|
resolve();
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1911
|
+
};
|
|
1912
|
+
const onDCClose = () => {
|
|
1913
|
+
cleanup();
|
|
1914
|
+
reject(
|
|
1915
|
+
new UnexpectedConnectionState(`DataChannel ${kind} closed while draining the buffer`),
|
|
1916
|
+
);
|
|
1917
|
+
};
|
|
1918
|
+
const onAbort = () => {
|
|
1919
|
+
cleanup();
|
|
1920
|
+
reject(
|
|
1921
|
+
new UnexpectedConnectionState(
|
|
1922
|
+
`DataChannel ${kind} was replaced or torn down while waiting for headroom`,
|
|
1923
|
+
),
|
|
1924
|
+
);
|
|
1925
|
+
};
|
|
1926
|
+
const cleanup = () => {
|
|
1927
|
+
dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
|
|
1928
|
+
dc.removeEventListener('close', onDCClose);
|
|
1929
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
1930
|
+
};
|
|
1931
|
+
if (abortSignal.aborted) {
|
|
1932
|
+
onAbort();
|
|
1933
|
+
return;
|
|
1735
1934
|
}
|
|
1935
|
+
dc.addEventListener('bufferedamountlow', onBufferedAmountLow);
|
|
1936
|
+
// Proxy along any error caused by the data channel closing while we wait.
|
|
1937
|
+
dc.addEventListener('close', onDCClose);
|
|
1938
|
+
// The channel object we're parked on can be abandoned without ever firing another event
|
|
1939
|
+
// (e.g. createDataChannels replacing it); the abort is our way out.
|
|
1940
|
+
abortSignal.addEventListener('abort', onAbort);
|
|
1736
1941
|
});
|
|
1737
1942
|
}
|
|
1738
1943
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Mutex } from '@livekit/mutex';
|
|
2
2
|
import type { RegionInfo, RegionSettings } from '@livekit/protocol';
|
|
3
|
-
import
|
|
3
|
+
import { LoggerNames, getLogger } from '../logger';
|
|
4
4
|
import { ConnectionError, ConnectionErrorReason } from './errors';
|
|
5
5
|
import { extractMaxAgeFromRequestHeaders, isCloud } from './utils';
|
|
6
6
|
|
|
7
|
+
const log = getLogger(LoggerNames.Region);
|
|
8
|
+
|
|
7
9
|
export const DEFAULT_MAX_AGE_MS = 5_000;
|
|
8
10
|
export const STOP_REFETCH_DELAY_MS = 30_000;
|
|
9
11
|
|
|
@@ -229,7 +231,7 @@ export class RegionUrlProvider {
|
|
|
229
231
|
if (regionsLeft.length > 0) {
|
|
230
232
|
const nextRegion = regionsLeft[0];
|
|
231
233
|
this.attemptedRegions.push(nextRegion);
|
|
232
|
-
log.
|
|
234
|
+
log.info(`switching to region: ${nextRegion.region}`, { region: nextRegion.region });
|
|
233
235
|
return nextRegion.url;
|
|
234
236
|
} else {
|
|
235
237
|
return null;
|
package/src/room/Room.ts
CHANGED
|
@@ -1383,7 +1383,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1383
1383
|
await Promise.all([
|
|
1384
1384
|
this.acquireAudioContext(),
|
|
1385
1385
|
...elements.map((e) => {
|
|
1386
|
-
|
|
1386
|
+
// when webAudioMix is enabled, attached elements are deliberately kept muted by
|
|
1387
|
+
// RemoteAudioTrack.attach() and audio is routed through the web audio graph instead.
|
|
1388
|
+
// unmuting them here would cause double playback on platforms where element.volume
|
|
1389
|
+
// has no effect (e.g. iOS Safari)
|
|
1390
|
+
if (!this.options.webAudioMix) {
|
|
1391
|
+
e.muted = false;
|
|
1392
|
+
}
|
|
1387
1393
|
return e.play();
|
|
1388
1394
|
}),
|
|
1389
1395
|
]);
|