Repository: pion/rtp
Pull request #55 (2020-05-02 17:28:17)

Support Audio Level Indication
Adds support for parsing the audio level extension header as described in https://tools.ietf.org/html/rfc6464.

Supports decoding both one and two byte formats but only encodes one byte. Let me know if we would like to support marshaling into two byte as well. In that case let me know what interface would make sense.

#54 



codecov[bot]: # [Codecov](https://codecov.io/gh/pion/rtp/pull/55?src=pr&el=h1) Report
> Merging [#55](https://codecov.io/gh/pion/rtp/pull/55?src=pr&el=desc) into [master](https://codecov.io/gh/pion/rtp/commit/8c17fece3019701b86f7ebfa70faa66c96fe8156&el=desc) will **increase** coverage by `0.66%`.
> The diff coverage is `100.00%`.

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

```diff
@@            Coverage Diff             @@
##           master      #55      +/-   ##
==========================================
+ Coverage   87.12%   87.78%   +0.66%     
==========================================
  Files          12       13       +1     
  Lines         497      524      +27     
==========================================
+ Hits          433      460      +27     
  Misses         50       50              
  Partials       14       14              
```

| Flag | Coverage Δ | |
|---|---|---|
| #go | `87.78% <100.00%> (+0.66%)` | :arrow_up: |
| #wasm | `87.78% <100.00%> (+0.66%)` | :arrow_up: |

| [Impacted Files](https://codecov.io/gh/pion/rtp/pull/55?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [audiolevelextension.go](https://codecov.io/gh/pion/rtp/pull/55/diff?src=pr&el=tree#diff-YXVkaW9sZXZlbGV4dGVuc2lvbi5nbw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pion/rtp/pull/55?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/55?src=pr&el=footer). Last update [8c17fec...70890e7](https://codecov.io/gh/pion/rtp/pull/55?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

tarrencev: Updated PR to follow the chromium implementation. A little simpler :) 

Patch:
diff --git a/README.md b/README.md
index 8296629..0796a5c 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contribu
 * [Atsushi Watanabe](https://github.com/at-wat)
 * [adwpc](https://github.com/adwpc) *add transport-cc extension*
 * [Bao Nguyen](https://github.com/sysbot) *add VP9 noop, bug fixes.
+* [Tarrence van As](https://github.com/tarrencev) *add audio level extension*
 
 ### License
 MIT License - see [LICENSE](LICENSE) for full text
diff --git a/audiolevelextension.go b/audiolevelextension.go
new file mode 100644
index 0000000..f85f6b0
--- /dev/null
+++ b/audiolevelextension.go
@@ -0,0 +1,82 @@
+package rtp
+
+import (
+	"errors"
+)
+
+const (
+	// AudioLevelOneByteExtensionSize One byte header size
+	AudioLevelOneByteExtensionSize = 2
+	// AudioLevelTwoByteExtensionSize Two byte header size
+	AudioLevelTwoByteExtensionSize = 4
+)
+
+var (
+	errInvalidSize           = errors.New("invalid buffer size")
+	errInvalidExtensonLength = errors.New("invalid extension length")
+	errAudioLevelOverflow    = errors.New("audio level overflow")
+)
+
+// AudioLevelExtension is a extension payload format described in
+// https://tools.ietf.org/html/rfc6464
+//
+// Implementation based on:
+// https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/source/rtp_header_extensions.cc#49
+//
+// One byte format:
+// 0                   1
+// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |  ID   | len=0 |V| level       |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+//
+// Two byte format:
+// 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=1     |V|    level    |    0 (pad)    |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+type AudioLevelExtension struct {
+	ID    uint8
+	Level uint8
+	Voice bool
+}
+
+// Marshal serializes the members to buffer
+func (a *AudioLevelExtension) Marshal() ([]byte, error) {
+	if a.Level > 127 {
+		return nil, errAudioLevelOverflow
+	}
+	voice := uint8(0x00)
+	if a.Voice {
+		voice = 0x80
+	}
+	buf := make([]byte, AudioLevelOneByteExtensionSize)
+	buf[0] = a.ID << 4 & 0xf0
+	buf[1] = voice | a.Level
+	return buf, nil
+}
+
+// Unmarshal parses the passed byte slice and stores the result in the members
+func (a *AudioLevelExtension) Unmarshal(rawData []byte) error {
+	// one byte format
+	switch len(rawData) {
+	case AudioLevelOneByteExtensionSize:
+		if rawData[0]&^0xF0 != 0 {
+			return errInvalidExtensonLength
+		}
+		a.ID = rawData[0] >> 4
+		a.Level = rawData[1] & 0x7F
+		a.Voice = rawData[1]&0x80 != 0
+		return nil
+	case AudioLevelTwoByteExtensionSize:
+		if rawData[1] != 1 {
+			return errInvalidExtensonLength
+		}
+		a.ID = rawData[0]
+		a.Level = rawData[2] & 0x7F
+		a.Voice = rawData[2]&0x80 != 0
+		return nil
+	}
+	return errInvalidSize
+}


Test patch:
diff --git a/audiolevelextension_test.go b/audiolevelextension_test.go
new file mode 100644
index 0000000..0314eb2
--- /dev/null
+++ b/audiolevelextension_test.go
@@ -0,0 +1,149 @@
+package rtp
+
+import (
+	"bytes"
+	"testing"
+)
+
+func TestAudioLevelExtensionTooSmall(t *testing.T) {
+	a := AudioLevelExtension{}
+
+	rawData := []byte{}
+
+	if err := a.Unmarshal(rawData); err != errInvalidSize {
+		t.Fatal("err != errInvalidSize")
+	}
+}
+
+func TestAudioLevelExtensionTooBig(t *testing.T) {
+	a := AudioLevelExtension{}
+
+	rawData := []byte{
+		0x00, 0x00, 0x00, 0x00, 0x00,
+	}
+
+	if err := a.Unmarshal(rawData); err != errInvalidSize {
+		t.Fatal("err != errInvalidSize")
+	}
+}
+
+func TestAudioLevelOneByteExtensionInvalidLength(t *testing.T) {
+	a := AudioLevelExtension{}
+
+	rawData := []byte{
+		0x31, 0x88,
+	}
+
+	if err := a.Unmarshal(rawData); err != errInvalidExtensonLength {
+		t.Fatal("err != errInvalidExtensonLength")
+	}
+}
+
+func TestAudioLevelTwoByteExtensionInvalidLength(t *testing.T) {
+	a := AudioLevelExtension{}
+
+	rawData := []byte{
+		0x30, 0x00, 0x00, 0x00,
+	}
+
+	if err := a.Unmarshal(rawData); err != errInvalidExtensonLength {
+		t.Fatal("err != errInvalidExtensonLength")
+	}
+}
+
+func TestAudioLevelOneByteExtensionVoiceTrue(t *testing.T) {
+	a1 := AudioLevelExtension{}
+
+	rawData := []byte{
+		0x30, 0x88,
+	}
+
+	if err := a1.Unmarshal(rawData); err != nil {
+		t.Fatal("Unmarshal error on extension data")
+	}
+
+	a2 := AudioLevelExtension{
+		ID:    3,
+		Level: 8,
+		Voice: true,
+	}
+
+	if a1 != a2 {
+		t.Error("Unmarshal failed")
+	}
+
+	dstData, _ := a2.Marshal()
+	if !bytes.Equal(dstData, rawData) {
+		t.Error("Marshal failed")
+	}
+}
+
+func TestAudioLevelOneByteExtensionVoiceFalse(t *testing.T) {
+	a1 := AudioLevelExtension{}
+
+	rawData := []byte{
+		0x30, 0x8,
+	}
+
+	if err := a1.Unmarshal(rawData); err != nil {
+		t.Fatal("Unmarshal error on extension data")
+	}
+
+	a2 := AudioLevelExtension{
+		ID:    3,
+		Level: 8,
+		Voice: false,
+	}
+
+	if a1 != a2 {
+		t.Error("Unmarshal failed")
+	}
+
+	dstData, _ := a2.Marshal()
+	if !bytes.Equal(dstData, rawData) {
+		t.Error("Marshal failed")
+	}
+}
+
+func TestAudioLevelOneByteExtensionLevelOverflow(t *testing.T) {
+	a := AudioLevelExtension{
+		ID:    3,
+		Level: 128,
+		Voice: false,
+	}
+
+	if _, err := a.Marshal(); err != errAudioLevelOverflow {
+		t.Fatal("err != errAudioLevelOverflow")
+	}
+}
+
+func TestAudioLevelTwoByteExtensionVoiceFalse(t *testing.T) {
+	a1 := AudioLevelExtension{}
+
+	oneByteRawData := []byte{
+		0x30, 0x8,
+	}
+
+	twoByteRawData := []byte{
+		0x3, 0x1, 0x8, 0x0,
+	}
+
+	if err := a1.Unmarshal(twoByteRawData); err != nil {
+		t.Fatal("Unmarshal error on extension data")
+	}
+
+	a2 := AudioLevelExtension{
+		ID:    3,
+		Level: 8,
+		Voice: false,
+	}
+
+	if a1 != a2 {
+		t.Error("Unmarshal failed")
+	}
+
+	dstData, _ := a2.Marshal()
+	if !bytes.Equal(dstData, oneByteRawData) {
+		t.Error("Marshal failed")
+	}
+}