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 7cd7560
Show file tree
Hide file tree
Showing 4 changed files with 521 additions and 64 deletions.
62 changes: 62 additions & 0 deletions codecs/vp9/bits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package vp9

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

View workflow job for this annotation

GitHub Actions / lint / Go

package-comments: should have a package comment (revive)

import "fmt"

func hasSpace(buf []byte, pos int, n int) error {
if n > ((len(buf) * 8) - pos) {
return fmt.Errorf("not enough bits")

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

View workflow job for this annotation

GitHub Actions / lint / Go

err113: do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"not enough bits\")" (goerr113)
}
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 15 in codecs/vp9/bits.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/bits.go#L15

Added line #L15 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 30 in codecs/vp9/bits.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/bits.go#L30

Added line #L30 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 41 in codecs/vp9/bits.go

View workflow job for this annotation

GitHub Actions / lint / Go

shadow: declaration of "v" shadows declaration at line 37 (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
}
216 changes: 216 additions & 0 deletions codecs/vp9/header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package vp9

import (
"fmt"
)

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

func (c *Header_ColorConfig) 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 22 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L19-L22

Added lines #L19 - L22 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L25-L28

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

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L36

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L44

Added line #L44 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L48-L50

Added lines #L48 - L50 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L53-L55

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L60-L61

Added lines #L60 - L61 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L63-L65

Added lines #L63 - L65 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L67-L69

Added lines #L67 - L69 were not covered by tests
}
*pos++

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L71

Added line #L71 was not covered by tests
}
}

return nil
}

// Header_FrameSize is the frame_size member of an header.
type Header_FrameSize struct { //nolint:revive
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 87 in codecs/vp9/header.go

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L87

Added line #L87 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 *Header_ColorConfig
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 fmt.Errorf("invalid frame marker")

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

View workflow job for this annotation

GitHub Actions / lint / Go

err113: do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"invalid frame marker\")" (goerr113)

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L120

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

View workflow job for this annotation

GitHub Actions / lint / Go

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L128-L130

Added lines #L128 - L130 were not covered by tests
}
pos++

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L132

Added line #L132 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L137

Added line #L137 was not covered by tests
}

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

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

View workflow job for this annotation

GitHub Actions / lint / Go

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L141-L143

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L145

Added line #L145 was not covered by tests

return nil

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L147

Added line #L147 was not covered by tests
}

err = hasSpace(buf, pos, 3)
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#L152

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L162

Added line #L162 was not covered by tests
}

frameSyncByte0 := uint8(readBitsUnsafe(buf, &pos, 8))
if frameSyncByte0 != 0x49 {
return fmt.Errorf("wrong frame_sync_byte_0")

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

View workflow job for this annotation

GitHub Actions / lint / Go

err113: do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"wrong frame_sync_byte_0\")" (goerr113)

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L167

Added line #L167 was not covered by tests
}

frameSyncByte1 := uint8(readBitsUnsafe(buf, &pos, 8))
if frameSyncByte1 != 0x83 {
return fmt.Errorf("wrong frame_sync_byte_1")

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

View workflow job for this annotation

GitHub Actions / lint / Go

err113: do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"wrong frame_sync_byte_1\")" (goerr113)

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L172

Added line #L172 was not covered by tests
}

frameSyncByte2 := uint8(readBitsUnsafe(buf, &pos, 8))
if frameSyncByte2 != 0x42 {
return fmt.Errorf("wrong frame_sync_byte_2")

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

View workflow job for this annotation

GitHub Actions / lint / Go

err113: do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"wrong frame_sync_byte_2\")" (goerr113)

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L177

Added line #L177 was not covered by tests
}

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

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
}

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

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L189

Added line #L189 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
}

// ChromaSubsampling returns the chroma subsampling format, in ISO-BMFF/vpcC format.
func (h Header) ChromaSubsampling() uint8 {
switch {
case !h.ColorConfig.SubsamplingX && !h.ColorConfig.SubsamplingY:
return 3 // 4:4:4
case h.ColorConfig.SubsamplingX && !h.ColorConfig.SubsamplingY:
return 2 // 4:2:2
default:
return 1 // 4:2:0 colocated with luma

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

View check run for this annotation

Codecov / codecov/patch

codecs/vp9/header.go#L207-L214

Added lines #L207 - L214 were not covered by tests
}
}

0 comments on commit 7cd7560

Please sign in to comment.