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