Skip to content

Commit bc5124c

Browse files
aler9Sean-Der
authored andcommitted
Fix VP9 decoding on iOS
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.
1 parent 12646b6 commit bc5124c

File tree

5 files changed

+598
-60
lines changed

5 files changed

+598
-60
lines changed

codecs/vp9/bits.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package vp9
5+
6+
import "errors"
7+
8+
var errNotEnoughBits = errors.New("not enough bits")
9+
10+
func hasSpace(buf []byte, pos int, n int) error {
11+
if n > ((len(buf) * 8) - pos) {
12+
return errNotEnoughBits
13+
}
14+
return nil
15+
}
16+
17+
func readFlag(buf []byte, pos *int) (bool, error) {
18+
err := hasSpace(buf, *pos, 1)
19+
if err != nil {
20+
return false, err
21+
}
22+
23+
return readFlagUnsafe(buf, pos), nil
24+
}
25+
26+
func readFlagUnsafe(buf []byte, pos *int) bool {
27+
b := (buf[*pos>>0x03] >> (7 - (*pos & 0x07))) & 0x01
28+
*pos++
29+
return b == 1
30+
}
31+
32+
func readBits(buf []byte, pos *int, n int) (uint64, error) {
33+
err := hasSpace(buf, *pos, n)
34+
if err != nil {
35+
return 0, err
36+
}
37+
38+
return readBitsUnsafe(buf, pos, n), nil
39+
}
40+
41+
func readBitsUnsafe(buf []byte, pos *int, n int) uint64 {
42+
res := 8 - (*pos & 0x07)
43+
if n < res {
44+
v := uint64((buf[*pos>>0x03] >> (res - n)) & (1<<n - 1))
45+
*pos += n
46+
return v
47+
}
48+
49+
v := uint64(buf[*pos>>0x03] & (1<<res - 1))
50+
*pos += res
51+
n -= res
52+
53+
for n >= 8 {
54+
v = (v << 8) | uint64(buf[*pos>>0x03])
55+
*pos += 8
56+
n -= 8
57+
}
58+
59+
if n > 0 {
60+
v = (v << n) | uint64(buf[*pos>>0x03]>>(8-n))
61+
*pos += n
62+
}
63+
64+
return v
65+
}

