Repository: pion/rtp
Pull request #196 (2022-07-29 22:26:05)

Add AbsCaptureTimeExtension and PlayoutDelayExtension
Both of these are experimental RTP header extensions defined in
libwebrtc.


codecov[bot]: # [Codecov](https://codecov.io/gh/pion/rtp/pull/196?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) Report
> Merging [#196](https://codecov.io/gh/pion/rtp/pull/196?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) (6c3188e) into [master](https://codecov.io/gh/pion/rtp/commit/9a6e1d08c3e7b8ec582c099f0071629f3bfe7356?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) (9a6e1d0) will **increase** coverage by `0.09%`.
> The diff coverage is `90.16%`.

```diff
@@            Coverage Diff             @@
##           master     #196      +/-   ##
==========================================
+ Coverage   87.36%   87.45%   +0.09%     
==========================================
  Files          18       20       +2     
  Lines        1773     1834      +61     
==========================================
+ Hits         1549     1604      +55     
- Misses        195      199       +4     
- Partials       29       31       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| go | `87.45% <90.16%> (+0.09%)` | :arrow_up: |
| wasm | `86.47% <90.16%> (+0.12%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/pion/rtp/pull/196?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | Coverage Δ | |
|---|---|---|
| [abscapturetimeextension.go](https://codecov.io/gh/pion/rtp/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-YWJzY2FwdHVyZXRpbWVleHRlbnNpb24uZ28=) | `86.66% <86.66%> (ø)` | |
| [playoutdelayextension.go](https://codecov.io/gh/pion/rtp/pull/196/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGxheW91dGRlbGF5ZXh0ZW5zaW9uLmdv) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pion/rtp/pull/196?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pion/rtp/pull/196?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion). Last update [9a6e1d0...6c3188e](https://codecov.io/gh/pion/rtp/pull/196?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).


Patch:
diff --git a/abscapturetimeextension.go b/abscapturetimeextension.go
new file mode 100644
index 0000000..f3005e9
--- /dev/null
+++ b/abscapturetimeextension.go
@@ -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,
+	}
+}
diff --git a/playoutdelayextension.go b/playoutdelayextension.go
new file mode 100644
index 0000000..e508503
--- /dev/null
+++ b/playoutdelayextension.go
@@ -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
+}


Test patch:
diff --git a/abscapturetimeextension_test.go b/abscapturetimeextension_test.go
new file mode 100644
index 0000000..d5b9062
--- /dev/null
+++ b/abscapturetimeextension_test.go
@@ -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())
+	}
+}
diff --git a/playoutdelayextension_test.go b/playoutdelayextension_test.go
new file mode 100644
index 0000000..6757b14
--- /dev/null
+++ b/playoutdelayextension_test.go
@@ -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")
+	}
+}