Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix VP9 decoding on iOS #269

Merged
merged 1 commit into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
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")
}
})
}
}