Skip to content

Commit

Permalink
Fix VP9 decoding on iOS
Browse files Browse the repository at this point in the history
The current implementation of the VP9 payloader produces payloads that
are not compatible with iOS. This is because the payloader provides
only the muxing strategy called "flexible mode".

According to the VP9 RFC draft, there are two ways to wrap VP9 frames
into RTP packets: the "flexible mode" and the "non-flexible mode", with
the latter being the preferred one for live-streaming applications. In
particular, all browsers encodes VP9 RTP packets in the "non-flexible
mode", while iOS supports decoding RTP packets in this mode only, and
this is probably a problem shared by other implementations.

This patch improves the VP9 payloader by adding support for the
"non-flexible mode". The "flexible mode" is retained and a flag is
provided to perform the switch between the two modes.
  • Loading branch information
aler9 committed Apr 28, 2024
1 parent 12646b6 commit 99874ad
Show file tree
Hide file tree
Showing 16 changed files with 631 additions and 60 deletions.
65 changes: 65 additions & 0 deletions codecs/vp9/bits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package vp9

import "errors"

var errNotEnoughBits = errors.New("not enough bits")

func hasSpace(buf []byte, pos int, n int) error {
if n > ((len(buf) * 8) - pos) {
return errNotEnoughBits
}
return nil
}

func readFlag(buf []byte, pos *int) (bool, error) {
err := hasSpace(buf, *pos, 1)
if err != nil {
return false, err
}

return readFlagUnsafe(buf, pos), nil
}

func readFlagUnsafe(buf []byte, pos *int) bool {
b := (buf[*pos>>0x03] >> (7 - (*pos & 0x07))) & 0x01
*pos++
return b == 1
}

func readBits(buf []byte, pos *int, n int) (uint64, error) {
err := hasSpace(buf, *pos, n)
if err != nil {
return 0, err
}

return readBitsUnsafe(buf, pos, n), nil
}

func readBitsUnsafe(buf []byte, pos *int, n int) uint64 {
res := 8 - (*pos & 0x07)
if n < res {
v := uint64((buf[*pos>>0x03] >> (res - n)) & (1<<n - 1))
*pos += n
return v
}

v := uint64(buf[*pos>>0x03] & (1<<res - 1))
*pos += res
n -= res

for n >= 8 {
v = (v << 8) | uint64(buf[*pos>>0x03])
*pos += 8
n -= 8
}

if n > 0 {
v = (v << n) | uint64(buf[*pos>>0x03]>>(8-n))
*pos += n
}

return v
}
220 changes: 220 additions & 0 deletions codecs/vp9/header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

// Package vp9 contains a VP9 header parser.
package vp9

import (
"errors"
)

var (
errInvalidFrameMarker = errors.New("invalid frame marker")
errWrongFrameSyncByte0 = errors.New("wrong frame_sync_byte_0")
errWrongFrameSyncByte1 = errors.New("wrong frame_sync_byte_1")
errWrongFrameSyncByte2 = errors.New("wrong frame_sync_byte_2")
)

// HeaderColorConfig is the color_config member of an header.
type HeaderColorConfig struct {
TenOrTwelveBit bool
BitDepth uint8
ColorSpace uint8
ColorRange bool
SubsamplingX bool
SubsamplingY bool
}

func (c *HeaderColorConfig) unmarshal(profile uint8, buf []byte, pos *int) error {
if profile >= 2 {
var err error
c.TenOrTwelveBit, err = readFlag(buf, pos)
if err != nil {
return err
}

if c.TenOrTwelveBit {
c.BitDepth = 12
} else {
c.BitDepth = 10

Check warning on line 39 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L36-L39

Added lines #L36 - L39 were not covered by tests
}
} else {
c.BitDepth = 8
}

tmp, err := readBits(buf, pos, 3)
if err != nil {
return err
}
c.ColorSpace = uint8(tmp)

if c.ColorSpace != 7 {
var err error
c.ColorRange, err = readFlag(buf, pos)
if err != nil {
return err

Check warning on line 55 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L55

Added line #L55 was not covered by tests
}

if profile == 1 || profile == 3 {
err := hasSpace(buf, *pos, 3)
if err != nil {
return err

Check warning on line 61 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L61

Added line #L61 was not covered by tests
}

c.SubsamplingX = readFlagUnsafe(buf, pos)
c.SubsamplingY = readFlagUnsafe(buf, pos)
*pos++
} else {
c.SubsamplingX = true
c.SubsamplingY = true
}
} else {
c.ColorRange = true

Check warning on line 72 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L71-L72

Added lines #L71 - L72 were not covered by tests

if profile == 1 || profile == 3 {
c.SubsamplingX = false
c.SubsamplingY = false

Check warning on line 76 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L74-L76

Added lines #L74 - L76 were not covered by tests

err := hasSpace(buf, *pos, 1)
if err != nil {
return err

Check warning on line 80 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L78-L80

Added lines #L78 - L80 were not covered by tests
}
*pos++

Check warning on line 82 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L82

Added line #L82 was not covered by tests
}
}

return nil
}

