Skip to content

Commit 4d9666c

Browse files
authored
Use nimble options for config validation (#15)
* use nimble options for config validation * Fix tests * Add rtsp config section * mix format
1 parent 4945abc commit 4d9666c

6 files changed

Lines changed: 177 additions & 155 deletions

File tree

config/test.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ config :shinkai, :server, enabled: false
66

77
config :shinkai, :rtmp, port: 0
88

9+
config :shinkai, :rtsp, enabled: false
10+
911
config :shinkai, :hls, storage_dir: "tmp"

lib/shinkai.ex

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ defmodule Shinkai do
1515
1616
To configure the http server responsible for serving HLS streams.
1717
18-
* `enabled` - Enable or disable the HTTP server.
19-
* `port` - Port number for the HTTP server.
20-
* `certfile` - Path to the SSL certificate file (optional).
21-
* `keyfile` - Path to the SSL key file (optional).
18+
#{NimbleOptions.docs(Shinkai.Config.server_schema())}
2219
2320
```elixir
2421
config :shinkai, :server,
@@ -40,12 +37,7 @@ defmodule Shinkai do
4037
4138
To configure HLS streaming options.
4239
43-
* `storage_dir` - Directory to store HLS segments.
44-
* `max_segments` - Maximum number of segments to keep.
45-
* `segment_duration` - Segment duration in milliseconds.
46-
* `part_duration` - Part duration in milliseconds.
47-
* `segment_type` - Type of segments to generate, either `fmp4`,
48-
`mpeg_ts`, or `low_latency`.
40+
#{NimbleOptions.docs(Shinkai.Config.hls_schema())}
4941
5042
```elixir
5143
config :shinkai, :hls,
@@ -66,8 +58,8 @@ defmodule Shinkai do
6658
### RTMP
6759
6860
To configure the RTMP server.
69-
* `enabled` - Enable or disable the RTMP server.
70-
* `port` - Port number for the RTMP server.
61+
62+
#{NimbleOptions.docs(Shinkai.Config.rtmp_schema())}
7163
7264
```elixir
7365
config :shinkai, :rtmp,
@@ -81,6 +73,24 @@ defmodule Shinkai do
8173
port: 1935 # Port number for the RTMP server (default: 1935)
8274
```
8375
76+
### RTSP
77+
78+
To configure the RTSP server.
79+
80+
#{NimbleOptions.docs(Shinkai.Config.rtsp_schema())}
81+
82+
```elixir
83+
config :shinkai, :rtsp,
84+
enabled: true,
85+
port: 8554
86+
```
87+
88+
```yaml
89+
rtsp:
90+
enabled: true # Enable or disable the RTSP server (default: true)
91+
port: 8554 # Port number for the RTSP server (default: 8554)
92+
```
93+
8494
### Paths
8595
8696
To configure media source paths. Each source should have a unique alphanumeric ID.

lib/shinkai/application.ex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ defmodule Shinkai.Application do
1616
{Sources.PublishManager, []},
1717
{Registry, name: Sink.Registry, keys: :duplicate},
1818
{Registry, name: Source.Registry, keys: :unique},
19-
{Task, fn -> Sources.start_all() end},
20-
{RTSP.Server, handler: Sources.RTSP.Handler, port: 8554}
19+
{Task, fn -> Sources.start_all() end}
2120
]
2221

2322
children =
@@ -27,6 +26,13 @@ defmodule Shinkai.Application do
2726
children
2827
end
2928

29+
children =
30+
if config[:rtsp][:enabled] do
31+
children ++ [{RTSP.Server, handler: Sources.RTSP.Handler, port: config[:rtsp][:port]}]
32+
else
33+
children
34+
end
35+
3036
children =
3137
if Code.ensure_loaded?(Bandit) and config[:server][:enabled] do
3238
children ++ [{Bandit, configure_bandit(config[:server])}]

lib/shinkai/config.ex

Lines changed: 130 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,107 @@ defmodule Shinkai.Config do
33

