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 27, 2024
1 parent 12646b6 commit 0532c6a
Show file tree
Hide file tree
Showing 4 changed files with 523 additions and 64 deletions.
67 changes: 67 additions & 0 deletions codecs/vp9/bits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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 {
v := uint64(0)

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

Check failure on line 46 in codecs/vp9/bits.go

View workflow job for this annotation

GitHub Actions / lint / Go

shadow: declaration of "v" shadows declaration at line 42 (govet)
*pos += n
return v
}

v = (v << res) | 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
}
213 changes: 213 additions & 0 deletions codecs/vp9/header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// 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 31 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L28-L31

Added lines #L28 - L31 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L34-L37

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

tmp, err := readBits(buf, pos, 3)
if err != nil {
return err

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L45

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L53

Added line #L53 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L57-L59

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

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L62-L64

Added lines #L62 - L64 were not covered by tests
} else {
c.SubsamplingX = true
c.SubsamplingY = true
}
} else {
c.ColorRange = true

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L69-L70

Added lines #L69 - L70 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L72-L74

Added lines #L72 - L74 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L76-L78

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L80

Added line #L80 was not covered by tests
}
}

return nil
}

// Header_FrameSize is the frame_size member of an header.
type Header_FrameSize struct { //nolint:revive

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

View workflow job for this annotation

GitHub Actions / lint / Go

ST1003: should not use underscores in Go names; type Header_FrameSize should be HeaderFrameSize (stylecheck)
FrameWidthMinus1 uint16
FrameHeightMinus1 uint16
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L96

Added line #L96 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
IsKeyFrame bool
ShowFrame bool
ErrorResilientMode bool
ColorConfig *HeaderColorConfig
FrameSize *Header_FrameSize
}

// 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 129 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L129

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

View workflow job for this annotation

GitHub Actions / lint / Go

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L137-L139

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

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
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L146

Added line #L146 was not covered by tests
}

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

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

View workflow job for this annotation

GitHub Actions / lint / Go

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L150-L152

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L154

Added line #L154 was not covered by tests

return nil

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L156

Added line #L156 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L161

Added line #L161 was not covered by tests
}

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

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L171

Added line #L171 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L176

Added line #L176 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L181

Added line #L181 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L186

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L192

Added line #L192 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L198

Added line #L198 was not covered by tests
}
}

return nil
}

// Width returns the video width.
func (h Header) Width() int {
return int(h.FrameSize.FrameWidthMinus1) + 1
}

// Height returns the video height.
func (h Header) Height() int {
return int(h.FrameSize.FrameHeightMinus1) + 1
}
Loading

0 comments on commit 0532c6a

Please sign in to comment.