// HeaderFrameSize is the frame_size member of an header.
type HeaderFrameSize struct {
FrameWidthMinus1 uint16
FrameHeightMinus1 uint16
}

func (s *HeaderFrameSize) unmarshal(buf []byte, pos *int) error {
err := hasSpace(buf, *pos, 32)
if err != nil {
return err
}

s.FrameWidthMinus1 = uint16(readBitsUnsafe(buf, pos, 16))
s.FrameHeightMinus1 = uint16(readBitsUnsafe(buf, pos, 16))
return nil
}

// Header is a VP9 Frame header.
// Specification:
// https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf
type Header struct {
Profile uint8
ShowExistingFrame bool
FrameToShowMapIdx uint8
NonKeyFrame bool
ShowFrame bool
ErrorResilientMode bool
ColorConfig *HeaderColorConfig
FrameSize *HeaderFrameSize
}

// Unmarshal decodes a Header.
func (h *Header) Unmarshal(buf []byte) error {
pos := 0

err := hasSpace(buf, pos, 4)
if err != nil {
return err
}

frameMarker := readBitsUnsafe(buf, &pos, 2)
if frameMarker != 2 {
return errInvalidFrameMarker
}

profileLowBit := uint8(readBitsUnsafe(buf, &pos, 1))
profileHighBit := uint8(readBitsUnsafe(buf, &pos, 1))
h.Profile = profileHighBit<<1 + profileLowBit

if h.Profile == 3 {
err := hasSpace(buf, pos, 1)

Check failure on line 139 in codecs/vp9/header.go

View workflow job for this annotation

GitHub Actions / lint / Go

shadow: declaration of "err" shadows declaration at line 124 (govet)
if err != nil {
return err

Check warning on line 141 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L141

Added line #L141 was not covered by tests
}
pos++
}

h.ShowExistingFrame, err = readFlag(buf, &pos)
if err != nil {
return err

Check warning on line 148 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L148

Added line #L148 was not covered by tests
}

if h.ShowExistingFrame {
tmp, err := readBits(buf, &pos, 3)

Check failure on line 152 in codecs/vp9/header.go

View workflow job for this annotation

GitHub Actions / lint / Go

shadow: declaration of "err" shadows declaration at line 124 (govet)
if err != nil {
return err
}
h.FrameToShowMapIdx = uint8(tmp)
return nil

Check warning on line 157 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L156-L157

Added lines #L156 - L157 were not covered by tests
}

err = hasSpace(buf, pos, 3)
if err != nil {
return err
}

h.NonKeyFrame = readFlagUnsafe(buf, &pos)
h.ShowFrame = readFlagUnsafe(buf, &pos)
h.ErrorResilientMode = readFlagUnsafe(buf, &pos)

if !h.NonKeyFrame {
err := hasSpace(buf, pos, 24)
if err != nil {
return err
}

frameSyncByte0 := uint8(readBitsUnsafe(buf, &pos, 8))
if frameSyncByte0 != 0x49 {
return errWrongFrameSyncByte0
}

frameSyncByte1 := uint8(readBitsUnsafe(buf, &pos, 8))
if frameSyncByte1 != 0x83 {
return errWrongFrameSyncByte1
}

frameSyncByte2 := uint8(readBitsUnsafe(buf, &pos, 8))
if frameSyncByte2 != 0x42 {
return errWrongFrameSyncByte2
}

h.ColorConfig = &HeaderColorConfig{}
err = h.ColorConfig.unmarshal(h.Profile, buf, &pos)
if err != nil {
return err
}

h.FrameSize = &HeaderFrameSize{}
err = h.FrameSize.unmarshal(buf, &pos)
if err != nil {
return err
}
}

return nil
}