44
use GenServer
55

6-
@top_level_keys [:rtmp, :server, :hls]
6+
@top_level_keys [:rtmp, :server, :hls, :rtsp]
77

8-
@default_config [
9-
rtmp: [
10-
enabled: true,
11-
port: 1935
8+
@rtmp_schema [
9+
enabled: [
10+
type: :boolean,
11+
default: true,
12+
doc: "Enable or disable rtmp"
1213
],
13-
server: [
14-
enabled: true,
15-
port: 8888,
16-
certfile: nil,
17-
keyfile: nil
14+
port: [
15+
type: {:in, 0..(2 ** 16 - 1)},
16+
default: 1935,
17+
doc: "RTMP listening port",
18+
type_doc: "`t::socket.port_number/0`",
19+
type_spec: quote(do: :socket.port_number())
20+
]
21+
]
22+
23+
@rtsp_schema [
24+
enabled: [
25+
type: :boolean,
26+
default: true,
27+
doc: "Enable or disable rtsp"
28+
],
29+
port: [
30+
type: {:in, 0..(2 ** 16 - 1)},
31+
default: 8554,
32+
doc: "RTSP listening port",
33+
type_doc: "`t::socket.port_number/0`",
34+
type_spec: quote(do: :socket.port_number())
35+
]
36+
]
37+
38+
@server_schema [
39+
enabled: [
40+
type: :boolean,
41+
default: true,
42+
doc: "Enable or disable http(s) server"
43+
],
44+
port: [
45+
type: {:in, 0..(2 ** 16 - 1)},
46+
default: 8888,
47+
doc: "http port",
48+
type_doc: "`t::socket.port_number/0`",
49+
type_spec: quote(do: :socket.port_number())
50+
],
51+
certfile: [
52+
type: {:or, [:string, nil]},
53+
default: nil,
54+
doc: "https certificate"
55+
],
56+
keyfile: [
57+
type: {:or, [:string, nil]},
58+
default: nil,
59+
doc: "https private key certificate"
60+
]
61+
]
62+
63+
@hls_schema [
64+
storage_dir: [
65+
type: :string,
66+
default: "/tmp/shinkai/hls",
67+
doc: "Directory to store HLS segments"
68+
],
69+
max_segments: [
70+
type: :non_neg_integer,
71+
default: 7,
72+
doc: "Max segments to keep in live playlists"
73+
],
74+
segment_duration: [
75+
type: :non_neg_integer,
76+
default: 2000,
77+
doc: "Segment duration in milliseconds"
1878
],
19-
hls: [
20-
storage_dir: "/tmp/shinkai/hls",
21-
max_segments: 7,
22-
segment_duration: 2_000,
23-
part_duration: 500,
24-
segment_type: :fmp4
79+
part_duration: [
80+
type: :non_neg_integer,
81+
default: 300,
82+
doc: "Part duration in milliseconds for low-latency HLS"
83+
],
84+
segment_type: [
85+
type: {:custom, __MODULE__, :validate_hls_segment_type, []},
86+
default: :fmp4,
87+
doc: "Type of segments to generate, either `:fmp4`, `:mpeg_ts` or `:low_latency`"
2588
]
2689
]
2790

91+
@doc false
92+
@spec server_schema() :: keyword()
93+
def server_schema, do: @server_schema
94+
95+
@doc false
96+
@spec rtmp_schema() :: keyword()
97+
def rtmp_schema, do: @rtmp_schema
98+
99+
@doc false
100+
@spec hls_schema() :: keyword()
101+
def hls_schema, do: @hls_schema
102+
103+
@doc false
104+
@spec rtsp_schema() :: keyword()
105+
def rtsp_schema, do: @rtsp_schema
106+
28107
def start_link(config) do
29108
GenServer.start_link(__MODULE__, config, name: __MODULE__)
30109
end
@@ -48,9 +127,14 @@ defmodule Shinkai.Config do
48127

49128
app_configs = Enum.map(@top_level_keys, &{&1, Application.get_env(:shinkai, &1, [])})
50129

51-
Enum.map(@default_config, fn {key, config} ->
130+
app_configs =
131+
@top_level_keys
132+
|> Enum.map(&{&1, []})
133+
|> Keyword.merge(app_configs)
134+
|> parse_and_validate()
135+
136+
Enum.map(app_configs, fn {key, config} ->
52137
config
53-
|> Keyword.merge(app_configs[key])
54138
|> Keyword.merge(user_config[key] || [])
55139
|> then(&{key, &1})
56140
end)
@@ -95,129 +179,41 @@ defmodule Shinkai.Config do
95179

96180
defp parse_and_validate([], acc), do: acc
97181

98-
defp parse_and_validate([{:hls, hls_config} | rest], acc) do
99-
hls_config = parse_and_validate_hls(hls_config)
100-
parse_and_validate(rest, [{:hls, hls_config} | acc])
101-
end
102-
103-
defp parse_and_validate([{:server, server_config} | rest], acc) do
104-
server_config = parse_and_validate_server(server_config)
105-
parse_and_validate(rest, [{:server, server_config} | acc])
106-
end
107-
108-
defp parse_and_validate([{:rtmp, rtmp_config} | rest], acc) do
109-
rtmp_config = parse_and_validate_rtmp(rtmp_config)
110-
parse_and_validate(rest, [{:rtmp, rtmp_config} | acc])
111-
end
112-
113-
defp parse_and_validate_hls(config, acc \\ [])
114-
115-
defp parse_and_validate_hls(nil, _acc), do: []
116-
defp parse_and_validate_hls([], acc), do: acc
117-
118-
defp parse_and_validate_hls(config, acc) when is_map(config) do
119-
parse_and_validate_hls(Map.to_list(config), acc)
120-
end
121-
122-
defp parse_and_validate_hls([{:segment_type, value} | rest], acc)
123-
when value in [:fmp4, :mpeg_ts, :low_latency] do
124-
parse_and_validate_hls(rest, [{:segment_type, value} | acc])
125-
end
126-
127-
defp parse_and_validate_hls([{"segment_type", value} | rest], acc)
128-
when value in ["fmp4", "mpeg_ts", "low_latency"] do
129-
parse_and_validate_hls(rest, [{:segment_type, String.to_atom(value)} | acc])
130-
end
131-
132-
defp parse_and_validate_hls([{key, value} | rest], acc)
133-
when key in ["segment_duration", :segment_duration] and is_integer(value) and value >= 1000 do
134-
parse_and_validate_hls(rest, [{:segment_duration, value} | acc])
135-
end
136-
137-
defp parse_and_validate_hls([{key, value} | rest], acc)
138-
when key in ["max_segments", :max_segments] and is_integer(value) and value > 3 do
139-
parse_and_validate_hls(rest, [{:max_segments, value} | acc])
140-
end
141-
142-
defp parse_and_validate_hls([{key, value} | rest], acc)
143-
when key in ["part_duration", :part_duration] and is_integer(value) and value >= 100 and
144-
value < 1000 do
145-
parse_and_validate_hls(rest, [{:part_duration, value} | acc])
146-
end
147-
148-
defp parse_and_validate_hls([{key, value} | rest], acc)
149-
when key in ["storage_dir", :storage_dir] do
150-
parse_and_validate_hls(rest, [{:storage_dir, value} | acc])
151-
end
152-
153-
defp parse_and_validate_hls([{key, value} | _rest], _acc) do
154-
raise ArgumentError, """
155-
Invalid HLS configuration key or value detected.
156-
Key: #{inspect(key)}, Value: #{inspect(value)}.
157-
"""
158-
end
182+
defp parse_and_validate([{key, config} | rest], acc) do
183+
config =
184+
case key do
185+
:hls -> do_parse_and_validate(config, @hls_schema)
186+
:server -> do_parse_and_validate(config, @server_schema)
187+
:rtmp -> do_parse_and_validate(config, @rtmp_schema)
188+
:rtsp -> do_parse_and_validate(config, @rtsp_schema)
189+
end
159190

160-
defp parse_and_validate_hls(config, _acc) do
161-
raise ArgumentError, """
162-
Invalid HLS configuration format detected.
163-
Config: #{inspect(config)}.
164-
"""
191+
parse_and_validate(rest, [{key, config} | acc])
165192
end
166193

167-
# HTTP server
168-
defp parse_and_validate_server(config, acc \\ [])
169-
defp parse_and_validate_server(nil, _acc), do: []
170-
defp parse_and_validate_server([], acc), do: acc
194+
defp do_parse_and_validate(config, schema) do
195+
config = config || []
171196

172-
defp parse_and_validate_server(config, acc) when is_map(config) do
173-
parse_and_validate_server(Map.to_list(config), acc)
174-
end
175-
176-
defp parse_and_validate_server([{key, value} | rest], acc)
177-
when key in ["enabled", :enabled] and is_boolean(value) do
178-
parse_and_validate_server(rest, [{:enabled, value} | acc])
179-
end
180-
181-
defp parse_and_validate_server([{key, value} | rest], acc)
182-
when key in [:port, "port"] and is_integer(value) and value > 0 and value < 65_536 do
183-
parse_and_validate_server(rest, [{:port, value} | acc])
184-
end
185-
186-
defp parse_and_validate_server([{key, value} | rest], acc)
187-
when key in ["certfile", "keyfile", :certfile, :keyfile] do
188-
parse_and_validate_server(rest, [{String.to_atom(key), value} | acc])
189-
end
197+
cond do
198+
Keyword.keyword?(config) ->
199+
NimbleOptions.validate!(config, schema)
190200

191-
defp parse_and_validate_server([{key, value} | _rest], _acc) do
192-
raise ArgumentError, """
193-
Invalid Server configuration key or value detected.
194-
Key: #{inspect(key)}, Value: #{inspect(value)}.
195-
"""
196-
end
201+
is_map(config) ->
202+
config
203+
|> Keyword.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
204+
|> NimbleOptions.validate!(schema)
197205

198-
# RTMP
199-
defp parse_and_validate_rtmp(config, acc \\ [])
200-
defp parse_and_validate_rtmp(nil, _acc), do: []
201-
defp parse_and_validate_rtmp([], acc), do: acc
202-
203-
defp parse_and_validate_rtmp(config, acc) when is_map(config) do
204-
parse_and_validate_rtmp(Map.to_list(config), acc)
205-
end
206-
207-
defp parse_and_validate_rtmp([{key, value} | rest], acc)
208-
when key in ["enabled", :enabled] and is_boolean(value) do
209-
parse_and_validate_rtmp(rest, [{:enabled, value} | acc])
210-
end
211-
212-
defp parse_and_validate_rtmp([{key, value} | rest], acc)
213-
when key in [:port, "port"] and is_integer(value) and value > 0 and value < 65_536 do
214-
parse_and_validate_rtmp(rest, [{:port, value} | acc])
206+
true ->
207+
raise ArgumentError, "Expected a map or keyword list received: #{inspect(config)}"
208+
end
215209
end
216210

217-
defp parse_and_validate_rtmp([{key, value} | _rest], _acc) do
218-
raise ArgumentError, """
219-
Invalid RTMP configuration key or value detected.
220-
Key: #{inspect(key)}, Value: #{inspect(value)}.
221-
"""
211+
@doc false
212+
def validate_hls_segment_type(value) do
213+
cond do
214+
value in [:mpeg_ts, :fmp4, :low_latency] -> {:ok, value}
215+
value in ["mpeg_ts", "fmp4", "low_latency"] -> {:ok, String.to_atom(value)}
216+
true -> {:error, value}
217+
end
222218
end
223219
end

0 commit comments

Comments
 (0)