Skip to content
Draft
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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
github.com/livekit/media-sdk v0.0.0-20251106223430-dd8f5e0de2cf
github.com/livekit/mediatransportutil v0.0.0-20251128105421-19c7a7b81c22
github.com/livekit/protocol v1.45.1
github.com/livekit/protocol v1.45.2
github.com/magefile/mage v1.15.0
github.com/moby/buildkit v0.26.2
github.com/moby/patternmatcher v0.6.0
Expand Down Expand Up @@ -60,7 +60,7 @@ require (
github.com/frostbyte73/core v0.1.1
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gammazero/deque v1.2.1
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ=
github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
Expand Down Expand Up @@ -159,8 +159,8 @@ github.com/livekit/media-sdk v0.0.0-20251106223430-dd8f5e0de2cf h1:gvun6axx2Mrh8
github.com/livekit/media-sdk v0.0.0-20251106223430-dd8f5e0de2cf/go.mod h1:7ssWiG+U4xnbvLih9WiZbhQP6zIKMjgXdUtIE1bm/E8=
github.com/livekit/mediatransportutil v0.0.0-20251128105421-19c7a7b81c22 h1:dzCBxOGLLWVtQhL7OYK2EGN+5Q+23Mq/jfz4vQisirA=
github.com/livekit/mediatransportutil v0.0.0-20251128105421-19c7a7b81c22/go.mod h1:mSNtYzSf6iY9xM3UX42VEI+STHvMgHmrYzEHPcdhB8A=
github.com/livekit/protocol v1.45.1 h1:4cbynsPZW32gS2z6nUWfAfr4YaTUwZSKUiLpSpjX+lQ=
github.com/livekit/protocol v1.45.1/go.mod h1:63AUi0vQak6Y6gPqSBHLc+ExYTUwEqF/m4b2IRW1iO0=
github.com/livekit/protocol v1.45.2 h1:JZMqVj3wTnYROfSPSaT0Tg3wmGot6aHXxmwXmUojfeM=
github.com/livekit/protocol v1.45.2/go.mod h1:e6QdWDkfot+M2nRh0eitJUS0ZLuwvKCsfiz2pWWSG3s=
github.com/livekit/psrpc v0.7.1 h1:ms37az0QTD3UXIWuUC5D/SkmKOlRMVRsI261eBWu/Vw=
github.com/livekit/psrpc v0.7.1/go.mod h1:bZ4iHFQptTkbPnB0LasvRNu/OBYXEu1NA6O5BMFo9kk=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
Expand Down
52 changes: 34 additions & 18 deletions localparticipant.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,18 @@ func (p *LocalParticipant) PublishTrack(track webrtc.TrackLocal, opts *TrackPubl
}

req := &livekit.AddTrackRequest{
Cid: track.ID(),
Name: opts.Name,
Source: opts.Source,
Type: kind.ProtoType(),
Width: uint32(opts.VideoWidth),
Height: uint32(opts.VideoHeight),
DisableDtx: opts.DisableDTX,
Stereo: opts.Stereo,
Stream: opts.Stream,
Encryption: opts.Encryption,
BackupCodecPolicy: opts.BackupCodecPolicy,
Cid: track.ID(),
Name: opts.Name,
Source: opts.Source,
Type: kind.ProtoType(),
Width: uint32(opts.VideoWidth),
Height: uint32(opts.VideoHeight),
DisableDtx: opts.DisableDTX,
Stereo: opts.Stereo,
Stream: opts.Stream,
Encryption: opts.Encryption,
BackupCodecPolicy: opts.BackupCodecPolicy,
PacketTrailerFeatures: packetTrailerFeaturesFromOpts(opts),
}
if kind == TrackKindVideo {
// single layer
Expand Down Expand Up @@ -287,13 +288,14 @@ func (p *LocalParticipant) PublishSimulcastTrack(tracks []*LocalTrack, opts *Tra
layers = append(layers, st.videoLayer)
}
req := &livekit.AddTrackRequest{
Cid: mainTrack.ID(),
Name: opts.Name,
Source: opts.Source,
Type: pub.Kind().ProtoType(),
Width: mainTrack.videoLayer.Width,
Height: mainTrack.videoLayer.Height,
Layers: layers,
Cid: mainTrack.ID(),
Name: opts.Name,
Source: opts.Source,
Type: pub.Kind().ProtoType(),
Width: mainTrack.videoLayer.Width,
Height: mainTrack.videoLayer.Height,
Layers: layers,
PacketTrailerFeatures: packetTrailerFeaturesFromOpts(opts),
SimulcastCodecs: []*livekit.SimulcastCodec{
{
Codec: mainTrack.Codec().MimeType,
Expand Down Expand Up @@ -1102,6 +1104,20 @@ func (p *LocalParticipant) SendFile(filePath string, options StreamBytesOptions)
return &writer.Info, nil
}

func packetTrailerFeaturesFromOpts(opts *TrackPublicationOptions) []livekit.PacketTrailerFeature {
if opts == nil || (!opts.AttachUserTimestamp && !opts.AttachFrameId) {
return nil
}
var features []livekit.PacketTrailerFeature
if opts.AttachUserTimestamp {
features = append(features, livekit.PacketTrailerFeature_PTF_USER_TIMESTAMP)
}
if opts.AttachFrameId {
features = append(features, livekit.PacketTrailerFeature_PTF_FRAME_ID)
}
return features
}

func (p *LocalParticipant) getPublishTransport() *PCTransport {
publisher, ok := p.engine.Publisher()
if ok {
Expand Down
177 changes: 177 additions & 0 deletions packet_trailer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package lksdk

import (
"bytes"
"encoding/binary"
)

// packetTrailerSEIUUID is the UUID embedded in H.264/H.265 SEI
// user_data_unregistered messages that carry an LKTS packet trailer.
var packetTrailerSEIUUID = [16]byte{
0x3f, 0xa8, 0x5f, 0x64, 0x57, 0x17, 0x45, 0x62,
0xb3, 0xfc, 0x2c, 0x96, 0x3f, 0x66, 0xaf, 0xa6,
}

const (
// packetTrailerMagic must remain consistent with kPacketTrailerMagic
// in rust-sdks/webrtc-sys/include/livekit/packet_trailer.h.
packetTrailerMagic = "LKTS"

// TLV tag IDs (XORed with 0xFF on the wire).
tagTimestampUs = 0x01 // value: 8 bytes big-endian uint64
tagFrameId = 0x02 // value: 4 bytes big-endian uint32

// TLV element sizes: tag(1) + len(1) + value.
timestampTlvSize = 10 // 1 + 1 + 8
frameIdTlvSize = 6 // 1 + 1 + 4

// Trailer envelope: [trailer_len: 1B XORed] [magic: 4B raw] = 5 bytes.
trailerEnvelopeSize = 5

packetTrailerMinSize = timestampTlvSize + trailerEnvelopeSize
packetTrailerMaxSize = timestampTlvSize + frameIdTlvSize + trailerEnvelopeSize
)

// FrameMetadata holds the metadata embedded in a packet trailer.
type FrameMetadata struct {
UserTimestampUs uint64
FrameId uint32
}

// appendPacketTrailer returns a new slice containing data followed by a
// TLV-encoded packet trailer. All TLV bytes are XORed with 0xFF to prevent
// H.264 NAL start-code sequences from appearing inside the trailer.
//
// Wire layout:
//
// [original data]
// [TLV: tag=0x01 ^ 0xFF, len=8 ^ 0xFF, 8-byte BE timestamp ^ 0xFF]
// [TLV: tag=0x02 ^ 0xFF, len=4 ^ 0xFF, 4-byte BE frame_id ^ 0xFF] (omitted when frameId == 0)
// [trailer_len ^ 0xFF]
// [magic "LKTS" raw]
func appendPacketTrailer(data []byte, meta FrameMetadata) []byte {
hasFrameId := meta.FrameId != 0
trailerLen := timestampTlvSize + trailerEnvelopeSize
if hasFrameId {
trailerLen += frameIdTlvSize
}

out := make([]byte, len(data)+trailerLen)
copy(out, data)
pos := len(data)

// TLV: timestamp_us
out[pos] = byte(tagTimestampUs) ^ 0xFF
out[pos+1] = 8 ^ 0xFF
binary.BigEndian.PutUint64(out[pos+2:], ^meta.UserTimestampUs)
pos += timestampTlvSize

// TLV: frame_id (only when non-zero)
if hasFrameId {
out[pos] = byte(tagFrameId) ^ 0xFF
out[pos+1] = 4 ^ 0xFF
binary.BigEndian.PutUint32(out[pos+2:], ^meta.FrameId)
pos += frameIdTlvSize
}

// Envelope: trailer_len (XORed) + magic (raw)
out[pos] = byte(trailerLen) ^ 0xFF
copy(out[pos+1:], packetTrailerMagic)

return out
}

// parsePacketTrailer attempts to parse a packet trailer from the end of the
// provided buffer. It returns the extracted FrameMetadata and true when a
// valid trailer is present.
func parsePacketTrailer(data []byte) (FrameMetadata, bool) {
if len(data) < trailerEnvelopeSize {
return FrameMetadata{}, false
}

magicStart := len(data) - len(packetTrailerMagic)
if string(data[magicStart:]) != packetTrailerMagic {
return FrameMetadata{}, false
}

trailerLen := int(data[magicStart-1] ^ 0xFF)
if trailerLen < trailerEnvelopeSize || trailerLen > len(data) {
return FrameMetadata{}, false
}

tlvStart := len(data) - trailerLen
tlvRegionLen := trailerLen - trailerEnvelopeSize

var meta FrameMetadata
foundAny := false
pos := 0

for pos+2 <= tlvRegionLen {
tag := data[tlvStart+pos] ^ 0xFF
length := int(data[tlvStart+pos+1] ^ 0xFF)
pos += 2

if pos+length > tlvRegionLen {
break
}

valStart := tlvStart + pos

switch {
case tag == tagTimestampUs && length == 8:
var ts uint64
for i := 0; i < 8; i++ {
ts = (ts << 8) | uint64(data[valStart+i]^0xFF)
}
meta.UserTimestampUs = ts
foundAny = true
case tag == tagFrameId && length == 4:
var fid uint32
for i := 0; i < 4; i++ {
fid = (fid << 8) | uint32(data[valStart+i]^0xFF)
}
meta.FrameId = fid
foundAny = true
}

pos += length
}

if !foundAny {
return FrameMetadata{}, false
}
return meta, true
}

// stripPacketTrailer returns the data with the packet trailer removed, if
// present. If no valid trailer is found, the original slice is returned.
func stripPacketTrailer(data []byte) []byte {
if len(data) < trailerEnvelopeSize {
return data
}

magicStart := len(data) - len(packetTrailerMagic)
if string(data[magicStart:]) != packetTrailerMagic {
return data
}

trailerLen := int(data[magicStart-1] ^ 0xFF)
if trailerLen < trailerEnvelopeSize || trailerLen > len(data) {
return data
}

return data[:len(data)-trailerLen]
}

// parseSEIUserData validates the UUID prefix of an SEI user_data_unregistered
// payload and parses the remaining bytes as an LKTS packet trailer.
// userData must start at the UUID (i.e. the first 16 bytes are the UUID).
func parseSEIUserData(userData []byte) (FrameMetadata, bool) {
if len(userData) < 16+packetTrailerMinSize {
return FrameMetadata{}, false
}
if !bytes.Equal(userData[:16], packetTrailerSEIUUID[:]) {
return FrameMetadata{}, false
}
return parsePacketTrailer(userData[16:])
}
55 changes: 55 additions & 0 deletions packet_trailer_h264_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package lksdk

// parseH264SEIPacketTrailer parses H264 SEI NAL units (type 6) carrying
// user_data_unregistered messages with an LKTS packet trailer and returns
// the embedded FrameMetadata when detected.
//
// Expected payload format (after the 1-byte NAL header):
//
// payloadType = 5 (user_data_unregistered)
// payloadSize = variable
// UUID = 16 bytes (3fa85f64-5717-4562-b3fc-2c963f66afa6)
// trailer = LKTS TLV-encoded packet trailer (XOR'd with 0xFF)
func parseH264SEIPacketTrailer(nalData []byte) (FrameMetadata, bool) {
if len(nalData) < 2 {
return FrameMetadata{}, false
}

// Skip 1-byte NAL header.
payload := nalData[1:]
i := 0

// Parse payloadType (can be extended with 0xFF bytes).
payloadType := 0
for i < len(payload) && payload[i] == 0xFF {
payloadType += 255
i++
}
if i >= len(payload) {
return FrameMetadata{}, false
}
payloadType += int(payload[i])
i++

if payloadType != 5 {
return FrameMetadata{}, false
}

// Parse payloadSize (can be extended with 0xFF bytes).
payloadSize := 0
for i < len(payload) && payload[i] == 0xFF {
payloadSize += 255
i++
}
if i >= len(payload) {
return FrameMetadata{}, false
}
payloadSize += int(payload[i])
i++

if len(payload) < i+payloadSize {
return FrameMetadata{}, false
}

return parseSEIUserData(payload[i : i+payloadSize])
}
Loading
Loading