Skip to content

Commit 069e8c6

Browse files
authored
Rework RTP API (#58)
* Flatten RTP API * Support more values in the CLI, add more docs * Add IP parsing
1 parent 55c2e3b commit 069e8c6

File tree

8 files changed

+246
-141
lines changed

8 files changed

+246
-141
lines changed

README.md

+21-2
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,35 @@ Make sure you have [Elixir](https://elixir-lang.org/) installed. The first call
100100

101101
## CLI
102102

103-
The CLI API is similar to the Elixir API, for example:
103+
The CLI API is a direct mapping of the Elixir API:
104+
* `:input` and `:output` options of `Boombox.run/2` are mapped to `-i` and `-o` CLI arguments respectively.
105+
* Option names, like `:some_option`, are mapped to CLI arguments by removing the colon, adding a leading double hyphen and replacing all underscores with hyphens, like `--some-option`.
106+
* Option values mappings depend on the option's type:
107+
- String values, like `"some_value"`, are mapped to CLI arguments by stripping the quotes, like `some_value`.
108+
- Atom values, like `:some_value`, are mapped to CLI arguments by stripping the leading colon, like `some_value`.
109+
- Integer values are identical in elixir and CLI.
110+
- Binary values, like `<<161, 63>>`, are represented in CLI as their representation in base 16, like `a13f`.
111+
112+
For example:
104113

105114
```elixir
106115
Boombox.run(input: "file.mp4", output: {:webrtc, "ws://localhost:8830"})
116+
Boombox.run(
117+
input:
118+
{:rtp,
119+
port: 50001,
120+
audio_encoding: :AAC,
121+
audio_specific_config: <<161, 63>>,
122+
aac_bitrate_mode: :hbr},
123+
output: "index.m3u8"
124+
)
107125
```
108126

109-
is equivalent to:
127+
are equivalent to:
110128

111129
```sh
112130
./boombox -i file.mp4 -o --webrtc ws://localhost:8830
131+
./boombox -i --rtp --port 50001 --audio-encoding AAC --audio-specific-config a13f --aac-bitrate-mode hbr -o index.m3u8
113132
```
114133

115134
It's also possible to pass an `.exs` script:

examples.livemd

+1-1
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ rtp_port = 50001
542542
t =
543543
Task.async(fn ->
544544
Boombox.run(
545-
input: {:rtp, port: rtp_port, track_configs: [audio: [encoding: :OPUS], video: [encoding: :H264]]},
545+
input: {:rtp, port: rtp_port, audio_encoding: :OPUS, video_encoding: :H264},
546546
output: "#{out_dir}/index.m3u8"
547547
)
548548
end)

lib/boombox.ex

+29-36
Original file line numberDiff line numberDiff line change
@@ -22,54 +22,47 @@ defmodule Boombox do
2222
]
2323

2424
@typedoc """
25-
Some encodings can/must be accompanied with encoding specific parameters:
26-
* AAC:
27-
- bitrate_mode - MUST be provided for both RTP input and output. Defines which mode should be assumed/set when depayloading/payloading.
28-
- audio_specific_config - MUST be provided for RTP input. Contains crucial information about the stream and has to be obtained from a side channel.
29-
* H264 and H265:
30-
- vpss (H265 only), ppss, spss - MAY be provided for RTP input. picture and sequence parameter sets, could be obtained from a side channel. They contain information about the encoded stream.
25+
When configuring a track for a media type (video or audio), the following options are used:
26+
* <media_type>_encoding - MUST be provided to configure given media type. Some options are encoding-specific. Currently supported encodings are: AAC, Opus, H264, H265.
27+
* <media_type>_payload_type, <media_type>_clock rate - MAY be provided. If not, an unofficial default will be used.
28+
The following encoding-specific parameters are available for both RTP input and output:
29+
* aac_bitrate_mode - MUST be provided for AAC encoding. Defines which mode should be assumed/set when depayloading/payloading.
3130
"""
32-
@type rtp_encoding_specific_params ::
33-
{:AAC, [{:bitrate_mode, RTP.AAC.Utils.mode()} | {:audio_specific_config, binary()}]}
34-
| {:H264, [{:ppss, [binary()]} | {:spss, [binary()]}]}
35-
| {:H265, [{:vpss, [binary()]} | {:ppss, [binary()]} | {:spss, [binary()]}]}
31+
@type common_rtp_opt ::
32+
{:video_encoding, RTP.encoding_name()}
33+
| {:video_payload_type, RTP.payload_type()}
34+
| {:video_clock_rate, RTP.clock_rate()}
35+
| {:audio_encoding, RTP.encoding_name()}
36+
| {:audio_payload_type, RTP.payload_type()}
37+
| {:audio_clock_rate, RTP.clock_rate()}
38+
| {:aac_bitrate_mode, RTP.AAC.Utils.mode()}
3639

3740
@typedoc """
38-
For each media type the following parameters are specified:
39-
* encoding - MUST be provided for both RTP input and output, some encodings require additional parameters, see `rtp_encoding_specific_params/0`.
40-
* payload_type, clock rate - MAY be provided for both RTP input and output, if not, then an unofficial default will be used.
41-
"""
42-
@type rtp_track_config :: [
43-
{:encoding, RTP.encoding_name() | rtp_encoding_specific_params()}
44-
| {:payload_type, RTP.payload_type()}
45-
| {:clock_rate, RTP.clock_rate()}
46-
]
41+
In order to configure a RTP input a receiving port MUST be provided and the media that will be received
42+
MUST be configured. Media configuration is explained further in `t:common_rtp_opt/0`.
4743
48-
@typedoc """
49-
In order to configure RTP input both a receiving port and media configurations must be provided.
50-
At least one media type needs to be configured.
44+
The following encoding-specific parameters are available for RTP input:
45+
* audio_specific_config - MUST be provided for AAC encoding. Contains crucial information about the stream and has to be obtained from a side channel.
46+
* vps (H265 only), pps, sps - MAY be provided for H264 or H265 encodings. Parameter sets, could be obtained from a side channel. They contain information about the encoded stream.
5147
"""
5248
@type in_rtp_opts :: [
53-
{:port, :inet.port_number()}
54-
| {:track_configs,
55-
[
56-
{:audio, rtp_track_config()}
57-
| {:video, rtp_track_config()}
58-
]}
49+
common_rtp_opt()
50+
| {:port, :inet.port_number()}
51+
| {:audio_specific_config, binary()}
52+
| {:vps, binary()}
53+
| {:pps, binary()}
54+
| {:sps, binary()}
5955
]
6056

6157
@typedoc """
62-
In order to configure RTP output the destination and media configurations must be provided.
63-
At least one media type needs to be configured.
58+
In order to configure a RTP output the target port and address MUST be provided (can be provided in `:target` option as a `<address>:<port>` string)
59+
and the media that will be sent MUST be configured. Media configuration is explained further in `t:common_rtp_opt/0`.
6460
"""
6561
@type out_rtp_opts :: [
66-
{:address, :inet.ip_address()}
62+
common_rtp_opt()
63+
| {:address, :inet.ip_address() | String.t()}
6764
| {:port, :inet.port_number()}
68-
| {:track_configs,
69-
[
70-
{:audio, rtp_track_config()}
71-
| {:video, rtp_track_config()}
72-
]}
65+
| {:target, String.t()}
7366
]
7467

7568
@type input ::

lib/boombox/rtp.ex

+92-68
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,31 @@ defmodule Boombox.RTP do
77
alias Boombox.Pipeline.Ready
88
alias Membrane.RTP
99

10-
@required_opts %{
11-
input: [:port, :track_configs],
12-
output: [:address, :port, :track_configs]
13-
}
10+
@supported_encodings [audio: [:AAC, :Opus], video: [:H264, :H265]]
1411

15-
@required_encoding_specific_params %{
12+
@encoding_specific_params_specs %{
1613
input: %{
17-
AAC: [bitrate_mode: [require?: true], audio_specific_config: [require?: true]],
18-
H264: [ppss: [require?: false, default: []], spss: [require?: false, default: []]],
14+
AAC: [aac_bitrate_mode: [require?: true], audio_specific_config: [require?: true]],
15+
H264: [pps: [require?: false, default: nil], sps: [require?: false, default: nil]],
1916
H265: [
20-
vpss: [require?: false, default: []],
21-
ppss: [require?: false, default: []],
22-
spss: [require?: false, default: []]
17+
vps: [require?: false, default: nil],
18+
pps: [require?: false, default: nil],
19+
sps: [require?: false, default: nil]
2320
]
2421
},
2522
output: %{
26-
AAC: [bitrate_mode: [require?: true]]
23+
AAC: [aac_bitrate_mode: [require?: true]]
2724
}
2825
}
2926

3027
@type parsed_input_encoding_specific_params ::
31-
%{bitrate_mode: RTP.AAC.Utils.mode(), audio_specific_config: binary()}
32-
| %{:ppss => [binary()], :spss => [binary()]}
33-
| %{:vpss => [binary()], :ppss => [binary()], :spss => [binary()]}
28+
%{aac_bitrate_mode: RTP.AAC.Utils.mode(), audio_specific_config: binary()}
29+
| %{vps: binary() | nil, pps: binary() | nil, sps: binary() | nil}
30+
| %{pps: binary() | nil, sps: binary() | nil}
3431
| %{}
3532

3633
@type parsed_output_encoding_specific_params ::
37-
%{bitrate_mode: RTP.AAC.Utils.mode()}
34+
%{aac_bitrate_mode: RTP.AAC.Utils.mode()}
3835
| %{}
3936

4037
@type parsed_input_track_config :: %{
@@ -84,13 +81,13 @@ defmodule Boombox.RTP do
8481
{depayloader, parser} =
8582
case track_config.encoding_name do
8683
:H264 ->
87-
ppss = track_config.encoding_specific_params.ppss
88-
spss = track_config.encoding_specific_params.spss
84+
ppss = List.wrap(track_config.encoding_specific_params.pps)
85+
spss = List.wrap(track_config.encoding_specific_params.sps)
8986
{Membrane.RTP.H264.Depayloader, %Membrane.H264.Parser{ppss: ppss, spss: spss}}
9087

9188
:AAC ->
9289
audio_specific_config = track_config.encoding_specific_params.audio_specific_config
93-
bitrate_mode = track_config.encoding_specific_params.bitrate_mode
90+
bitrate_mode = track_config.encoding_specific_params.aac_bitrate_mode
9491

9592
{%Membrane.RTP.AAC.Depayloader{mode: bitrate_mode},
9693
%Membrane.AAC.Parser{audio_specific_config: audio_specific_config}}
@@ -99,9 +96,9 @@ defmodule Boombox.RTP do
9996
{Membrane.RTP.Opus.Depayloader, Membrane.Opus.Parser}
10097

10198
:H265 ->
102-
vpss = Map.get(track_config.encoding_specific_params, :vpss, [])
103-
ppss = Map.get(track_config.encoding_specific_params, :ppss, [])
104-
spss = Map.get(track_config.encoding_specific_params, :spss, [])
99+
vpss = List.wrap(track_config.encoding_specific_params.vps)
100+
ppss = List.wrap(track_config.encoding_specific_params.pps)
101+
spss = List.wrap(track_config.encoding_specific_params.sps)
105102

106103
{Membrane.RTP.H265.Depayloader,
107104
%Membrane.H265.Parser{vpss: vpss, ppss: ppss, spss: spss}}
@@ -151,7 +148,7 @@ defmodule Boombox.RTP do
151148
{%Membrane.AAC{encapsulation: :none},
152149
%Membrane.AAC.Parser{out_encapsulation: :none},
153150
%Membrane.RTP.AAC.Payloader{
154-
mode: track_config.encoding_specific_params.bitrate_mode,
151+
mode: track_config.encoding_specific_params.aac_bitrate_mode,
155152
frames_per_packet: 1
156153
}}
157154

@@ -189,42 +186,85 @@ defmodule Boombox.RTP do
189186
@spec validate_and_parse_options(:input, Boombox.in_rtp_opts()) :: parsed_in_opts()
190187
@spec validate_and_parse_options(:output, Boombox.out_rtp_opts()) :: parsed_out_opts()
191188
defp validate_and_parse_options(direction, opts) do
192-
Enum.each(@required_opts[direction], fn required_option ->
193-
unless Keyword.has_key?(opts, required_option) do
194-
raise "Required option #{inspect(required_option)} not present in passed RTP options"
195-
end
196-
end)
189+
transport_opts =
190+
case direction do
191+
:input ->
192+
if Keyword.has_key?(opts, :port) do
193+
%{port: opts[:port]}
194+
else
195+
raise "Receiving port not specified in RTP options"
196+
end
197197

198-
if opts[:track_configs] == [] do
199-
raise "No RTP media configured"
200-
end
198+
:output ->
199+
parse_output_target(opts)
200+
end
201201

202202
parsed_track_configs =
203-
Map.new(opts[:track_configs], fn {media_type, track_config} ->
204-
{media_type, validate_and_parse_track_config!(direction, track_config)}
203+
[:audio, :video]
204+
|> Enum.filter(fn
205+
:video -> opts[:video_encoding] != nil
206+
:audio -> opts[:audio_encoding] != nil
207+
end)
208+
|> Map.new(fn media_type ->
209+
{media_type, validate_and_parse_track_config(direction, media_type, opts)}
205210
end)
206211

207-
case direction do
208-
:input ->
209-
%{port: opts[:port], track_configs: parsed_track_configs}
212+
if parsed_track_configs == %{} do
213+
raise "No RTP media configured"
214+
end
215+
216+
Map.put(transport_opts, :track_configs, parsed_track_configs)
217+
end
210218

211-
:output ->
212-
%{address: opts[:address], port: opts[:port], track_configs: parsed_track_configs}
219+
@spec parse_output_target(
220+
target: String.t(),
221+
address: :inet.ip4_address() | String.t(),
222+
port: :inet.port_number()
223+
) :: %{address: :inet.ip4_address(), port: :inet.port_number()}
224+
defp parse_output_target(opts) do
225+
case Map.new(opts) do
226+
%{target: target} ->
227+
[address_string, port_string] = String.split(target, ":")
228+
{:ok, address} = :inet.parse_ipv4_address(String.to_charlist(address_string))
229+
%{address: address, port: String.to_integer(port_string)}
230+
231+
%{address: address, port: port} when is_binary(address) ->
232+
{:ok, address} = :inet.parse_ipv4_address(String.to_charlist(address))
233+
%{address: address, port: port}
234+
235+
%{address: address, port: port} when is_tuple(address) ->
236+
%{address: address, port: port}
237+
238+
_invalid_target ->
239+
raise "RTP output target address and port not specified"
213240
end
214241
end
215242

216-
@spec validate_and_parse_track_config!(:input, Boombox.rtp_track_config()) ::
243+
@spec validate_and_parse_track_config(:input, :video | :audio, Boombox.in_rtp_opts()) ::
217244
parsed_input_track_config()
218-
@spec validate_and_parse_track_config!(:output, Boombox.rtp_track_config()) ::
245+
@spec validate_and_parse_track_config(:output, :video | :audio, Boombox.out_rtp_opts()) ::
219246
parsed_output_track_config()
220-
defp validate_and_parse_track_config!(direction, track_config) do
221-
{encoding_name, encoding_specific_params} =
222-
validate_and_parse_encoding!(direction, track_config[:encoding])
247+
defp validate_and_parse_track_config(direction, media_type, opts) do
248+
{encoding_name, payload_type, clock_rate} =
249+
case media_type do
250+
:audio -> {opts[:audio_encoding], opts[:audio_payload_type], opts[:audio_clock_rate]}
251+
:video -> {opts[:video_encoding], opts[:video_payload_type], opts[:video_clock_rate]}
252+
end
223253

224-
track_config = Keyword.put(track_config, :encoding_name, encoding_name)
254+
if encoding_name not in @supported_encodings[media_type] do
255+
raise "Encoding #{inspect(encoding_name)} for #{inspect(media_type)} media type not supported"
256+
end
225257

226-
%{payload_type: payload_type, clock_rate: clock_rate} =
227-
RTP.PayloadFormat.resolve(track_config)
258+
%{
259+
payload_format: %RTP.PayloadFormat{encoding_name: encoding_name},
260+
payload_type: payload_type,
261+
clock_rate: clock_rate
262+
} =
263+
RTP.PayloadFormat.resolve(
264+
encoding_name: encoding_name,
265+
payload_type: payload_type,
266+
clock_rate: clock_rate
267+
)
228268

229269
if payload_type == nil do
230270
raise "payload_type for encoding #{inspect(encoding_name)} not provided with no default value registered"
@@ -234,6 +274,13 @@ defmodule Boombox.RTP do
234274
raise "clock_rate for encoding #{inspect(encoding_name)} and payload_type #{inspect(payload_type)} not provided with no default value registered"
235275
end
236276

277+
encoding_specific_params_specs =
278+
Map.get(@encoding_specific_params_specs[direction], encoding_name, [])
279+
280+
{:ok, encoding_specific_params} =
281+
Keyword.intersect(encoding_specific_params_specs, opts)
282+
|> Bunch.Config.parse(encoding_specific_params_specs)
283+
237284
%{
238285
encoding_name: encoding_name,
239286
encoding_specific_params: encoding_specific_params,
@@ -242,29 +289,6 @@ defmodule Boombox.RTP do
242289
}
243290
end
244291

245-
@spec validate_and_parse_encoding!(
246-
:input,
247-
RTP.encoding_name() | Boombox.rtp_encoding_specific_params()
248-
) :: {RTP.encoding_name(), parsed_input_encoding_specific_params()}
249-
@spec validate_and_parse_encoding!(
250-
:output,
251-
RTP.encoding_name() | Boombox.rtp_encoding_specific_params()
252-
) :: {RTP.encoding_name(), parsed_output_encoding_specific_params()}
253-
defp validate_and_parse_encoding!(direction, encoding) do
254-
case encoding do
255-
nil ->
256-
raise "Encoding name not provided"
257-
258-
encoding when is_atom(encoding) ->
259-
validate_and_parse_encoding!(direction, {encoding, []})
260-
261-
{encoding, encoding_params} when is_atom(encoding) ->
262-
field_specs = Map.get(@required_encoding_specific_params[direction], encoding, [])
263-
{:ok, encoding_params} = Bunch.Config.parse(encoding_params, field_specs)
264-
{encoding, encoding_params}
265-
end
266-
end
267-
268292
@spec get_payload_type_mapping(%{audio: parsed_track_config(), video: parsed_track_config()}) ::
269293
RTP.PayloadFormat.payload_type_mapping()
270294
defp get_payload_type_mapping(track_configs) do

0 commit comments

Comments
 (0)