// Width returns the video width.
func (h Header) Width() uint16 {
if h.FrameSize == nil {
return 0

Check warning on line 209 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L209

Added line #L209 was not covered by tests
}
return h.FrameSize.FrameWidthMinus1 + 1
}

// Height returns the video height.
func (h Header) Height() uint16 {
if h.FrameSize == nil {
return 0

Check warning on line 217 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L217

Added line #L217 was not covered by tests
}
return h.FrameSize.FrameHeightMinus1 + 1
}
96 changes: 96 additions & 0 deletions codecs/vp9/header_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package vp9

import (
"reflect"
"testing"
)

var cases = []struct {

Check failure on line 11 in codecs/vp9/header_test.go

View workflow job for this annotation

GitHub Actions / lint / Go

cases is a global variable (gochecknoglobals)
name string
byts []byte
sh Header
width uint16
height uint16
}{
{
"chrome webrtc",
[]byte{
0x82, 0x49, 0x83, 0x42, 0x00, 0x77, 0xf0, 0x32,
0x34, 0x30, 0x38, 0x24, 0x1c, 0x19, 0x40, 0x18,
0x03, 0x40, 0x5f, 0xb4,
},
Header{
ShowFrame: true,
ColorConfig: &HeaderColorConfig{
BitDepth: 8,
SubsamplingX: true,
SubsamplingY: true,
},
FrameSize: &HeaderFrameSize{
FrameWidthMinus1: 1919,
FrameHeightMinus1: 803,
},
},
1920,
804,
},
{
"vp9 sample",
[]byte{
0x82, 0x49, 0x83, 0x42, 0x40, 0xef, 0xf0, 0x86,
0xf4, 0x04, 0x21, 0xa0, 0xe0, 0x00, 0x30, 0x70,
0x00, 0x00, 0x00, 0x01,
},
Header{
ShowFrame: true,
ColorConfig: &HeaderColorConfig{
BitDepth: 8,
ColorSpace: 2,
SubsamplingX: true,
SubsamplingY: true,
},
FrameSize: &HeaderFrameSize{
FrameWidthMinus1: 3839,
FrameHeightMinus1: 2159,
},
},
3840,
2160,
},
}

func TestHeaderUnmarshal(t *testing.T) {
for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) {
var sh Header
err := sh.Unmarshal(ca.byts)
if err != nil {
t.Fatal("unexpected error")
}

if !reflect.DeepEqual(ca.sh, sh) {
t.Fatalf("expected %#+v, got %#+v", ca.sh, sh)
}
if ca.width != sh.Width() {
t.Fatalf("unexpected width")
}
if ca.height != sh.Height() {
t.Fatalf("unexpected height")
}
})
}
}

func FuzzHeaderUnmarshal(f *testing.F) {
for _, ca := range cases {
f.Add(ca.byts)
}

f.Fuzz(func(_ *testing.T, b []byte) {
var sh Header
sh.Unmarshal(b) //nolint:errcheck

Check failure on line 94 in codecs/vp9/header_test.go

View workflow job for this annotation

GitHub Actions / lint / Go

G104: Errors unhandled. (gosec)
})
}
2 changes: 2 additions & 0 deletions codecs/vp9/testdata/fuzz/FuzzHeaderUnmarshal/02ff90541f5f1194
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\xa3000")
2 changes: 2 additions & 0 deletions codecs/vp9/testdata/fuzz/FuzzHeaderUnmarshal/26f4501756141118
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\xa3I00")
2 changes: 2 additions & 0 deletions codecs/vp9/testdata/fuzz/FuzzHeaderUnmarshal/32c8b8b4246d92a8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\x91")
2 changes: 2 additions & 0 deletions codecs/vp9/testdata/fuzz/FuzzHeaderUnmarshal/49ada3d55320a56f
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("\xa3I\x83B")
2 changes: 2 additions & 0 deletions codecs/vp9/testdata/fuzz/FuzzHeaderUnmarshal/582528ddfad69eb5
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0")

0 comments on commit 99874ad

Please sign in to comment.