at master 14 kB view raw
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}