Skip to content
Open
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
36 changes: 35 additions & 1 deletion readersampleprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ func (p *ReaderSampleProvider) OnBind() error {
p.h264reader, err = h264reader.NewReaderWithOptions(p.reader, h264reader.WithIncludeSEI(true))
}
case webrtc.MimeTypeH265:
p.h265reader, err = h265reader.NewReader(p.reader)
p.h265reader, err = h265reader.NewReaderWithOptions(p.reader, h265reader.WithIncludeSEI(true))
case webrtc.MimeTypeVP8, webrtc.MimeTypeVP9:
var ivfHeader *ivfreader.IVFFileHeader
p.ivfReader, ivfHeader, err = ivfreader.NewWith(p.reader)
Expand Down Expand Up @@ -392,6 +392,27 @@ func (p *ReaderSampleProvider) NextSample(ctx context.Context) (media.Sample, er
return sample, err
}

if nal.NalUnitType == 39 { // prefix SEI
if p.appendUserTimestamp {
if ts, ok := parseH265SEIUserTimestamp(nal.Data); ok {
p.pendingUserTimestampUs = ts
p.hasPendingUserTimestamp = true
}
continue
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this continue needed or should it fall through and return empty data?

}
// If SEI, clear the data and do not return a frame (try next NAL)
sample.Data = nil
sample.Duration = 0
return sample, nil
}

if nal.NalUnitType == 40 { // suffix SEI
// Ignore suffix SEI entirely (do not parse or append).
sample.Data = nil
sample.Duration = 0
return sample, nil
}

// aggregate vps,sps,pps into a single AP packet (chrome requires this)
if nal.NalUnitType == 32 || nal.NalUnitType == 33 || nal.NalUnitType == 34 {
sample.Data = append(sample.Data, []byte{0, 0, 0, 1}...) // add NAL prefix
Expand All @@ -416,6 +437,19 @@ func (p *ReaderSampleProvider) NextSample(ctx context.Context) (media.Sample, er
return sample, nil
}

// Attach the LKTS trailer to the encoded frame payload when enabled.
// If we didn't see a preceding timestamp, we still append a trailer with
// a zero timestamp.
if p.appendUserTimestamp {
ts := int64(0)
if p.hasPendingUserTimestamp {
ts = p.pendingUserTimestampUs
p.hasPendingUserTimestamp = false
p.pendingUserTimestampUs = 0
}
sample.Data = appendUserTimestampTrailer(sample.Data, ts)
}

sample.Duration = defaultH265FrameDuration
break
}
Expand Down
94 changes: 94 additions & 0 deletions user_timestamp_h265_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package lksdk

import (
"bytes"
"encoding/binary"
"fmt"
)

// parseH265SEIUserTimestamp parses H265 prefix SEI NAL units (type 39) carrying
// user_data_unregistered messages and returns a timestamp (microseconds) when detected.
//
// Expected payload format (after the 2-byte NAL header):
//
// payloadType = 5 (user_data_unregistered)
// payloadSize = 24
// UUID = 16 bytes (3fa85f64-5717-4562-b3fc-2c963f66afa6)
// timestamp_us = 8 bytes, big-endian
// trailing = 0x80 (stop bits + padding)
func parseH265SEIUserTimestamp(nalData []byte) (int64, bool) {
if len(nalData) < 3 {
logger.Infow("H265 SEI user_data_unregistered: nal too short", "nal_len", len(nalData))
return 0, false
}

// Skip 2-byte NAL header.
payload := nalData[2:]
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) {
logger.Infow("H265 SEI user_data_unregistered: payloadType truncated", "payload_len", len(payload))
return 0, false
}
payloadType += int(payload[i])
i++

// We only care about user_data_unregistered (type 5).
if payloadType != 5 {
return 0, 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) {
logger.Infow("H265 SEI user_data_unregistered: payloadSize truncated", "payload_len", len(payload))
return 0, false
}
payloadSize += int(payload[i])
i++

if payloadSize < 24 || len(payload) < i+payloadSize {
// Not enough data for UUID (16) + timestamp (8).
logger.Infow(
"H265 SEI user_data_unregistered: insufficient data for UUID + timestamp",
"payloadSize", payloadSize,
"payload_len", len(payload),
"offset", i,
)
return 0, false
}

userData := payload[i : i+payloadSize]
uuidBytes := userData[:16]
tsBytes := userData[16:24]

// Validate the UUID matches the exact user timestamp UUID we expect.
if !bytes.Equal(uuidBytes, userTimestampSEIUUID[:]) {
return 0, false
}

timestampUS := binary.BigEndian.Uint64(tsBytes)

// Format UUID as 8-4-4-4-12 hex segments (for debug logs).
uuid := fmt.Sprintf("%x-%x-%x-%x-%x",
uuidBytes[0:4],
uuidBytes[4:6],
uuidBytes[6:8],
uuidBytes[8:10],
uuidBytes[10:16],
)

logger.Debugw("H265 SEI user_data_unregistered parsed", "uuid", uuid, "timestamp_us", timestampUS)

return int64(timestampUS), true
}
45 changes: 45 additions & 0 deletions user_timestamp_h265_parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package lksdk

import (
"encoding/binary"
"testing"
)

func TestParseH265SEIUserTimestamp_UUIDValidation(t *testing.T) {
const wantTS = int64(1234567890)

var tsBuf [8]byte
binary.BigEndian.PutUint64(tsBuf[:], uint64(wantTS))

buildNAL := func(uuid [16]byte) []byte {
// 2-byte NAL header for prefix SEI (nal_unit_type = 39).
nal := []byte{0x4e, 0x01}

// payloadType = 5 (user_data_unregistered)
// payloadSize = 24 (16-byte UUID + 8-byte timestamp)
nal = append(nal, 0x05, 0x18)
nal = append(nal, uuid[:]...)
nal = append(nal, tsBuf[:]...)
return nal
}

t.Run("accepts matching UUID", func(t *testing.T) {
gotTS, ok := parseH265SEIUserTimestamp(buildNAL(userTimestampSEIUUID))
if !ok {
t.Fatalf("expected ok=true")
}
if gotTS != wantTS {
t.Fatalf("timestamp mismatch: got %d want %d", gotTS, wantTS)
}
})

t.Run("rejects non-matching UUID", func(t *testing.T) {
badUUID := userTimestampSEIUUID
badUUID[0] ^= 0xff

_, ok := parseH265SEIUserTimestamp(buildNAL(badUUID))
if ok {
t.Fatalf("expected ok=false")
}
})
}
Loading