codecs/vp9/header.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
// Package vp9 contains a VP9 header parser.
5+
package vp9
6+
7+
import (
8+
"errors"
9+
)
10+
11+
var (
12+
errInvalidFrameMarker = errors.New("invalid frame marker")
13+
errWrongFrameSyncByte0 = errors.New("wrong frame_sync_byte_0")
14+
errWrongFrameSyncByte1 = errors.New("wrong frame_sync_byte_1")
15+
errWrongFrameSyncByte2 = errors.New("wrong frame_sync_byte_2")
16+
)
17+
18+
// HeaderColorConfig is the color_config member of an header.
19+
type HeaderColorConfig struct {
20+
TenOrTwelveBit bool
21+
BitDepth uint8
22+
ColorSpace uint8
23+
ColorRange bool
24+
SubsamplingX bool
25+
SubsamplingY bool
26+
}
27+
28+
func (c *HeaderColorConfig) unmarshal(profile uint8, buf []byte, pos *int) error {
29+
if profile >= 2 {
30+
var err error
31+
c.TenOrTwelveBit, err = readFlag(buf, pos)
32+
if err != nil {
33+
return err
34+
}
35+
36+
if c.TenOrTwelveBit {
37+
c.BitDepth = 12
38+
} else {
39+
c.BitDepth = 10
40+
}
41+
} else {
42+
c.BitDepth = 8
43+
}
44+
45+
tmp, err := readBits(buf, pos, 3)
46+
if err != nil {
47+
return err
48+
}
49+
c.ColorSpace = uint8(tmp)
50+
51+
if c.ColorSpace != 7 {
52+
var err error
53+
c.ColorRange, err = readFlag(buf, pos)
54+
if err != nil {
55+
return err
56+
}
57+
58+
if profile == 1 || profile == 3 {
59+
err := hasSpace(buf, *pos, 3)
60+
if err != nil {
61+
return err
62+
}
63+
64+
c.SubsamplingX = readFlagUnsafe(buf, pos)
65+
c.SubsamplingY = readFlagUnsafe(buf, pos)
66+
*pos++
67+
} else {
68+
c.SubsamplingX = true
69+
c.SubsamplingY = true
70+
}
71+
} else {
72+
c.ColorRange = true
73+
74+
if profile == 1 || profile == 3 {
75+
c.SubsamplingX = false
76+
c.SubsamplingY = false
77+
78+
err := hasSpace(buf, *pos, 1)
79+
if err != nil {
80+
return err
81+
}
82+
*pos++
83+
}
84+
}
85+
86+
return nil
87+
}
88+
89+
// HeaderFrameSize is the frame_size member of an header.
90+
type HeaderFrameSize struct {
91+
FrameWidthMinus1 uint16
92+
FrameHeightMinus1 uint16
93+
}
94+
95+
func (s *HeaderFrameSize) unmarshal(buf []byte, pos *int) error {
96+
err := hasSpace(buf, *pos, 32)
97+
if err != nil {
98+
return err
99+
}
100+
101+
s.FrameWidthMinus1 = uint16(readBitsUnsafe(buf, pos, 16))
102+
s.FrameHeightMinus1 = uint16(readBitsUnsafe(buf, pos, 16))
103+
return nil
104+
}
105+
106+
// Header is a VP9 Frame header.
107+
// Specification:
108+
// https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf
109+
type Header struct {
110+
Profile uint8
111+
ShowExistingFrame bool
112+
FrameToShowMapIdx uint8
113+
NonKeyFrame bool
114+
ShowFrame bool
115+
ErrorResilientMode bool
116+
ColorConfig *HeaderColorConfig
117+
FrameSize *HeaderFrameSize
118+
}
119+
120+
// Unmarshal decodes a Header.
121+
func (h *Header) Unmarshal(buf []byte) error {
122+
pos := 0
123+
124+
err := hasSpace(buf, pos, 4)
125+
if err != nil {
126+
return err
127+
}
128+
129+
frameMarker := readBitsUnsafe(buf, &pos, 2)
130+
if frameMarker != 2 {
131+
return errInvalidFrameMarker
132+
}
133+
134+
profileLowBit := uint8(readBitsUnsafe(buf, &pos, 1))
135+
profileHighBit := uint8(readBitsUnsafe(buf, &pos, 1))
136+
h.Profile = profileHighBit<<1 + profileLowBit
137+
138+
if h.Profile == 3 {
139+
err = hasSpace(buf, pos, 1)
140+
if err != nil {
141+
return err
142+
}
143+
pos++
144+
}
145+
146+
h.ShowExistingFrame, err = readFlag(buf, &pos)
147+
if err != nil {
148+
return err
149+
}
150+
151+
if h.ShowExistingFrame {
152+
var tmp uint64
153+
tmp, err = readBits(buf, &pos, 3)
154+
if err != nil {
155+
return err
156+
}
157+
h.FrameToShowMapIdx = uint8(tmp)
158+
return nil
159+
}
160+
161+
err = hasSpace(buf, pos, 3)
162+
if err != nil {
163+
return err
164+
}
165+
166+
h.NonKeyFrame = readFlagUnsafe(buf, &pos)
167+
h.ShowFrame = readFlagUnsafe(buf, &pos)
168+
h.ErrorResilientMode = readFlagUnsafe(buf, &pos)
169+
170+
if !h.NonKeyFrame {
171+
err := hasSpace(buf, pos, 24)
172+
if err != nil {
173+
return err
174+
}
175+
176+
frameSyncByte0 := uint8(readBitsUnsafe(buf, &pos, 8))
177+
if frameSyncByte0 != 0x49 {
178+
return errWrongFrameSyncByte0
179+
}
180+
181+
frameSyncByte1 := uint8(readBitsUnsafe(buf, &pos, 8))
182+
if frameSyncByte1 != 0x83 {
183+
return errWrongFrameSyncByte1
184+
}
185+
186+
frameSyncByte2 := uint8(readBitsUnsafe(buf, &pos, 8))
187+
if frameSyncByte2 != 0x42 {
188+
return errWrongFrameSyncByte2
189+
}
190+
191+
h.ColorConfig = &HeaderColorConfig{}
192+
err = h.ColorConfig.unmarshal(h.Profile, buf, &pos)
193+
if err != nil {
194+
return err
195+
}
196+
197+
h.FrameSize = &HeaderFrameSize{}
198+
err = h.FrameSize.unmarshal(buf, &pos)
199+
if err != nil {
200+
return err
201+
}
202+
}
203+
204+
return nil
205+
}
206+
207+
// Width returns the video width.
208+
func (h Header) Width() uint16 {
209+
if h.FrameSize == nil {
210+
return 0
211+
}
212+
return h.FrameSize.FrameWidthMinus1 + 1
213+
}
214+
215+
// Height returns the video height.
216+
func (h Header) Height() uint16 {
217+
if h.FrameSize == nil {
218+
return 0
219+
}
220+
return h.FrameSize.FrameHeightMinus1 + 1
221+
}

