1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.jibri;
7
8 format = pkgs.formats.hocon { };
9
10 # We're passing passwords in environment variables that have names generated
11 # from an attribute name, which may not be a valid bash identifier.
12 toVarName = s: "XMPP_PASSWORD_" + stringAsChars (c: if builtins.match "[A-Za-z0-9]" c != null then c else "_") s;
13
14 defaultJibriConfig = {
15 id = "";
16 single-use-mode = false;
17
18 api = {
19 http.external-api-port = 2222;
20 http.internal-api-port = 3333;
21
22 xmpp.environments = flip mapAttrsToList cfg.xmppEnvironments (name: env: {
23 inherit name;
24
25 xmpp-server-hosts = env.xmppServerHosts;
26 xmpp-domain = env.xmppDomain;
27 control-muc = {
28 domain = env.control.muc.domain;
29 room-name = env.control.muc.roomName;
30 nickname = env.control.muc.nickname;
31 };
32
33 control-login = {
34 domain = env.control.login.domain;
35 username = env.control.login.username;
36 password = format.lib.mkSubstitution (toVarName "${name}_control");
37 };
38
39 call-login = {
40 domain = env.call.login.domain;
41 username = env.call.login.username;
42 password = format.lib.mkSubstitution (toVarName "${name}_call");
43 };
44
45 strip-from-room-domain = env.stripFromRoomDomain;
46 usage-timeout = env.usageTimeout;
47 trust-all-xmpp-certs = env.disableCertificateVerification;
48 });
49 };
50
51 recording = {
52 recordings-directory = "/tmp/recordings";
53 finalize-script = "${cfg.finalizeScript}";
54 };
55
56 streaming.rtmp-allow-list = [ ".*" ];
57
58 chrome.flags = [
59 "--use-fake-ui-for-media-stream"
60 "--start-maximized"
61 "--kiosk"
62 "--enabled"
63 "--disable-infobars"
64 "--autoplay-policy=no-user-gesture-required"
65 ]
66 ++ lists.optional cfg.ignoreCert
67 "--ignore-certificate-errors";
68
69
70 stats.enable-stats-d = true;
71 webhook.subscribers = [ ];
72
73 jwt-info = { };
74
75 call-status-checks = {
76 no-media-timout = "30 seconds";
77 all-muted-timeout = "10 minutes";
78 default-call-empty-timout = "30 seconds";
79 };
80 };
81 # Allow overriding leaves of the default config despite types.attrs not doing any merging.
82 jibriConfig = recursiveUpdate defaultJibriConfig cfg.config;
83 configFile = format.generate "jibri.conf" { jibri = jibriConfig; };
84in
85{
86 options.services.jibri = with types; {
87 enable = mkEnableOption "Jitsi BRoadcasting Infrastructure. Currently Jibri must be run on a host that is also running {option}`services.jitsi-meet.enable`, so for most use cases it will be simpler to run {option}`services.jitsi-meet.jibri.enable`";
88 config = mkOption {
89 type = format.type;
90 default = { };
91 description = ''
92 Jibri configuration.
93 See <https://github.com/jitsi/jibri/blob/master/src/main/resources/reference.conf>
94 for default configuration with comments.
95 '';
96 };
97
98 finalizeScript = mkOption {
99 type = types.path;
100 default = pkgs.writeScript "finalize_recording.sh" ''
101 #!/bin/sh
102
103 RECORDINGS_DIR=$1
104
105 echo "This is a dummy finalize script" > /tmp/finalize.out
106 echo "The script was invoked with recordings directory $RECORDINGS_DIR." >> /tmp/finalize.out
107 echo "You should put any finalize logic (renaming, uploading to a service" >> /tmp/finalize.out
108 echo "or storage provider, etc.) in this script" >> /tmp/finalize.out
109
110 exit 0
111 '';
112 defaultText = literalExpression ''
113 pkgs.writeScript "finalize_recording.sh" ''''''
114 #!/bin/sh
115
116 RECORDINGS_DIR=$1
117
118 echo "This is a dummy finalize script" > /tmp/finalize.out
119 echo "The script was invoked with recordings directory $RECORDINGS_DIR." >> /tmp/finalize.out
120 echo "You should put any finalize logic (renaming, uploading to a service" >> /tmp/finalize.out
121 echo "or storage provider, etc.) in this script" >> /tmp/finalize.out
122
123 exit 0
124 '''''';
125 '';
126 example = literalExpression ''
127 pkgs.writeScript "finalize_recording.sh" ''''''
128 #!/bin/sh
129 RECORDINGS_DIR=$1
130 ''${pkgs.rclone}/bin/rclone copy $RECORDINGS_DIR RCLONE_REMOTE:jibri-recordings/ -v --log-file=/var/log/jitsi/jibri/recording-upload.txt
131 exit 0
132 '''''';
133 '';
134 description = ''
135 This script runs when jibri finishes recording a video of a conference.
136 '';
137 };
138
139 ignoreCert = mkOption {
140 type = bool;
141 default = false;
142 example = true;
143 description = ''
144 Whether to enable the flag "--ignore-certificate-errors" for the Chromium browser opened by Jibri.
145 Intended for use in automated tests or anywhere else where using a verified cert for Jitsi-Meet is not possible.
146 '';
147 };
148
149 xmppEnvironments = mkOption {
150 description = ''
151 XMPP servers to connect to.
152 '';
153 example = literalExpression ''
154 "jitsi-meet" = {
155 xmppServerHosts = [ "localhost" ];
156 xmppDomain = config.services.jitsi-meet.hostName;
157
158 control.muc = {
159 domain = "internal.''${config.services.jitsi-meet.hostName}";
160 roomName = "JibriBrewery";
161 nickname = "jibri";
162 };
163
164 control.login = {
165 domain = "auth.''${config.services.jitsi-meet.hostName}";
166 username = "jibri";
167 passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret";
168 };
169
170 call.login = {
171 domain = "recorder.''${config.services.jitsi-meet.hostName}";
172 username = "recorder";
173 passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret";
174 };
175
176 usageTimeout = "0";
177 disableCertificateVerification = true;
178 stripFromRoomDomain = "conference.";
179 };
180 '';
181 default = { };
182 type = attrsOf (submodule ({ name, ... }: {
183 options = {
184 xmppServerHosts = mkOption {
185 type = listOf str;
186 example = [ "xmpp.example.org" ];
187 description = ''
188 Hostnames of the XMPP servers to connect to.
189 '';
190 };
191 xmppDomain = mkOption {
192 type = str;
193 example = "xmpp.example.org";
194 description = ''
195 The base XMPP domain.
196 '';
197 };
198 control.muc.domain = mkOption {
199 type = str;
200 description = ''
201 The domain part of the MUC to connect to for control.
202 '';
203 };
204 control.muc.roomName = mkOption {
205 type = str;
206 default = "JibriBrewery";
207 description = ''
208 The room name of the MUC to connect to for control.
209 '';
210 };
211 control.muc.nickname = mkOption {
212 type = str;
213 default = "jibri";
214 description = ''
215 The nickname for this Jibri instance in the MUC.
216 '';
217 };
218 control.login.domain = mkOption {
219 type = str;
220 description = ''
221 The domain part of the JID for this Jibri instance.
222 '';
223 };
224 control.login.username = mkOption {
225 type = str;
226 default = "jvb";
227 description = ''
228 User part of the JID.
229 '';
230 };
231 control.login.passwordFile = mkOption {
232 type = str;
233 example = "/run/keys/jibri-xmpp1";
234 description = ''
235 File containing the password for the user.
236 '';
237 };
238
239 call.login.domain = mkOption {
240 type = str;
241 example = "recorder.xmpp.example.org";
242 description = ''
243 The domain part of the JID for the recorder.
244 '';
245 };
246 call.login.username = mkOption {
247 type = str;
248 default = "recorder";
249 description = ''
250 User part of the JID for the recorder.
251 '';
252 };
253 call.login.passwordFile = mkOption {
254 type = str;
255 example = "/run/keys/jibri-recorder-xmpp1";
256 description = ''
257 File containing the password for the user.
258 '';
259 };
260 disableCertificateVerification = mkOption {
261 type = bool;
262 default = false;
263 description = ''
264 Whether to skip validation of the server's certificate.
265 '';
266 };
267
268 stripFromRoomDomain = mkOption {
269 type = str;
270 default = "0";
271 example = "conference.";
272 description = ''
273 The prefix to strip from the room's JID domain to derive the call URL.
274 '';
275 };
276 usageTimeout = mkOption {
277 type = str;
278 default = "0";
279 example = "1 hour";
280 description = ''
281 The duration that the Jibri session can be.
282 A value of zero means indefinitely.
283 '';
284 };
285 };
286
287 config =
288 let
289 nick = mkDefault (builtins.replaceStrings [ "." ] [ "-" ] (
290 config.networking.hostName + optionalString (config.networking.domain != null) ".${config.networking.domain}"
291 ));
292 in
293 {
294 call.login.username = nick;
295 control.muc.nickname = nick;
296 };
297 }));
298 };
299 };
300
301 config = mkIf cfg.enable {
302 users.groups.jibri = { };
303 users.groups.plugdev = { };
304 users.users.jibri = {
305 isSystemUser = true;
306 group = "jibri";
307 home = "/var/lib/jibri";
308 extraGroups = [ "jitsi-meet" "adm" "audio" "video" "plugdev" ];
309 };
310
311 systemd.services.jibri-xorg = {
312 description = "Jitsi Xorg Process";
313
314 after = [ "network.target" ];
315 wantedBy = [ "jibri.service" "jibri-icewm.service" ];
316
317 preStart = ''
318 cp --no-preserve=mode,ownership ${pkgs.jibri}/etc/jitsi/jibri/* /var/lib/jibri
319 mv /var/lib/jibri/{,.}asoundrc
320 '';
321
322 environment.DISPLAY = ":0";
323 serviceConfig = {
324 Type = "simple";
325
326 User = "jibri";
327 Group = "jibri";
328 KillMode = "process";
329 Restart = "on-failure";
330 RestartPreventExitStatus = 255;
331
332 StateDirectory = "jibri";
333
334 ExecStart = "${pkgs.xorg.xorgserver}/bin/Xorg -nocursor -noreset +extension RANDR +extension RENDER -config ${pkgs.jibri}/etc/jitsi/jibri/xorg-video-dummy.conf -logfile /dev/null :0";
335 };
336 };
337
338 systemd.services.jibri-icewm = {
339 description = "Jitsi Window Manager";
340
341 requires = [ "jibri-xorg.service" ];
342 after = [ "jibri-xorg.service" ];
343 wantedBy = [ "jibri.service" ];
344
345 environment.DISPLAY = ":0";
346 serviceConfig = {
347 Type = "simple";
348
349 User = "jibri";
350 Group = "jibri";
351 Restart = "on-failure";
352 RestartPreventExitStatus = 255;
353
354 StateDirectory = "jibri";
355
356 ExecStart = "${pkgs.icewm}/bin/icewm-session";
357 };
358 };
359
360 systemd.services.jibri = {
361 description = "Jibri Process";
362
363 requires = [ "jibri-icewm.service" "jibri-xorg.service" ];
364 after = [ "network.target" ];
365 wantedBy = [ "multi-user.target" ];
366
367 path = with pkgs; [ chromedriver chromium ffmpeg-full ];
368
369 script = (concatStrings (mapAttrsToList
370 (name: env: ''
371 export ${toVarName "${name}_control"}=$(cat ${env.control.login.passwordFile})
372 export ${toVarName "${name}_call"}=$(cat ${env.call.login.passwordFile})
373 '')
374 cfg.xmppEnvironments))
375 + ''
376 ${pkgs.jdk11_headless}/bin/java -Djava.util.logging.config.file=${./logging.properties-journal} -Dconfig.file=${configFile} -jar ${pkgs.jibri}/opt/jitsi/jibri/jibri.jar --config /var/lib/jibri/jibri.json
377 '';
378
379 environment.HOME = "/var/lib/jibri";
380
381 serviceConfig = {
382 Type = "simple";
383
384 User = "jibri";
385 Group = "jibri";
386 Restart = "always";
387 RestartPreventExitStatus = 255;
388
389 StateDirectory = "jibri";
390 };
391 };
392
393 systemd.tmpfiles.settings."10-jibri"."/var/log/jitsi/jibri".d = {
394 user = "jibri";
395 group = "jibri";
396 mode = "755";
397 };
398
399 # Configure Chromium to not show the "Chrome is being controlled by automatic test software" message.
400 environment.etc."chromium/policies/managed/managed_policies.json".text = builtins.toJSON { CommandLineFlagSecurityWarningsEnabled = false; };
401 warnings = [ "All security warnings for Chromium have been disabled. This is necessary for Jibri, but it also impacts all other uses of Chromium on this system." ];
402
403 boot = {
404 extraModprobeConfig = ''
405 options snd-aloop enable=1,1,1,1,1,1,1,1
406 '';
407 kernelModules = [ "snd-aloop" ];
408 };
409 };
410
411 meta.maintainers = lib.teams.jitsi.members;
412}