1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8let
9
10 inherit (lib)
11 attrValues
12 concatStringsSep
13 filterAttrs
14 length
15 listToAttrs
16 literalExpression
17 makeSearchPathOutput
18 mkEnableOption
19 mkIf
20 mkOption
21 nameValuePair
22 optionals
23 types
24 ;
25 inherit (utils) escapeSystemdPath;
26
27 cfg = config.services.v4l2-relayd;
28
29 kernelPackages = config.boot.kernelPackages;
30
31 gst = (
32 with pkgs.gst_all_1;
33 [
34 gst-plugins-bad
35 gst-plugins-base
36 gst-plugins-good
37 gstreamer.out
38 ]
39 );
40
41 instanceOpts =
42 { name, ... }:
43 {
44 options = {
45 enable = mkEnableOption "this v4l2-relayd instance";
46
47 name = mkOption {
48 type = types.str;
49 default = name;
50 description = ''
51 The name of the instance.
52 '';
53 };
54
55 cardLabel = mkOption {
56 type = types.str;
57 description = ''
58 The name the camera will show up as.
59 '';
60 };
61
62 extraPackages = mkOption {
63 type = with types; listOf package;
64 default = [ ];
65 description = ''
66 Extra packages to add to {env}`GST_PLUGIN_PATH` for the instance.
67 '';
68 };
69
70 input = {
71 pipeline = mkOption {
72 type = types.str;
73 description = ''
74 The gstreamer-pipeline to use for the input-stream.
75 '';
76 };
77
78 format = mkOption {
79 type = types.str;
80 default = "YUY2";
81 description = ''
82 The video-format to read from input-stream.
83 '';
84 };
85
86 width = mkOption {
87 type = types.ints.positive;
88 default = 1280;
89 description = ''
90 The width to read from input-stream.
91 '';
92 };
93
94 height = mkOption {
95 type = types.ints.positive;
96 default = 720;
97 description = ''
98 The height to read from input-stream.
99 '';
100 };
101
102 framerate = mkOption {
103 type = types.ints.positive;
104 default = 30;
105 description = ''
106 The framerate to read from input-stream.
107 '';
108 };
109 };
110
111 output = {
112 format = mkOption {
113 type = types.str;
114 default = "YUY2";
115 description = ''
116 The video-format to write to output-stream.
117 '';
118 };
119 };
120
121 };
122 };
123
124in
125{
126
127 options.services.v4l2-relayd = {
128
129 instances = mkOption {
130 type = with types; attrsOf (submodule instanceOpts);
131 default = { };
132 example = literalExpression ''
133 {
134 example = {
135 cardLabel = "Example card";
136 input.pipeline = "videotestsrc";
137 };
138 }
139 '';
140 description = ''
141 v4l2-relayd instances to be created.
142 '';
143 };
144
145 };
146
147 config =
148 let
149
150 mkInstanceService = instance: {
151 description = "Streaming relay for v4l2loopback using GStreamer";
152
153 after = [
154 "modprobe@v4l2loopback.service"
155 "systemd-logind.service"
156 ];
157 wantedBy = [ "multi-user.target" ];
158
159 serviceConfig = {
160 Type = "simple";
161 Restart = "always";
162 PrivateNetwork = true;
163 PrivateTmp = true;
164 LimitNPROC = 1;
165 };
166
167 environment = {
168 GST_PLUGIN_PATH = makeSearchPathOutput "lib" "lib/gstreamer-1.0" (gst ++ instance.extraPackages);
169 V4L2_DEVICE_FILE = "/run/v4l2-relayd-${instance.name}/device";
170 };
171
172 script =
173 let
174 appsrcOptions = concatStringsSep "," [
175 "caps=video/x-raw"
176 "format=${instance.input.format}"
177 "width=${toString instance.input.width}"
178 "height=${toString instance.input.height}"
179 "framerate=${toString instance.input.framerate}/1"
180 ];
181
182 outputPipeline = [
183 "appsrc name=appsrc ${appsrcOptions}"
184 "videoconvert"
185 ]
186 ++ optionals (instance.input.format != instance.output.format) [
187 "video/x-raw,format=${instance.output.format}"
188 "queue"
189 ]
190 ++ [ "v4l2sink name=v4l2sink device=$(cat $V4L2_DEVICE_FILE)" ];
191 in
192 ''
193 exec ${pkgs.v4l2-relayd}/bin/v4l2-relayd -i "${instance.input.pipeline}" -o "${concatStringsSep " ! " outputPipeline}"
194 '';
195
196 preStart = ''
197 mkdir -p $(dirname $V4L2_DEVICE_FILE)
198 ${kernelPackages.v4l2loopback.bin}/bin/v4l2loopback-ctl add -x 1 -n "${instance.cardLabel}" > $V4L2_DEVICE_FILE
199 '';
200
201 postStop = ''
202 ${kernelPackages.v4l2loopback.bin}/bin/v4l2loopback-ctl delete $(cat $V4L2_DEVICE_FILE)
203 rm -rf $(dirname $V4L2_DEVICE_FILE)
204 '';
205 };
206
207 mkInstanceServices =
208 instances:
209 listToAttrs (
210 map (
211 instance:
212 nameValuePair "v4l2-relayd-${escapeSystemdPath instance.name}" (mkInstanceService instance)
213 ) instances
214 );
215
216 enabledInstances = attrValues (filterAttrs (n: v: v.enable) cfg.instances);
217
218 in
219 {
220
221 boot = mkIf ((length enabledInstances) > 0) {
222 extraModulePackages = [ kernelPackages.v4l2loopback ];
223 kernelModules = [ "v4l2loopback" ];
224 };
225
226 systemd.services = mkInstanceServices enabledInstances;
227
228 };
229
230 meta.maintainers = with lib.maintainers; [ betaboon ];
231}