Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions datatrack/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2026 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package datatrack

import (
"math/rand/v2"
"time"
)

const clockRate = 90_000

// timestamp is a packet-level timestamp in ticks of clockRate.
type timestamp uint32

func randomTimestamp() timestamp {
return timestamp(rand.Uint32())
}

func (t timestamp) isBefore(other timestamp) bool {
if t-other == 1<<31 {
return t < other
}
return int32(t-other) < 0
Comment thread
ladvoc marked this conversation as resolved.
}

func (t timestamp) wrappingAdd(ticks uint32) timestamp {
return timestamp(uint32(t) + ticks)
}

// clock maps instants to monotonic packet timestamps.
type clock struct {
epoch time.Time
base timestamp
prev timestamp
}

func newClock(base timestamp) *clock {
return newClockWithEpoch(time.Now(), base)
}

func newClockWithEpoch(epoch time.Time, base timestamp) *clock {
return &clock{epoch: epoch, base: base, prev: base}
}

func (c *clock) now() timestamp {
return c.at(time.Now())
}

func (c *clock) at(instant time.Time) timestamp {
ts := c.base.wrappingAdd(durationToTicks(instant.Sub(c.epoch)))
if ts.isBefore(c.prev) {
ts = c.prev
}
c.prev = ts
Comment thread
ladvoc marked this conversation as resolved.
return ts
}

func durationToTicks(d time.Duration) uint32 {
nanos := d.Nanoseconds()
if nanos < 0 {
nanos = 0
}
seconds, remainder := uint64(nanos)/1_000_000_000, uint64(nanos)%1_000_000_000
return uint32(seconds*clockRate + (remainder*clockRate+500_000_000)/1_000_000_000)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can potentially make this simpler.

d.Seconds()

returns float64. So, you can just multiply that by clockRate and cast back to uint32 I think. Something like

return uint32(float64(clockRate) * d.Seconds())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May need bounds check though of < 0 or > something which will actually overflow uint32. I don't think there is great handling for greater than uint32 other than just let it wrap. So, maybe something like

return uint32(max(0, float64(clockRate) * d.Seconds()))

@ladvoc ladvoc Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went back and forth on this with an agent, and it pointed me to this:

In all non-constant conversions involving floating-point or complex values, if the result type cannot represent the value the conversion succeeds but the result value is implementation-dependent.

Which I think means the cast to uint32 may wrap on some arches and saturate on others if we use float64. For reference, this is the same calculation in the Rust implementation; the main difference there is Duration is guaranteed to not be negative.

}
212 changes: 212 additions & 0 deletions datatrack/depacketizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// Copyright 2026 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package datatrack

import (
"fmt"
"slices"

dtp "github.com/livekit/protocol/datatrack"
)

const maxBufferPackets = 128

// depacketizer reassembles packets into frames.
type depacketizer struct {
partials map[uint16]*partialFrame
// frame numbers of partials in insertion order, oldest first
order []uint16
}

type partialFrame struct {
startSequence uint16
extensions Extensions
payloads map[uint16][]byte
}

type depacketizerFrame struct {
payload []byte
extensions Extensions
}

type depacketizerPushOptions struct {
// maxPartialFrames is the number of frames assembled concurrently; the oldest partial is
// evicted when a new frame arrives at capacity.
maxPartialFrames int
}

// depacketizerPushResult may carry both a completed frame and a dropped one, since a single
// packet can complete one frame while evicting another.
type depacketizerPushResult struct {
frame *depacketizerFrame
drop *depacketizerDropError
}

type depacketizerDropReason int

const (
depacketizerDropReasonInterrupted depacketizerDropReason = iota
depacketizerDropReasonUnknownFrame
depacketizerDropReasonBufferFull
depacketizerDropReasonIncomplete
)

// depacketizerDropError records a dropped frame.
type depacketizerDropError struct {
frameNumber uint16
reason depacketizerDropReason
// newFrameNumber is the frame that interrupted the dropped one
newFrameNumber uint16
// received and expected packet counts of an incomplete frame
received, expected uint16
}

func (e *depacketizerDropError) Error() string {
var reason string
switch e.reason {
case depacketizerDropReasonInterrupted:
reason = fmt.Sprintf("interrupted by new frame %d", e.newFrameNumber)
case depacketizerDropReasonUnknownFrame:
reason = "unknown frame"
case depacketizerDropReasonBufferFull:
reason = "buffer full"
case depacketizerDropReasonIncomplete:
reason = fmt.Sprintf("incomplete (%d/%d)", e.received, e.expected)
}
return fmt.Sprintf("frame %d dropped: %s", e.frameNumber, reason)
}

func newDepacketizer() *depacketizer {
return &depacketizer{partials: make(map[uint16]*partialFrame)}
}

