Skip to content

Commit

Permalink
Add experimental header extensions
Browse files Browse the repository at this point in the history
Add AbsCaptureTimeExtension and PlayoutDelayExtension implementations.
Both of these are experimental RTP header extensions defined in
libwebrtc.
  • Loading branch information
kevmo314 authored and Sean-Der committed Jun 14, 2023
1 parent 6ef1c2c commit a51bc4f
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
87 changes: 87 additions & 0 deletions abscapturetimeextension.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package rtp

import (
"encoding/binary"
"time"
)

const (
absCaptureTimeExtensionSize = 8
absCaptureTimeExtendedExtensionSize = 16
)

// AbsCaptureTimeExtension is a extension payload format in
// http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ID | len=7 | absolute capture timestamp (bit 0-23) |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | absolute capture timestamp (bit 24-55) |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ... (56-63) |
// +-+-+-+-+-+-+-+-+
type AbsCaptureTimeExtension struct {
Timestamp uint64
EstimatedCaptureClockOffset *int64
}

// Marshal serializes the members to buffer.
func (t AbsCaptureTimeExtension) Marshal() ([]byte, error) {
if t.EstimatedCaptureClockOffset != nil {
buf := make([]byte, 16)
binary.BigEndian.PutUint64(buf[0:8], t.Timestamp)
binary.BigEndian.PutUint64(buf[8:16], uint64(*t.EstimatedCaptureClockOffset))
return buf, nil
}
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf[0:8], t.Timestamp)
return buf, nil
}

// Unmarshal parses the passed byte slice and stores the result in the members.
func (t *AbsCaptureTimeExtension) Unmarshal(rawData []byte) error {
if len(rawData) < absCaptureTimeExtensionSize {
return errTooSmall
}
t.Timestamp = binary.BigEndian.Uint64(rawData[0:8])
if len(rawData) >= absCaptureTimeExtendedExtensionSize {
offset := int64(binary.BigEndian.Uint64(rawData[8:16]))
t.EstimatedCaptureClockOffset = &offset
}
return nil
}

// CaptureTime produces the estimated time.Time represented by this extension.
func (t AbsCaptureTimeExtension) CaptureTime() time.Time {
return toTime(t.Timestamp)
}

// EstimatedCaptureClockOffsetDuration produces the estimated time.Duration represented by this extension.
func (t AbsCaptureTimeExtension) EstimatedCaptureClockOffsetDuration() *time.Duration {
if t.EstimatedCaptureClockOffset == nil {
return nil
}
offset := *t.EstimatedCaptureClockOffset
duration := time.Duration(offset/(1<<32))*time.Second + time.Duration((offset&0xFFFFFFFF)*1e9/(1<<32))*time.Nanosecond
return &duration
}

// NewAbsCaptureTimeExtension makes new AbsCaptureTimeExtension from time.Time.
func NewAbsCaptureTimeExtension(captureTime time.Time) *AbsCaptureTimeExtension {
return &AbsCaptureTimeExtension{
Timestamp: toNtpTime(captureTime),
}
}

// NewAbsCaptureTimeExtensionWithCaptureClockOffset makes new AbsCaptureTimeExtension from time.Time and a clock offset.
func NewAbsCaptureTimeExtensionWithCaptureClockOffset(captureTime time.Time, captureClockOffset time.Duration) *AbsCaptureTimeExtension {
ns := captureClockOffset.Nanoseconds()
lsb := (ns / 1e9) & 0xFFFFFFFF
msb := (((ns % 1e9) * (1 << 32)) / 1e9) & 0xFFFFFFFF
offset := (lsb << 32) | msb
return &AbsCaptureTimeExtension{
Timestamp: toNtpTime(captureTime),
EstimatedCaptureClockOffset: &offset,
}
}
43 changes: 43 additions & 0 deletions abscapturetimeextension_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package rtp

import (
"testing"
"time"
)

func TestAbsCaptureTimeExtension_Roundtrip(t *testing.T) {
t0 := time.Now()
e1 := NewAbsCaptureTimeExtension(t0)
b1, err1 := e1.Marshal()
if err1 != nil {
t.Fatal(err1)
}
var o1 AbsCaptureTimeExtension
if err := o1.Unmarshal(b1); err != nil {
t.Fatal(err)
}
dt1 := o1.CaptureTime().Sub(t0).Seconds()
if dt1 < -0.001 || dt1 > 0.001 {
t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o1.CaptureTime(), dt1)
}
if o1.EstimatedCaptureClockOffsetDuration() != nil {
t.Fatalf("duration differs, want nil got %d", o1.EstimatedCaptureClockOffsetDuration())
}

