Skip to content

Commit 7f1a127

Browse files
authored
Add support for VP8 simulcast (#198)
1 parent 4cebfd8 commit 7f1a127

File tree

6 files changed

+400
-16
lines changed

6 files changed

+400
-16
lines changed

guides/advanced/simulcast.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ This process is known as munging.
8686
alias ExWebRTC.PeerConnection
8787
alias ExWebRTC.RTP.{H264, Munger}
8888

89-
m = Munger.new(90_000)
89+
m = Munger.new(:h264, 90_000)
9090
```
9191

9292
2. When a packet from an encoding that we want to forward arrives, rewrite its sequnce number and timestamp:

lib/ex_webrtc/rtp/munger.ex

+50-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule ExWebRTC.RTP.Munger do
22
@moduledoc """
33
RTP Munger allows for converting RTP packet timestamps and sequence numbers
44
to a common domain.
5+
It also rewrites parts of the RTP payload that may require a similar behaviour.
56
67
This is useful when e.g. changing between Simulcast layers - the sender sends
78
three separate RTP streams (also called layers or encodings), but the receiver can receive only a
@@ -12,7 +13,7 @@ defmodule ExWebRTC.RTP.Munger do
1213
# and this is a GenServer
1314
1415
def init() do
15-
{:ok, %{munger: Munger.new(90_000), layer: "h"}}
16+
{:ok, %{munger: Munger.new(:h264, 90_000), layer: "h"}}
1617
end
1718
1819
def handle_info({:ex_webrtc, _from, {:rtp, _id, rid, packet}}, state) do
@@ -34,6 +35,8 @@ defmodule ExWebRTC.RTP.Munger do
3435
"""
3536

3637
alias ExRTP.Packet
38+
alias ExWebRTC.RTPCodecParameters
39+
alias ExWebRTC.RTP.VP8
3740

3841
@max_rtp_ts 0xFFFFFFFF
3942
@max_rtp_sn 0xFFFF
@@ -47,14 +50,16 @@ defmodule ExWebRTC.RTP.Munger do
4750
# * `sn_offset` - offset for sequence numbers
4851
# * `ts_offset` - offset for timestamps
4952
# * `update?` - flag telling if the next munged packets belongs to a new encoding
53+
# * `vp8_munger` - VP8 munger, only used when RTP packets contain VP8 codec
5054
@opaque t() :: %__MODULE__{
5155
clock_rate: non_neg_integer(),
5256
rtp_sn: non_neg_integer() | nil,
5357
rtp_ts: non_neg_integer() | nil,
5458
wc_ts: integer() | nil,
5559
sn_offset: integer(),
5660
ts_offset: integer(),
57-
update?: boolean()
61+
update?: boolean(),
62+
vp8_munger: VP8.Munger.t() | nil
5863
}
5964

6065
@enforce_keys [:clock_rate]
@@ -64,19 +69,31 @@ defmodule ExWebRTC.RTP.Munger do
6469
:wc_ts,
6570
sn_offset: 0,
6671
ts_offset: 0,
67-
update?: false
72+
update?: false,
73+
vp8_munger: nil
6874
] ++ @enforce_keys
6975

7076
@doc """
7177
Creates a new `t:ExWebRTC.RTP.Munger.t/0`.
7278
7379
`clock_rate` is the clock rate of the codec carried in munged RTP packets.
7480
"""
75-
@spec new(non_neg_integer()) :: t()
76-
def new(clock_rate) do
81+
@spec new(:h264 | :vp8 | RTPCodecParameters.t(), non_neg_integer()) :: t()
82+
def new(:h264, clock_rate) do
7783
%__MODULE__{clock_rate: clock_rate}
7884
end
7985

86+
def new(:vp8, clock_rate) do
87+
%__MODULE__{clock_rate: clock_rate, vp8_munger: VP8.Munger.new()}
88+
end
89+
90+
def new(%RTPCodecParameters{} = codec_params) do
91+
case codec_params.mime_type do
92+
"video/H264" -> new(:h264, codec_params.clock_rate)
93+
"video/VP8" -> new(:vp8, codec_params.clock_rate)
94+
end
95+
end
96+
8097
@doc """
8198
Informs the munger that the next packet passed to `munge/2` will come
8299
from a different RTP stream.
@@ -90,17 +107,30 @@ defmodule ExWebRTC.RTP.Munger do
90107
@spec munge(t(), Packet.t()) :: {Packet.t(), t()}
91108
def munge(%{rtp_sn: nil} = munger, packet) do
92109
# first packet ever munged
110+
vp8_munger = munger.vp8_munger && VP8.Munger.init(munger.vp8_munger, packet.payload)
111+
93112
munger = %__MODULE__{
94113
munger
95114
| rtp_sn: packet.sequence_number,
96115
rtp_ts: packet.timestamp,
97-
wc_ts: get_wc_ts(packet)
116+
wc_ts: get_wc_ts(packet),
117+
vp8_munger: vp8_munger
98118
}
99119

100120
{packet, munger}
101121
end
102122

103123
def munge(munger, packet) when munger.update? do
124+
{vp8_munger, rtp_payload} =
125+
if munger.vp8_munger do
126+
vp8_munger = VP8.Munger.update(munger.vp8_munger, packet.payload)
127+
VP8.Munger.munge(vp8_munger, packet.payload)
128+
else
129+
{munger.vp8_munger, packet.payload}
130+
end
131+
132+
packet = %ExRTP.Packet{packet | payload: rtp_payload}
133+
104134
wc_ts = get_wc_ts(packet)
105135

106136
native_in_sec = System.convert_time_unit(1, :second, :native)
@@ -124,7 +154,8 @@ defmodule ExWebRTC.RTP.Munger do
124154
| rtp_sn: new_packet.sequence_number,
125155
rtp_ts: new_packet.timestamp,
126156
wc_ts: wc_ts,
127-
update?: false
157+
update?: false,
158+
vp8_munger: vp8_munger
128159
}
129160

130161
{new_packet, munger}
@@ -135,6 +166,15 @@ defmodule ExWebRTC.RTP.Munger do
135166
# the first packet after the encoding update
136167
# as these might conflict with packets from the previous layer
137168
# and we should change on a keyframe anyways
169+
{vp8_munger, rtp_payload} =
170+
if munger.vp8_munger do
171+
VP8.Munger.munge(munger.vp8_munger, packet.payload)
172+
else
173+
{munger.vp8_munger, packet.payload}
174+
end
175+
176+
packet = %ExRTP.Packet{packet | payload: rtp_payload}
177+
138178
wc_ts = get_wc_ts(packet)
139179

140180
new_packet = adjust_packet(munger, packet)
@@ -148,10 +188,11 @@ defmodule ExWebRTC.RTP.Munger do
148188
munger
149189
| rtp_sn: new_packet.sequence_number,
150190
rtp_ts: new_packet.timestamp,
151-
wc_ts: wc_ts
191+
wc_ts: wc_ts,
192+
vp8_munger: vp8_munger
152193
}
153194
else
154-
munger
195+
%__MODULE__{munger | vp8_munger: vp8_munger}
155196
end
156197

157198
{new_packet, munger}

lib/ex_webrtc/rtp/vp8.ex

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
defmodule ExWebRTC.RTP.VP8 do
2+
@moduledoc """
3+
Utilities for RTP packets carrying VP8 encoded payload.
4+
"""
5+
6+
alias ExRTP.Packet
7+
alias ExWebRTC.RTP.VP8
8+
9+
@doc """
10+
Checks whether RTP payload contains VP8 keyframe.
11+
"""
12+
@spec keyframe?(Packet.t()) :: boolean()
13+
def keyframe?(%Packet{payload: rtp_payload}) do
14+
# RTP payload contains VP8 keyframe when P bit in VP8 payload header is set to 0
15+
# besides this S bit (start of VP8 partition) and PID (partition index)
16+
# have to be 1 and 0 respectively
17+
# for more information refer to RFC 7741 Sections 4.2 and 4.3
18+
19+
with {:ok, vp8_payload} <- VP8.Payload.parse(rtp_payload),
20+
<<_size0::3, _h::1, _ver::3, p::1, _size1::8, _size2::8, _rest::binary>> <- rtp_payload do
21+
vp8_payload.s == 1 and vp8_payload.pid == 0 and p == 0
22+
else
23+
_err -> false
24+
end
25+
end
26+
end

lib/ex_webrtc/rtp/vp8/munger.ex

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
defmodule ExWebRTC.RTP.VP8.Munger do
2+
@moduledoc false
3+
# Module responsible for rewriting VP8 RTP payload fields
4+
# to provide transparent switch between simulcast encodings.
5+
import Bitwise
6+
7+
alias ExWebRTC.RTP.VP8
8+
9+
@type t() :: %__MODULE__{
10+
pic_id_used: boolean(),
11+
last_pic_id: integer(),
12+
pic_id_offset: integer(),
13+
tl0picidx_used: boolean(),
14+
last_tl0picidx: integer(),
15+
tl0picidx_offset: integer(),
16+
keyidx_used: boolean(),
17+
last_keyidx: integer(),
18+
keyidx_offset: integer()
19+
}
20+
21+
defstruct pic_id_used: false,
22+
last_pic_id: 0,
23+
pic_id_offset: 0,
24+
tl0picidx_used: false,
25+
last_tl0picidx: 0,
26+
tl0picidx_offset: 0,
27+
keyidx_used: false,
28+
last_keyidx: 0,
29+
keyidx_offset: 0
30+
31+
@spec new() :: t()
32+
def new() do
33+
%__MODULE__{}
34+
end
35+
36+
@spec init(t(), binary()) :: t()
37+
def init(vp8_munger, rtp_payload) do
38+
{:ok, vp8_payload} = VP8.Payload.parse(rtp_payload)
39+
40+
last_pic_id = vp8_payload.picture_id || 0
41+
last_tl0picidx = vp8_payload.tl0picidx || 0
42+
last_keyidx = vp8_payload.keyidx || 0
43+
44+
%__MODULE__{
45+
vp8_munger
46+
| pic_id_used: vp8_payload.picture_id != nil,
47+
last_pic_id: last_pic_id,
48+
tl0picidx_used: vp8_payload.tl0picidx != nil,
49+
last_tl0picidx: last_tl0picidx,
50+
keyidx_used: vp8_payload.keyidx != nil,
51+
last_keyidx: last_keyidx
52+
}
53+
end
54+
55+
@spec update(t(), binary()) :: t()
56+
def update(vp8_munger, rtp_payload) do
57+
{:ok, vp8_payload} = VP8.Payload.parse(rtp_payload)
58+
59+
%VP8.Payload{
60+
keyidx: keyidx,
61+
picture_id: pic_id,
62+
tl0picidx: tl0picidx
63+
} = vp8_payload
64+
65+
pic_id_offset = (vp8_munger.pic_id_used && pic_id - vp8_munger.last_pic_id - 1) || 0
66+
67+
tl0picidx_offset =
68+
(vp8_munger.tl0picidx_used && tl0picidx - vp8_munger.last_tl0picidx - 1) || 0
69+
70+
keyidx_offset = (vp8_munger.keyidx_used && keyidx - vp8_munger.last_keyidx - 1) || 0
71+
72+
%__MODULE__{
73+
vp8_munger
74+
| pic_id_offset: pic_id_offset,
75+
tl0picidx_offset: tl0picidx_offset,
76+
keyidx_offset: keyidx_offset
77+
}
78+
end
79+
80+
@spec munge(t(), binary()) :: {t(), binary()}
81+
def munge(vp8_munger, <<>> = rtp_payload), do: {vp8_munger, rtp_payload}
82+
83+
def munge(vp8_munger, rtp_payload) do
84+
{:ok, vp8_payload} = VP8.Payload.parse(rtp_payload)
85+
86+
%VP8.Payload{
87+
keyidx: keyidx,
88+
picture_id: pic_id,
89+
tl0picidx: tl0picidx
90+
} = vp8_payload
91+
92+
munged_pic_id = pic_id && rem(pic_id + (1 <<< 15) - vp8_munger.pic_id_offset, 1 <<< 15)
93+
94+
munged_tl0picidx =
95+
tl0picidx && rem(tl0picidx + (1 <<< 8) - vp8_munger.tl0picidx_offset, 1 <<< 8)
96+
97+
munged_keyidx = keyidx && rem(keyidx + (1 <<< 5) - vp8_munger.keyidx_offset, 1 <<< 5)
98+
99+
vp8_payload =
100+
%VP8.Payload{
101+
vp8_payload
102+
| keyidx: munged_keyidx,
103+
picture_id: munged_pic_id,
104+
tl0picidx: munged_tl0picidx
105+
}
106+
|> VP8.Payload.serialize()
107+
108+
vp8_munger = %__MODULE__{
109+
vp8_munger
110+
| last_pic_id: munged_pic_id,
111+
last_tl0picidx: munged_tl0picidx,
112+
last_keyidx: munged_keyidx
113+
}
114+
115+
{vp8_munger, vp8_payload}
116+
end
117+
end

test/ex_webrtc/rtp/munger_test.exs

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule ExWebRTC.RTP.MungerTest do
1111
@packet Packet.new(<<0::128*8>>)
1212

1313
test "assigns sequence numbers properly" do
14-
munger = Munger.new(@clock_rate)
14+
munger = Munger.new(:h264, @clock_rate)
1515

1616
l1_packet = %{@packet | sequence_number: 100}
1717
{^l1_packet, munger} = Munger.munge(munger, l1_packet)
@@ -45,7 +45,7 @@ defmodule ExWebRTC.RTP.MungerTest do
4545
end
4646

4747
test "handles input sequence number rollover" do
48-
munger = Munger.new(@clock_rate)
48+
munger = Munger.new(:h264, @clock_rate)
4949

5050
l1_packet = %{@packet | sequence_number: 100}
5151
{^l1_packet, munger} = Munger.munge(munger, l1_packet)
@@ -77,7 +77,7 @@ defmodule ExWebRTC.RTP.MungerTest do
7777
end
7878

7979
test "handles output sequence number rollover" do
80-
munger = Munger.new(@clock_rate)
80+
munger = Munger.new(:h264, @clock_rate)
8181

8282
l1_packet = %{@packet | sequence_number: @max_sn - 2}
8383
{^l1_packet, munger} = Munger.munge(munger, l1_packet)
@@ -98,7 +98,7 @@ defmodule ExWebRTC.RTP.MungerTest do
9898
end
9999

100100
test "assigns timestamps properly" do
101-
munger = Munger.new(@clock_rate)
101+
munger = Munger.new(:h264, @clock_rate)
102102

103103
l1_packet = %{@packet | sequence_number: 100, timestamp: 5000}
104104
{^l1_packet, munger} = Munger.munge(munger, l1_packet)
@@ -134,7 +134,7 @@ defmodule ExWebRTC.RTP.MungerTest do
134134
end
135135

136136
test "handles input timestamp rollover" do
137-
munger = Munger.new(@clock_rate)
137+
munger = Munger.new(:h264, @clock_rate)
138138

139139
l1_packet = %{@packet | sequence_number: 100, timestamp: 5000}
140140
{^l1_packet, munger} = Munger.munge(munger, l1_packet)
@@ -152,7 +152,7 @@ defmodule ExWebRTC.RTP.MungerTest do
152152
end
153153

154154
test "handles output timestamp rollover" do
155-
munger = Munger.new(@clock_rate)
155+
munger = Munger.new(:h264, @clock_rate)
156156

157157
l1_packet = %{@packet | sequence_number: 100, timestamp: @max_ts}
158158
{^l1_packet, munger} = Munger.munge(munger, l1_packet)

0 commit comments

Comments
 (0)