Skip to content

Commit 481912b

Browse files
authored
Add VP8 depayloader (#45)
1 parent 9276b18 commit 481912b

File tree

5 files changed

+378
-0
lines changed

5 files changed

+378
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
defmodule ExWebRTC.RTP.VP8Depayloader do
2+
@moduledoc """
3+
Reassembles VP8 frames from RTP packets.
4+
5+
Based on [RFC 7741: RTP Payload Format for VP8 Video](https://datatracker.ietf.org/doc/html/rfc7741)
6+
"""
7+
require Logger
8+
9+
alias ExWebRTC.RTP.VP8Payload
10+
11+
@opaque t() :: %__MODULE__{
12+
current_frame: nil,
13+
current_timestamp: nil
14+
}
15+
16+
defstruct [:current_frame, :current_timestamp]
17+
18+
@spec new() :: t()
19+
def new() do
20+
%__MODULE__{}
21+
end
22+
23+
@spec write(t(), ExRTP.Packet.t()) :: {:ok, t()} | {:ok, binary(), t()}
24+
def write(depayloader, packet) do
25+
with {:ok, vp8_payload} <- VP8Payload.parse(packet.payload) do
26+
depayloader =
27+
case {depayloader.current_frame, vp8_payload} do
28+
{nil, %VP8Payload{s: 1, pid: 0}} ->
29+
%{
30+
depayloader
31+
| current_frame: vp8_payload.payload,
32+
current_timestamp: packet.timestamp
33+
}
34+
35+
{nil, _vp8_payload} ->
36+
Logger.debug("Dropping vp8 payload as it doesn't start a new frame")
37+
depayloader
38+
39+
{_current_frame, %VP8Payload{s: 1, pid: 0}} ->
40+
Logger.debug("""
41+
Received packet that starts a new frame without finishing the previous frame. \
42+
Droping previous frame.\
43+
""")
44+
45+
%{
46+
depayloader
47+
| current_frame: vp8_payload.payload,
48+
current_timestamp: packet.timestamp
49+
}
50+
51+
_ when packet.timestamp != depayloader.current_timestamp ->
52+
Logger.debug("""
53+
Received packet with timestamp from a new frame that is not a beginning of this frame \
54+
and without finishing the previous frame. Droping both.\
55+
""")
56+
57+
%{depayloader | current_frame: nil, current_timestamp: nil}
58+
59+
{current_frame, vp8_payload} ->
60+
%{depayloader | current_frame: current_frame <> vp8_payload.payload}
61+
end
62+
63+
case {depayloader.current_frame, packet.marker} do
64+
{current_frame, true} when current_frame != nil ->
65+
{:ok, current_frame, %{depayloader | current_frame: nil, current_timestamp: nil}}
66+
67+
_ ->
68+
{:ok, depayloader}
69+
end
70+
end
71+
end
72+
end

lib/ex_webrtc/rtp/vp8_payload.ex

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
defmodule ExWebRTC.RTP.VP8Payload do
2+
@moduledoc """
3+
Defines VP8 payload structure stored in RTP packet payload.
4+
5+
Based on [RFC 7741: RTP Payload Format for VP8 Video](https://datatracker.ietf.org/doc/html/rfc7741)
6+
"""
7+
8+
@type t() :: %__MODULE__{
9+
n: 0 | 1,
10+
s: 0 | 1,
11+
pid: non_neg_integer(),
12+
picture_id: non_neg_integer() | nil,
13+
tl0picidx: non_neg_integer() | nil,
14+
tid: non_neg_integer() | nil,
15+
y: 0 | 1 | nil,
16+
keyidx: non_neg_integer() | nil,
17+
payload: binary()
18+
}
19+
20+
@enforce_keys [:n, :s, :pid, :payload]
21+
defstruct @enforce_keys ++ [:picture_id, :tl0picidx, :tid, :y, :keyidx]
22+
23+
@doc """
24+
Parses RTP payload as VP8 payload.
25+
"""
26+
@spec parse(binary()) :: {:ok, t()} | {:error, :invalid_packet}
27+
def parse(rtp_payload)
28+
29+
def parse(<<>>), do: {:error, :invalid_packet}
30+
31+
def parse(<<0::1, 0::1, n::1, s::1, 0::1, pid::3, payload::binary>>) do
32+
if payload == <<>> do
33+
{:error, :invalid_packet}
34+
else
35+
{:ok,
36+
%__MODULE__{
37+
n: n,
38+
s: s,
39+
pid: pid,
40+
payload: payload
41+
}}
42+
end
43+
end
44+
45+
def parse(<<1::1, 0::1, n::1, s::1, 0::1, pid::3, i::1, l::1, t::1, k::1, 0::4, rest::binary>>) do
46+
with {:ok, picture_id, rest} <- parse_picture_id(i, rest),
47+
{:ok, tl0picidx, rest} <- parse_tl0picidx(l, rest),
48+
{:ok, tid, y, keyidx, rest} <- parse_tidykeyidx(t, k, rest) do
49+
if rest == <<>> do
50+
{:error, :invalid_packet}
51+
else
52+
{:ok,
53+
%__MODULE__{
54+
n: n,
55+
s: s,
56+
pid: pid,
57+
picture_id: picture_id,
58+
tl0picidx: tl0picidx,
59+
tid: tid,
60+
y: y,
61+
keyidx: keyidx,
62+
payload: rest
63+
}}
64+
end
65+
end
66+
end
67+
68+
def parse(_), do: {:error, :invalid_packet}
69+
70+
defp parse_picture_id(0, rest),
71+
do: {:ok, nil, rest}
72+
73+
defp parse_picture_id(1, <<0::1, picture_id::7, rest::binary>>), do: {:ok, picture_id, rest}
74+
defp parse_picture_id(1, <<1::1, picture_id::15, rest::binary>>), do: {:ok, picture_id, rest}
75+
defp parse_picture_id(_, _), do: {:error, :invalid_packet}
76+
77+
defp parse_tl0picidx(0, rest), do: {:ok, nil, rest}
78+
defp parse_tl0picidx(1, <<tl0picidx, rest::binary>>), do: {:ok, tl0picidx, rest}
79+
defp parse_tl0picidx(_, _), do: {:error, :invalid_packet}
80+
81+
defp parse_tidykeyidx(0, 0, rest), do: {:ok, nil, nil, nil, rest}
82+
83+
defp parse_tidykeyidx(1, 0, <<tid::2, y::1, _keyidx::5, rest::binary>>),
84+
do: {:ok, tid, y, nil, rest}
85+
86+
# note that both pion and web browser always set y bit to 0 in this case
87+
# but RFC 7741, sec. 4.2 (definition for Y bit) explicitly states that Y bit
88+
# can be set when T is 0 and K is 1
89+
defp parse_tidykeyidx(0, 1, <<_tid::2, y::1, keyidx::5, rest::binary>>),
90+
do: {:ok, nil, y, keyidx, rest}
91+
92+
defp parse_tidykeyidx(1, 1, <<tid::2, y::1, keyidx::5, rest::binary>>),
93+
do: {:ok, tid, y, keyidx, rest}
94+
95+
defp parse_tidykeyidx(_, _, _), do: {:error, :invalid_packet}
96+
97+
@spec serialize(t()) :: binary()
98+
def serialize(
99+
%__MODULE__{
100+
picture_id: nil,
101+
tl0picidx: nil,
102+
tid: nil,
103+
y: nil,
104+
keyidx: nil
105+
} = vp8_payload
106+
) do
107+
p = vp8_payload
108+
<<0::1, 0::1, p.n::1, p.s::1, 0::1, p.pid::3, p.payload::binary>>
109+
end
110+
111+
def serialize(vp8_payload) do
112+
p = vp8_payload
113+
i = if p.picture_id, do: 1, else: 0
114+
l = if p.tl0picidx, do: 1, else: 0
115+
t = if p.tid, do: 1, else: 0
116+
k = if p.keyidx, do: 1, else: 0
117+
118+
payload =
119+
<<1::1, 0::1, p.n::1, p.s::1, 0::1, p.pid::3, i::1, l::1, t::1, k::1, 0::4>>
120+
|> add_picture_id(p.picture_id)
121+
|> add_tl0picidx(p.tl0picidx)
122+
|> add_tidykeyidx(p.tid, p.y, p.keyidx)
123+
124+
<<payload::binary, vp8_payload.payload::binary>>
125+
end
126+
127+
defp add_picture_id(payload, nil), do: payload
128+
129+
defp add_picture_id(payload, picture_id) when picture_id in 0..127 do
130+
<<payload::binary, 0::1, picture_id::7>>
131+
end
132+
133+
defp add_picture_id(payload, picture_id) when picture_id in 128..32_767 do
134+
<<payload::binary, 1::1, picture_id::15>>
135+
end
136+
137+
defp add_tl0picidx(payload, nil), do: payload
138+
139+
defp add_tl0picidx(payload, tl0picidx) do
140+
<<payload::binary, tl0picidx>>
141+
end
142+
143+
defp add_tidykeyidx(payload, nil, nil, nil), do: payload
144+
145+
defp add_tidykeyidx(_payload, tid, nil, _keyidx) when tid != nil,
146+
do: raise("VP8 Y bit has to be set when TID is set")
147+
148+
defp add_tidykeyidx(payload, tid, y, keyidx) do
149+
<<payload::binary, tid || 0::2, y || 0::1, keyidx || 0::5>>
150+
end
151+
end

lib/ex_webrtc/rtp/vp8_payloader.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ defmodule ExWebRTC.RTP.VP8Payloader do
22
@moduledoc """
33
Encapsulates VP8 video frames into RTP packets.
44
5+
Based on [RFC 7741: RTP Payload Format for VP8 Video](https://datatracker.ietf.org/doc/html/rfc7741)
6+
57
It does not support `X` bit right now, in particular it
68
does not pay attention to VP8 partion boundaries (see RFC 7741 sec. 4.4).
79
"""
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
defmodule ExWebRTC.RTP.VP8DepayloaderTest do
2+
use ExUnit.Case, async: true
3+
4+
alias ExWebRTC.RTP.{VP8Payload, VP8Depayloader}
5+
6+
test "write/2" do
7+
depayloader = VP8Depayloader.new()
8+
# random vp8 data, not necessarily correct
9+
data = <<0, 1, 2, 3>>
10+
11+
# packet with entire frame
12+
vp8_payload = %VP8Payload{n: 0, s: 1, pid: 0, payload: data}
13+
vp8_payload = VP8Payload.serialize(vp8_payload)
14+
15+
packet = ExRTP.Packet.new(vp8_payload, 0, 0, 0, 0, marker: true)
16+
17+
assert {:ok, ^data, %{current_frame: nil, current_timestamp: nil} = depayloader} =
18+
VP8Depayloader.write(depayloader, packet)
19+
20+
# packet that doesn't start a new frame
21+
vp8_payload = %VP8Payload{n: 0, s: 0, pid: 0, payload: data}
22+
vp8_payload = VP8Payload.serialize(vp8_payload)
23+
24+
packet = ExRTP.Packet.new(vp8_payload, 0, 0, 0, 0)
25+
26+
assert {:ok, %{current_frame: nil, current_timestamp: nil} = depayloader} =
27+
VP8Depayloader.write(depayloader, packet)
28+
29+
# packet that starts a new frame without finishing the previous one
30+
vp8_payload = %VP8Payload{n: 0, s: 1, pid: 0, payload: data}
31+
vp8_payload = VP8Payload.serialize(vp8_payload)
32+
33+
packet = ExRTP.Packet.new(vp8_payload, 0, 0, 0, 0)
34+
35+
assert {:ok, %{current_frame: ^data, current_timestamp: 0} = depayloader} =
36+
VP8Depayloader.write(depayloader, packet)
37+
38+
data2 = data <> <<0>>
39+
vp8_payload = %VP8Payload{n: 0, s: 1, pid: 0, payload: data2}
40+
vp8_payload = VP8Payload.serialize(vp8_payload)
41+
42+
packet = ExRTP.Packet.new(vp8_payload, 0, 0, 3000, 0)
43+
44+
assert {:ok, %{current_frame: ^data2, current_timestamp: 3000} = depayloader} =
45+
VP8Depayloader.write(depayloader, packet)
46+
47+
# packet with timestamp from a new frame that is not a beginning of this frame
48+
data2 = data
49+
vp8_payload = %VP8Payload{n: 0, s: 0, pid: 0, payload: data2}
50+
vp8_payload = VP8Payload.serialize(vp8_payload)
51+
52+
packet = ExRTP.Packet.new(vp8_payload, 0, 0, 6000, 0)
53+
54+
assert {:ok, %{current_frame: nil, current_timestamp: nil}} =
55+
VP8Depayloader.write(depayloader, packet)
56+
end
57+
end
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
defmodule ExWebrtc.Rtp.Vp8PayloadTest do
2+
use ExUnit.Case, async: true
3+
4+
alias ExWebRTC.RTP.VP8Payload
5+
6+
test "parse/1 and serialize/1" do
7+
# test vectors are based on RFC 7741, sec. 4.6
8+
9+
# random vp8 data, not necessarily correct
10+
vp8_payload = <<0, 1, 2, 3>>
11+
12+
# X=1, S=1, PID=0, I=1, pciture_id=17
13+
frame =
14+
<<1::1, 0::1, 0::1, 1::1, 0::1, 0::3, 1::1, 0::7, 0::1, 17::7, vp8_payload::binary>>
15+
16+
parsed_frame =
17+
%VP8Payload{
18+
n: 0,
19+
s: 1,
20+
pid: 0,
21+
picture_id: 17,
22+
tl0picidx: nil,
23+
tid: nil,
24+
y: nil,
25+
keyidx: nil,
26+
payload: vp8_payload
27+
}
28+
29+
assert {:ok, parsed_frame} == VP8Payload.parse(frame)
30+
assert frame == VP8Payload.serialize(parsed_frame)
31+
32+
# X=0, S=1, PID=0
33+
frame = <<0::1, 0::1, 0::1, 1::1, 0::1, 0::3, vp8_payload::binary>>
34+
35+
parsed_frame = %VP8Payload{
36+
n: 0,
37+
s: 1,
38+
pid: 0,
39+
picture_id: nil,
40+
tl0picidx: nil,
41+
tid: nil,
42+
y: nil,
43+
keyidx: nil,
44+
payload: vp8_payload
45+
}
46+
47+
assert {:ok, parsed_frame} == VP8Payload.parse(frame)
48+
assert frame == VP8Payload.serialize(parsed_frame)
49+
50+
# X=1, S=1, I=1, L=1, T=1, K=1, M=1, picture_id=4711
51+
frame =
52+
<<1::1, 0::1, 0::1, 1::1, 0::1, 0::3, 1::1, 1::1, 1::1, 1::1, 0::4, 1::1, 4711::15, 1::8,
53+
1::2, 1::1, 1::5, vp8_payload::binary>>
54+
55+
parsed_frame = %VP8Payload{
56+
n: 0,
57+
s: 1,
58+
pid: 0,
59+
picture_id: 4711,
60+
tl0picidx: 1,
61+
tid: 1,
62+
y: 1,
63+
keyidx: 1,
64+
payload: vp8_payload
65+
}
66+
67+
assert {:ok, parsed_frame} == VP8Payload.parse(frame)
68+
assert frame == VP8Payload.serialize(parsed_frame)
69+
70+
assert {:error, :invalid_packet} = VP8Payload.parse(<<>>)
71+
72+
# X=0 and no vp8_payload
73+
assert {:error, :invalid_packet} =
74+
VP8Payload.parse(<<0::1, 0::1, 0::1, 1::1, 0::1, 0::3>>)
75+
76+
# X=1, I=1 picture_id=1 and no vp8_payload
77+
frame = <<1::1, 0::1, 0::1, 1::1, 0::1, 0::3, 1::1, 0::7, 0::1, 1::7>>
78+
assert {:error, :invalid_packet} = VP8Payload.parse(frame)
79+
80+
# invalid reserved bit
81+
assert {:error, :invalid_packet} =
82+
VP8Payload.parse(<<0::1, 1::1, 0::1, 1::1, 1::1, 0::3>>)
83+
84+
# missing picture id
85+
missing_picture_id = <<1::1, 0::1, 0::1, 1::1, 0::1, 0::3, 1::1, 0::7>>
86+
assert {:error, :invalid_packet} = VP8Payload.parse(missing_picture_id)
87+
88+
# missing tl0picidx
89+
missing_tl0picidx = <<1::1, 0::1, 0::1, 1::1, 0::1, 0::3, 0::1, 1::1, 0::6>>
90+
assert {:error, :invalid_packet} = VP8Payload.parse(missing_tl0picidx)
91+
92+
# missing tidykeyidx
93+
missing_tidykeyidx = <<1::1, 0::1, 0::1, 1::1, 0::1, 0::3, 0::2, 1::1, 0::1, 0::4>>
94+
assert {:error, :invalid_packet} = VP8Payload.parse(missing_tidykeyidx)
95+
end
96+
end

0 commit comments

Comments
 (0)