codecs/vp9/header_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package vp9
5+
6+
import (
7+
"reflect"
8+
"testing"
9+
)
10+
11+
func TestHeaderUnmarshal(t *testing.T) {
12+
cases := []struct {
13+
name string
14+
byts []byte
15+
sh Header
16+
width uint16
17+
height uint16
18+
}{
19+
{
20+
"chrome webrtc",
21+
[]byte{
22+
0x82, 0x49, 0x83, 0x42, 0x00, 0x77, 0xf0, 0x32,
23+
0x34, 0x30, 0x38, 0x24, 0x1c, 0x19, 0x40, 0x18,
24+
0x03, 0x40, 0x5f, 0xb4,
25+
},
26+
Header{
27+
ShowFrame: true,
28+
ColorConfig: &HeaderColorConfig{
29+
BitDepth: 8,
30+
SubsamplingX: true,
31+
SubsamplingY: true,
32+
},
33+
FrameSize: &HeaderFrameSize{
34+
FrameWidthMinus1: 1919,
35+
FrameHeightMinus1: 803,
36+
},
37+
},
38+
1920,
39+
804,
40+
},
41+
{
42+
"vp9 sample",
43+
[]byte{
44+
0x82, 0x49, 0x83, 0x42, 0x40, 0xef, 0xf0, 0x86,
45+
0xf4, 0x04, 0x21, 0xa0, 0xe0, 0x00, 0x30, 0x70,
46+
0x00, 0x00, 0x00, 0x01,
47+
},
48+
Header{
49+
ShowFrame: true,
50+
ColorConfig: &HeaderColorConfig{
51+
BitDepth: 8,
52+
ColorSpace: 2,
53+
SubsamplingX: true,
54+
SubsamplingY: true,
55+
},
56+
FrameSize: &HeaderFrameSize{
57+
FrameWidthMinus1: 3839,
58+
FrameHeightMinus1: 2159,
59+
},
60+
},
61+
3840,
62+
2160,
63+
},
64+
}
65+
66+
for _, ca := range cases {
67+
t.Run(ca.name, func(t *testing.T) {
68+
var sh Header
69+
err := sh.Unmarshal(ca.byts)
70+
if err != nil {
71+
t.Fatal("unexpected error")
72+
}
73+
74+
if !reflect.DeepEqual(ca.sh, sh) {
75+
t.Fatalf("expected %#+v, got %#+v", ca.sh, sh)
76+
}
77+
if ca.width != sh.Width() {
78+
t.Fatalf("unexpected width")
79+
}
80+
if ca.height != sh.Height() {
81+
t.Fatalf("unexpected height")
82+
}
83+
})
84+
}
85+
}

0 commit comments

Comments
 (0)