e2 := NewAbsCaptureTimeExtensionWithCaptureClockOffset(t0, 1250*time.Millisecond)
b2, err2 := e2.Marshal()
if err2 != nil {
t.Fatal(err2)
}
var o2 AbsCaptureTimeExtension
if err := o2.Unmarshal(b2); err != nil {
t.Fatal(err)
}
dt2 := o1.CaptureTime().Sub(t0).Seconds()
if dt2 < -0.001 || dt2 > 0.001 {
t.Fatalf("timestamp differs, want %v got %v (dt=%f)", t0, o2.CaptureTime(), dt2)
}
if *o2.EstimatedCaptureClockOffsetDuration() != 1250*time.Millisecond {
t.Fatalf("duration differs, want 250ms got %d", *o2.EstimatedCaptureClockOffsetDuration())
}
}
47 changes: 47 additions & 0 deletions playoutdelayextension.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package rtp

import (
"encoding/binary"
"errors"
)

const (
playoutDelayExtensionSize = 3
playoutDelayMaxValue = (1 << 12) - 1
)

var errPlayoutDelayInvalidValue = errors.New("invalid playout delay value")

// PlayoutDelayExtension is a extension payload format in
// http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ID | len=2 | MIN delay | MAX delay |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
type PlayoutDelayExtension struct {
minDelay, maxDelay uint16
}

// Marshal serializes the members to buffer
func (p PlayoutDelayExtension) Marshal() ([]byte, error) {
if p.minDelay > playoutDelayMaxValue || p.maxDelay > playoutDelayMaxValue {
return nil, errPlayoutDelayInvalidValue
}

return []byte{
byte(p.minDelay >> 4),
byte(p.minDelay<<4) | byte(p.maxDelay>>8),
byte(p.maxDelay),
}, nil
}

// Unmarshal parses the passed byte slice and stores the result in the members
func (p *PlayoutDelayExtension) Unmarshal(rawData []byte) error {
if len(rawData) < playoutDelayExtensionSize {
return errTooSmall
}
p.minDelay = binary.BigEndian.Uint16(rawData[0:2]) >> 4
p.maxDelay = binary.BigEndian.Uint16(rawData[1:3]) & 0x0FFF
return nil
}
70 changes: 70 additions & 0 deletions playoutdelayextension_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package rtp

import (
"bytes"
"errors"
"testing"
)

func TestPlayoutDelayExtensionTooSmall(t *testing.T) {
t1 := PlayoutDelayExtension{}

var rawData []byte

if err := t1.Unmarshal(rawData); !errors.Is(err, errTooSmall) {
t.Fatal("err != errTooSmall")
}
}

func TestPlayoutDelayExtensionTooLarge(t *testing.T) {
t1 := PlayoutDelayExtension{minDelay: 1 << 12, maxDelay: 1 << 12}

if _, err := t1.Marshal(); !errors.Is(err, errPlayoutDelayInvalidValue) {
t.Fatal("err != errPlayoutDelayInvalidValue")
}
}

func TestPlayoutDelayExtension(t *testing.T) {
t1 := PlayoutDelayExtension{}

rawData := []byte{
0x01, 0x01, 0x00,
}

if err := t1.Unmarshal(rawData); err != nil {
t.Fatal("Unmarshal error on extension data")
}

t2 := PlayoutDelayExtension{
minDelay: 1 << 4, maxDelay: 1 << 8,
}

if t1 != t2 {
t.Error("Unmarshal failed")
}

dstData, _ := t2.Marshal()
if !bytes.Equal(dstData, rawData) {
t.Error("Marshal failed")
}
}

func TestPlayoutDelayExtensionExtraBytes(t *testing.T) {
t1 := PlayoutDelayExtension{}

rawData := []byte{
0x01, 0x01, 0x00, 0xff, 0xff,
}

if err := t1.Unmarshal(rawData); err != nil {
t.Fatal("Unmarshal error on extension data")
}

t2 := PlayoutDelayExtension{
minDelay: 1 << 4, maxDelay: 1 << 8,
}

if t1 != t2 {
t.Error("Unmarshal failed")
}
}

0 comments on commit a51bc4f

Please sign in to comment.