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 d7dea6e
Show file tree
Hide file tree
Showing 5 changed files with 599 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

Check warning on line 20 in codecs/vp9/bits.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/bits.go#L20

Added line #L20 was not covered by tests
}

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

Check warning on line 35 in codecs/vp9/bits.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/bits.go#L35

Added line #L35 was not covered by tests
}

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
}
221 changes: 221 additions & 0 deletions codecs/vp9/header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// 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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L30-L33

Added lines #L30 - L33 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L47

Added line #L47 was not covered by tests
}
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#L59-L61

Added lines #L59 - L61 were not covered by tests
}

c.SubsamplingX = readFlagUnsafe(buf, pos)
c.SubsamplingY = readFlagUnsafe(buf, pos)
*pos++

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L64-L66

Added lines #L64 - L66 were not covered by tests
} 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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L98

Added line #L98 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L131

Added line #L131 was not covered by tests
}

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#L139-L141

Added lines #L139 - L141 were not covered by tests
}
pos++

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L143

Added line #L143 was not covered by tests
}

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 {
var tmp uint64
tmp, err = readBits(buf, &pos, 3)
if err != nil {
return err

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L152-L155

Added lines #L152 - L155 were not covered by tests
}
h.FrameToShowMapIdx = uint8(tmp)
return nil

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L157-L158

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

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L163

Added line #L163 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L173

Added line #L173 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L178

Added line #L178 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L183

Added line #L183 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L188

Added line #L188 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L194

Added line #L194 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L200

Added line #L200 was not covered by tests
}
}

return nil
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L210

Added line #L210 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 218 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L218

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

package vp9

import (
"reflect"
"testing"
)

func TestHeaderUnmarshal(t *testing.T) {
cases := []struct {
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,
},
}

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")
}
})
}
}
Loading

0 comments on commit d7dea6e

Please sign in to comment.