func (d *depacketizer) push(packet dtp.Packet, options depacketizerPushOptions) depacketizerPushResult {
switch markerOf(&packet.Header) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this method be pushed to protocol? Yeah, server does not need that, but it may at some point if it has to do sub-sampling. Also feels cleaner to have all packet utilities there.

When server needs to depacketize, this whole thing could move to protocol too, but that is later. Would be good to have all packet utilities in protocol for now.

@ladvoc ladvoc Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this is something that will likely be shared in the future. I'm assuming you're talking about the markerOf helper in particular?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, meant markerOf. Basically, anything to do with packet. Can be unit tested there.

case FrameMarkerSingle:
return d.frameFromSingle(packet, options)
case FrameMarkerStart:
return d.beginPartial(packet, options)
default:
return d.pushToPartial(packet)
}
}

func (d *depacketizer) frameFromSingle(packet dtp.Packet, options depacketizerPushOptions) depacketizerPushResult {
var result depacketizerPushResult
// A single packet is a self-contained frame and does not occupy a partials slot, but if
// the partials map is at capacity it is treated as a signal that the oldest in-flight
// partial is stale and evicts it.
if len(d.partials) >= options.maxPartialFrames {
result.drop = d.evictOldest(packet.FrameNumber)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The single frame has not been appended to the partials, do we still need to evict one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Single arriving while the partials map is at capacity is treated as a signal that the oldest in-flight partial is stale: the publisher has moved on to a later frame, and on an unordered channel with no retransmits its missing packets aren't coming. It doesn't change what gets dropped (the next Start would evict it anyway), only that the drop is reported promptly and attributed to the frame that actually superseded it. This matches the Rust implementation.

}
result.frame = &depacketizerFrame{payload: packet.Payload, extensions: extensionsOf(&packet.Header)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extensionsOf also, can it be moved to protocol?

return result
}

func (d *depacketizer) evictOldest(newFrameNumber uint16) *depacketizerDropError {
if len(d.order) == 0 {
return nil
}
oldest := d.order[0]
d.remove(oldest)
return &depacketizerDropError{frameNumber: oldest, reason: depacketizerDropReasonInterrupted, newFrameNumber: newFrameNumber}
}

func (d *depacketizer) remove(frameNumber uint16) {
delete(d.partials, frameNumber)
if i := slices.Index(d.order, frameNumber); i >= 0 {
d.order = slices.Delete(d.order, i, i+1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you want another level of protection here and do the map delete only if this condition is true?

}
}

func (d *depacketizer) beginPartial(packet dtp.Packet, options depacketizerPushOptions) depacketizerPushResult {
var result depacketizerPushResult
for len(d.partials) >= options.maxPartialFrames {
evicted := d.evictOldest(packet.FrameNumber)
if evicted == nil {
break
}
if result.drop == nil {
result.drop = evicted

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the drop used only for logging/debugging? Should it report all rather than just the first?

}
}

if _, replacing := d.partials[packet.FrameNumber]; !replacing {
d.order = append(d.order, packet.FrameNumber)
}
Comment thread
ladvoc marked this conversation as resolved.
d.partials[packet.FrameNumber] = &partialFrame{
startSequence: packet.SequenceNumber,
extensions: extensionsOf(&packet.Header),
payloads: map[uint16][]byte{packet.SequenceNumber: packet.Payload},
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is enforcing that the begin packet has to come first. Guess that is a known limitation per the Devin comment above and your reply, but wanted to flag.

return result
}

func (d *depacketizer) pushToPartial(packet dtp.Packet) depacketizerPushResult {
frameNumber := packet.FrameNumber
partial, ok := d.partials[frameNumber]
if !ok {
return depacketizerPushResult{drop: &depacketizerDropError{frameNumber: frameNumber, reason: depacketizerDropReasonUnknownFrame}}
}

if len(partial.payloads) >= maxBufferPackets {
d.remove(frameNumber)
return depacketizerPushResult{drop: &depacketizerDropError{frameNumber: frameNumber, reason: depacketizerDropReasonBufferFull}}
}

partial.payloads[packet.SequenceNumber] = packet.Payload

if markerOf(&packet.Header) == FrameMarkerFinal {
d.remove(frameNumber)
return finalize(frameNumber, partial, packet.SequenceNumber)
Comment thread
ladvoc marked this conversation as resolved.
}
return depacketizerPushResult{}
}

func finalize(frameNumber uint16, partial *partialFrame, endSequence uint16) depacketizerPushResult {
received := uint16(len(partial.payloads))
Comment thread
ladvoc marked this conversation as resolved.
expected := endSequence - partial.startSequence + 1
if received != expected {
return depacketizerPushResult{drop: &depacketizerDropError{
frameNumber: frameNumber,
reason: depacketizerDropReasonIncomplete,
received: received,
expected: expected,
}}
}

payloadLen := 0
for _, payload := range partial.payloads {
payloadLen += len(payload)
}
payload := make([]byte, 0, payloadLen)

for sequence := partial.startSequence; ; sequence++ {
partialPayload, ok := partial.payloads[sequence]
if !ok {
break
}
payload = append(payload, partialPayload...)
if sequence == endSequence {
return depacketizerPushResult{frame: &depacketizerFrame{payload: payload, extensions: partial.extensions}}
}
}
return depacketizerPushResult{drop: &depacketizerDropError{
frameNumber: frameNumber,
reason: depacketizerDropReasonIncomplete,
received: received,
expected: expected,
}}
}
Loading
Loading