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