Repository: pion/rtp
Pull request #46 (2020-03-25 15:32:04)

Add AbsSendTimeExtension unmarshaller
Implement Marshal, Unmarshal and absolute `time.Time` estimation.


at-wat: @wdouglass @adwpc @Sean-Der please take a look!
codecov[bot]: # [Codecov](https://codecov.io/gh/pion/rtp/pull/46?src=pr&el=h1) Report
> Merging [#46](https://codecov.io/gh/pion/rtp/pull/46?src=pr&el=desc) into [master](https://codecov.io/gh/pion/rtp/commit/cfeee237106ea293979796c30a3a1e1fbafaa355&el=desc) will **increase** coverage by `0.14%`.
> The diff coverage is `80.00%`.

[![Impacted file tree graph](https://codecov.io/gh/pion/rtp/pull/46/graphs/tree.svg?width=650&height=150&src=pr&token=4KrbiBsspj)](https://codecov.io/gh/pion/rtp/pull/46?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #46      +/-   ##
==========================================
+ Coverage   83.90%   84.05%   +0.14%     
==========================================
  Files          11       12       +1     
  Lines         435      464      +29     
==========================================
+ Hits          365      390      +25     
- Misses         59       62       +3     
- Partials       11       12       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| #go | `84.05% <80.00%> (+0.14%)` | :arrow_up: |
| #wasm | `84.05% <80.00%> (+0.14%)` | :arrow_up: |

| [Impacted Files](https://codecov.io/gh/pion/rtp/pull/46?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packetizer.go](https://codecov.io/gh/pion/rtp/pull/46/diff?src=pr&el=tree#diff-cGFja2V0aXplci5nbw==) | `66.66% <0.00%> (-9.26%)` | :arrow_down: |
| [abssendtimeextension.go](https://codecov.io/gh/pion/rtp/pull/46/diff?src=pr&el=tree#diff-YWJzc2VuZHRpbWVleHRlbnNpb24uZ28=) | `94.73% <94.73%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pion/rtp/pull/46?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pion/rtp/pull/46?src=pr&el=footer). Last update [cfeee23...fa01edc](https://codecov.io/gh/pion/rtp/pull/46?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

at-wat: @wdouglass thank you for your prompt review!

Patch:
diff --git a/abssendtimeextension.go b/abssendtimeextension.go
new file mode 100644
index 0000000..5f560e8
--- /dev/null
+++ b/abssendtimeextension.go
@@ -0,0 +1,80 @@
+package rtp
+
+import (
+	"time"
+)
+
+const (
+	absSendTimeExtensionSize = 4
+)
+
+// AbsSendTimeExtension is a extension payload format in
+// http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
+//  0 1 2 3 4 5 6 7
+// +-+-+-+-+-+-+-+-+
+// |  ID   |  len  |
+// +-+-+-+-+-+-+-+-+
+// per RFC 5285
+// Len is the number of bytes in the extension - 1.
+type AbsSendTimeExtension struct {
+	ID        uint8
+	Timestamp uint64
+}
+
+// Marshal serializes the members to buffer.
+func (t *AbsSendTimeExtension) Marshal() ([]byte, error) {
+	return []byte{
+		(t.ID << 4) | 2,
+		byte(t.Timestamp & 0xFF0000 >> 16),
+		byte(t.Timestamp & 0xFF00 >> 8),
+		byte(t.Timestamp & 0xFF),
+	}, nil
+}
+
+// Unmarshal parses the passed byte slice and stores the result in the members.
+func (t *AbsSendTimeExtension) Unmarshal(rawData []byte) error {
+	if len(rawData) < absSendTimeExtensionSize {
+		return errTooSmall
+	}
+	t.ID = rawData[0] >> 4
+	t.Timestamp = uint64(rawData[1])<<16 | uint64(rawData[2])<<8 | uint64(rawData[3])
+	return nil
+}
+
+// Estimate absolute send time according to the receive time.
+// Note that if the transmission delay is larger than 64 seconds, estimated time will be wrong.
+func (t *AbsSendTimeExtension) Estimate(receive time.Time) time.Time {
+	receiveNTP := toNtpTime(receive)
+	ntp := receiveNTP&0xFFFFFFC000000000 | (t.Timestamp&0xFFFFFF)<<14
+	if receiveNTP < ntp {
+		// Receive time must be always later than send time
+		ntp -= 0x1000000 << 14
+	}
+
+	return toTime(ntp)
+}
+
+func toNtpTime(t time.Time) uint64 {
+	var s uint64
+	var f uint64
+	u := uint64(t.UnixNano())
+	s = u / 1e9
+	s += 0x83AA7E80 //offset in seconds between unix epoch and ntp epoch
+	f = u % 1e9
+	f <<= 32
+	f /= 1e9
+	s <<= 32
+
+	return s | f
+}
+
+func toTime(t uint64) time.Time {
+	s := t >> 32
+	f := t & 0xFFFFFFFF
+	f *= 1e9
+	f >>= 32
+	s -= 0x83AA7E80
+	u := s*1e9 + f
+
+	return time.Unix(0, int64(u))
+}
diff --git a/go.mod b/go.mod
index bbbfa74..1046d76 100644
--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,3 @@
 module github.com/pion/rtp
 
 go 1.12
-
-require github.com/stretchr/testify v1.5.1
diff --git a/go.sum b/go.sum
index 331fa69..e69de29 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +0,0 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/packetizer.go b/packetizer.go
index 4c6d7ed..37032b7 100644
--- a/packetizer.go
+++ b/packetizer.go
@@ -51,20 +51,6 @@ func (p *packetizer) EnableAbsSendTime(value int) {
 	p.extensionNumbers.AbsSendTime = value
 }
 
-func toNtpTime(t time.Time) uint64 {
-	var s uint64
-	var f uint64
-	u := uint64(t.UnixNano())
-	s = u / 1e9
-	s += 0x83AA7E80 //offset in seconds between unix epoch and ntp epoch
-	f = u % 1e9
-	f <<= 32
-	f /= 1e9
-	s <<= 32
-
-	return s | f
-}
-
 // Packetize packetizes the payload of an RTP packet and returns one or more RTP packets
 func (p *packetizer) Packetize(payload []byte, samples uint32) []*Packet {
 	// Guard against an empty payload
@@ -97,20 +83,14 @@ func (p *packetizer) Packetize(payload []byte, samples uint32) []*Packet {
 		//apply http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
 		packets[len(packets)-1].Header.Extension = true
 		packets[len(packets)-1].ExtensionProfile = 0xBEDE
-		packets[len(packets)-1].ExtensionPayload = []byte{
-			//the first byte is
-			// 0 1 2 3 4 5 6 7
-			//+-+-+-+-+-+-+-+-+
-			//|  ID   |  len  |
-			//+-+-+-+-+-+-+-+-+
-			//per RFC 5285
-			//Len is the number of bytes in the extension - 1
-
-			byte((p.extensionNumbers.AbsSendTime << 4) | 2),
-			byte(t & 0xFF0000 >> 16),
-			byte(t & 0xFF00 >> 8),
-			byte(t & 0xFF),
+		b, err := (&AbsSendTimeExtension{
+			ID:        uint8(p.extensionNumbers.AbsSendTime),
+			Timestamp: t,
+		}).Marshal()
+		if err != nil {
+			return nil // never happens
 		}
+		packets[len(packets)-1].ExtensionPayload = b
 	}
 
 	return packets


Test patch:
diff --git a/abssendtimeextension_test.go b/abssendtimeextension_test.go
new file mode 100644
index 0000000..5efb9b3
--- /dev/null
+++ b/abssendtimeextension_test.go
@@ -0,0 +1,98 @@
+package rtp
+
+import (
+	"testing"
+	"time"
+)
+
+const absSendTimeResolution = 3800 * time.Nanosecond
+
+func TestNtpConversion(t *testing.T) {
+	loc := time.FixedZone("UTC-5", -5*60*60)
+
+	tests := []struct {
+		t time.Time
+		n uint64
+	}{
+		{t: time.Date(1985, time.June, 23, 4, 0, 0, 0, loc), n: 0xa0c65b1000000000},
+		{t: time.Date(1999, time.December, 31, 23, 59, 59, 500000, loc), n: 0xbc18084f0020c49b},
+		{t: time.Date(2019, time.March, 27, 13, 39, 30, 8675309, loc), n: 0xe04641e202388b88},
+	}
+
+	for i, in := range tests {
+		out := toNtpTime(in.t)
+		if out != in.n {
+			t.Errorf("[%d] Converted NTP time from time.Time differs, expected: %d, got: %d",
+				i, in.n, out,
+			)
+		}
+	}
+	for i, in := range tests {
+		out := toTime(in.n)
+		diff := in.t.Sub(out)
+		if diff < -absSendTimeResolution || absSendTimeResolution < diff {
+			t.Errorf("[%d] Converted time.Time from NTP time differs, expected: %v, got: %v",
+				i, in.t.UTC(), out.UTC(),
+			)
+		}
+	}
+}
+
+func TestAbsSendTimeExtension_Roundtrip(t *testing.T) {
+	tests := []AbsSendTimeExtension{
+		{
+			ID:        1,
+			Timestamp: 123456,
+		},
+		{
+			ID:        2,
+			Timestamp: 654321,
+		},
+	}
+	for i, in := range tests {
+		b, err := in.Marshal()
+		if err != nil {
+			t.Fatal(err)
+		}
+		var out AbsSendTimeExtension
+		if err = out.Unmarshal(b); err != nil {
+			t.Fatal(err)
+		}
+		if in.ID != out.ID {
+			t.Errorf("[%d] ID differs, expected: %d, got: %d", i, in.ID, out.ID)
+		}
+		if in.Timestamp != out.Timestamp {
+			t.Errorf("[%d] Timestamp differs, expected: %d, got: %d", i, in.Timestamp, out.Timestamp)
+		}
+	}
+}
+
+func TestAbsSendTimeExtension_Estimate(t *testing.T) {
+	tests := []struct {
+		sendNTP    uint64
+		receiveNTP uint64
+	}{ //FFFFFFC000000000 mask of second
+		{0xa0c65b1000100000, 0xa0c65b1001000000}, // not carried
+		{0xa0c65b3f00000000, 0xa0c65b4001000000}, // carried during transmission
+	}
+	for i, in := range tests {
+		inTime := toTime(in.sendNTP)
+		send := &AbsSendTimeExtension{0, in.sendNTP >> 14}
+		b, err := send.Marshal()
+		if err != nil {
+			t.Fatal(err)
+		}
+		var received AbsSendTimeExtension
+		if err = received.Unmarshal(b); err != nil {
+			t.Fatal(err)
+		}
+
+		estimated := received.Estimate(toTime(in.receiveNTP))
+		diff := estimated.Sub(inTime)
+		if diff < -absSendTimeResolution || absSendTimeResolution < diff {
+			t.Errorf("[%d] Estimated time differs, expected: %v, estimated: %v (receive time: %v)",
+				i, inTime.UTC(), estimated.UTC(), toTime(in.receiveNTP).UTC(),
+			)
+		}
+	}
+}
diff --git a/packetizer_test.go b/packetizer_test.go
index e3c9337..8036d93 100644
--- a/packetizer_test.go
+++ b/packetizer_test.go
@@ -3,30 +3,10 @@ package rtp
 import (
 	"fmt"
 	"testing"
-	"time"
 
 	"github.com/pion/rtp/codecs"
-	"github.com/stretchr/testify/assert"
 )
 
-func TestNtpConversion(t *testing.T) {
-	loc := time.FixedZone("UTC-5", -5*60*60)
-
-	tests := []struct {
-		t time.Time
-		n uint64
-	}{
-		{t: time.Date(1985, time.June, 23, 4, 0, 0, 0, loc), n: 0xa0c65b1000000000},
-		{t: time.Date(1999, time.December, 31, 23, 59, 59, 500000, loc), n: 0xbc18084f0020c49b},
-		{t: time.Date(2019, time.March, 27, 13, 39, 30, 8675309, loc), n: 0xe04641e202388b88},
-	}
-
-	for _, in := range tests {
-		out := toNtpTime(in.t)
-		assert.Equal(t, in.n, out)
-	}
-}
-
 func TestPacketizer(t *testing.T) {
 	multiplepayload := make([]byte, 128)
 	//use the G722 payloader here, because it's very simple and all 0s is valid G722 data.