-
Notifications
You must be signed in to change notification settings - Fork 247
e2e reliablility for data channel #1546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'livekit-client': patch | ||
| --- | ||
|
|
||
| Improve e2e reliablility of data channel |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { | |
| ClientConfiguration, | ||
| type ConnectionQualityUpdate, | ||
| DataChannelInfo, | ||
| DataChannelReceiveState, | ||
| DataPacket, | ||
| DataPacket_Kind, | ||
| DisconnectReason, | ||
|
|
@@ -44,6 +45,8 @@ import { | |
| } from '../api/SignalClient'; | ||
| import log, { LoggerNames, getLogger } from '../logger'; | ||
| import type { InternalRoomOptions } from '../options'; | ||
| import { DataPacketBuffer } from '../utils/dataPacketBuffer'; | ||
| import { TTLMap } from '../utils/ttlmap'; | ||
| import PCTransport, { PCEvents } from './PCTransport'; | ||
| import { PCTransportManager, PCTransportState } from './PCTransportManager'; | ||
| import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy'; | ||
|
|
@@ -81,6 +84,7 @@ const lossyDataChannel = '_lossy'; | |
| const reliableDataChannel = '_reliable'; | ||
| const minReconnectWait = 2 * 1000; | ||
| const leaveReconnect = 'leave-reconnect'; | ||
| const reliabeReceiveStateTTL = 30_000; | ||
|
|
||
| enum PCState { | ||
| New, | ||
|
|
@@ -178,6 +182,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit | |
|
|
||
| private publisherConnectionPromise: Promise<void> | undefined; | ||
|
|
||
| private reliableDataSequence: number = 1; | ||
|
|
||
| private reliableMessageBuffer = new DataPacketBuffer(); | ||
|
|
||
| private reliableReceivedState: TTLMap<string, number> = new TTLMap(reliabeReceiveStateTTL); | ||
|
|
||
| constructor(private options: InternalRoomOptions) { | ||
| super(); | ||
| this.log = getLogger(options.loggerName ?? LoggerNames.Engine); | ||
|
|
@@ -310,6 +320,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit | |
| this.lossyDCSub = undefined; | ||
| this.reliableDC = undefined; | ||
| this.reliableDCSub = undefined; | ||
| this.reliableMessageBuffer = new DataPacketBuffer(); | ||
| this.reliableDataSequence = 1; | ||
| this.reliableReceivedState.clear(); | ||
| } | ||
|
|
||
| async cleanupClient() { | ||
|
|
@@ -677,6 +690,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit | |
| } | ||
| const dp = DataPacket.fromBinary(new Uint8Array(buffer)); | ||
|
|
||
| if (dp.sequence > 0 && dp.participantSid !== '') { | ||
| const lastSeq = this.reliableReceivedState.get(dp.participantSid); | ||
| if (lastSeq && dp.sequence <= lastSeq) { | ||
| // ignore duplicate or out-of-order packets in reliable channel | ||
| return; | ||
| } | ||
| this.reliableReceivedState.set(dp.participantSid, dp.sequence); | ||
| } | ||
|
|
||
| if (dp.value?.case === 'speaker') { | ||
| // dispatch speaker updates | ||
| this.emit(EngineEvent.ActiveSpeakersUpdate, dp.value.value.speakers); | ||
|
|
@@ -1033,6 +1055,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit | |
| if (res) { | ||
| const rtcConfig = this.makeRTCConfiguration(res); | ||
| this.pcManager.updateConfiguration(rtcConfig); | ||
| if (this.latestJoinResponse) { | ||
| this.latestJoinResponse.serverInfo = res.serverInfo; | ||
| } | ||
| } else { | ||
| this.log.warn('Did not receive reconnect response', this.logContext); | ||
| } | ||
|
|
@@ -1059,6 +1084,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit | |
| this.createDataChannels(); | ||
| } | ||
|
|
||
| if (res?.lastMessageSeq) { | ||
| this.resendReliableMessagesForResume(res.lastMessageSeq); | ||
| } | ||
|
|
||
| // resume success | ||
| this.emit(EngineEvent.Resumed); | ||
| } | ||
|
|
@@ -1151,19 +1180,42 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit | |
|
|
||
| /* @internal */ | ||
| async sendDataPacket(packet: DataPacket, kind: DataPacket_Kind) { | ||
| const msg = packet.toBinary(); | ||
|
|
||
| // make sure we do have a data connection | ||
| await this.ensurePublisherConnected(kind); | ||
|
|
||
| if (kind === DataPacket_Kind.RELIABLE) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, thinking this.ensurePublisherConnection might race for parallel
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, that's right.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, the message order in the buffer should be same as the sequence number. I assume the message will be in order since there is no
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. got it, yeah, I think your assumption is correct! |
||
| packet.sequence = this.reliableDataSequence; | ||
| this.reliableDataSequence += 1; | ||
| } | ||
| const msg = packet.toBinary(); | ||
| const dc = this.dataChannelForKind(kind); | ||
| if (dc) { | ||
| if (kind === DataPacket_Kind.RELIABLE) { | ||
| this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence }); | ||
| } | ||
|
|
||
| if (this.attemptingReconnect) { | ||
| return; | ||
| } | ||
|
|
||
| dc.send(msg); | ||
| } | ||
|
|
||
| this.updateAndEmitDCBufferStatus(kind); | ||
| } | ||
|
|
||
| private async resendReliableMessagesForResume(lastMessageSeq: number) { | ||
| await this.ensurePublisherConnected(DataPacket_Kind.RELIABLE); | ||
| const dc = this.dataChannelForKind(DataPacket_Kind.RELIABLE); | ||
| if (dc) { | ||
| this.reliableMessageBuffer.popToSequence(lastMessageSeq); | ||
lukasIO marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.reliableMessageBuffer.getAll().forEach((msg) => { | ||
| dc.send(msg.data); | ||
| }); | ||
| } | ||
| this.updateAndEmitDCBufferStatus(DataPacket_Kind.RELIABLE); | ||
| } | ||
|
|
||
| private updateAndEmitDCBufferStatus = (kind: DataPacket_Kind) => { | ||
| const status = this.isBufferStatusLow(kind); | ||
| if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) { | ||
|
|
@@ -1175,6 +1227,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit | |
| private isBufferStatusLow = (kind: DataPacket_Kind): boolean | undefined => { | ||
| const dc = this.dataChannelForKind(kind); | ||
| if (dc) { | ||
| if (kind === DataPacket_Kind.RELIABLE) { | ||
| this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount); | ||
| } | ||
| return dc.bufferedAmount <= dc.bufferedAmountLowThreshold; | ||
| } | ||
| }; | ||
|
|
@@ -1409,6 +1464,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit | |
| publishTracks: getTrackPublicationInfo(localTracks), | ||
| dataChannels: this.dataChannelsInfo(), | ||
| trackSidsDisabled, | ||
| datachannelReceiveStates: this.reliableReceivedState.map((seq, sid) => { | ||
| return new DataChannelReceiveState({ | ||
| publisherSid: sid, | ||
| lastSeq: seq, | ||
| }); | ||
| }), | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| export interface DataPacketItem { | ||
| data: Uint8Array; | ||
| sequence: number; | ||
| } | ||
|
|
||
| export class DataPacketBuffer { | ||
| private buffer: DataPacketItem[] = []; | ||
|
|
||
| private _totalSize = 0; | ||
|
|
||
| push(item: DataPacketItem) { | ||
| this.buffer.push(item); | ||
| this._totalSize += item.data.byteLength; | ||
| } | ||
|
|
||
| pop(): DataPacketItem | undefined { | ||
| const item = this.buffer.shift(); | ||
| if (item) { | ||
| this._totalSize -= item.data.byteLength; | ||
| } | ||
| return item; | ||
| } | ||
|
|
||
| getAll(): DataPacketItem[] { | ||
| return this.buffer.slice(); | ||
| } | ||
|
|
||
| popToSequence(sequence: number) { | ||
| while (this.buffer.length > 0) { | ||
| const first = this.buffer[0]; | ||
| if (first.sequence <= sequence) { | ||
| this.pop(); | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| alignBufferedAmount(bufferedAmount: number) { | ||
| while (this.buffer.length > 0) { | ||
| const first = this.buffer[0]; | ||
| if (this._totalSize - first.data.byteLength <= bufferedAmount) { | ||
| break; | ||
| } | ||
| this.pop(); | ||
| } | ||
| } | ||
|
|
||
| get length(): number { | ||
| return this.buffer.length; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.