Skip to content

Commit 580b802

Browse files
committed
Add VP8Depayloader
1 parent d0b2778 commit 580b802

File tree

4 files changed

+370
-0
lines changed

4 files changed

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

lib/ex_webrtc/rtp/vp8_payload.ex

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

0 commit comments

Comments
 (0)