diff --git a/nixos/modules/services/networking/jibri/default.nix b/nixos/modules/services/networking/jibri/default.nix
new file mode 100644
index 000000000000..6075676c1bdb
--- /dev/null
+++ b/nixos/modules/services/networking/jibri/default.nix
@@ -0,0 +1,353 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.jibri;
+
+ # Copied from the jitsi-videobridge.nix file.
+ toHOCON = x:
+ if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}")
+ else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}"
+ else if isList x then "[${ concatMapStringsSep "," toHOCON x }]"
+ else builtins.toJSON x;
+
+ # We're passing passwords in environment variables that have names generated
+ # from an attribute name, which may not be a valid bash identifier.
+ toVarName = s: "XMPP_PASSWORD_" + stringAsChars (c: if builtins.match "[A-Za-z0-9]" c != null then c else "_") s;
+
+ defaultJibriConfig = {
+ id = "";
+ single-use-mode = false;
+
+ api = {
+ http.external-api-port = 2222;
+ http.internal-api-port = 3333;
+
+ xmpp.environments = flip mapAttrsToList cfg.xmppEnvironments (name: env: {
+ inherit name;
+
+ xmpp-server-hosts = env.xmppServerHosts;
+ xmpp-domain = env.xmppDomain;
+ control-muc = {
+ domain = env.control.muc.domain;
+ room-name = env.control.muc.roomName;
+ nickname = env.control.muc.nickname;
+ };
+
+ control-login = {
+ domain = env.control.login.domain;
+ username = env.control.login.username;
+ password.__hocon_envvar = toVarName "${name}_control";
+ };
+
+ call-login = {
+ domain = env.call.login.domain;
+ username = env.call.login.username;
+ password.__hocon_envvar = toVarName "${name}_call";
+ };
+
+ strip-from-room-domain = env.stripFromRoomDomain;
+ usage-timeout = env.usageTimeout;
+ trust-all-xmpp-certs = env.disableCertificateVerification;
+ });
+ };
+
+ recording = {
+ recordings-directory = "/tmp/recordings";
+ finalize-script = "${cfg.finalizeScript}";
+ };
+
+ streaming.rtmp-allow-list = [ ".*" ];
+
+ chrome.flags = [
+ "--use-fake-ui-for-media-stream"
+ "--start-maximized"
+ "--kiosk"
+ "--enabled"
+ "--disable-infobars"
+ "--autoplay-policy=no-user-gesture-required"
+ ];
+
+ stats.enable-stats-d = true;
+ webhook.subscribers = [ ];
+
+ jwt-info = { };
+
+ call-status-checks = {
+ no-media-timout = "30 seconds";
+ all-muted-timeout = "10 minutes";
+ default-call-empty-timout = "30 seconds";
+ };
+ };
+ # Allow overriding leaves of the default config despite types.attrs not doing any merging.
+ jibriConfig = recursiveUpdate defaultJibriConfig cfg.config;
+ configFile = pkgs.writeText "jibri.conf" (toHOCON { jibri = jibriConfig; });
+in
+{
+ options.services.jibri = with types; {
+ enable = mkEnableOption "Jitsi BRoadcasting Infrastructure. Currently the only supported way of enabling a jibri instance is with ";
+ config = mkOption {
+ type = attrs;
+ default = { };
+ description = ''
+ Jibri configuration.
+ See
+ for default configuration with comments.
+ '';
+ };
+
+ finalizeScript = mkOption {
+ type = types.path;
+ default = pkgs.writeScript "finalize_recording.sh" ''
+ #!/bin/sh
+
+ RECORDINGS_DIR=$1
+
+ echo "This is a dummy finalize script" > /tmp/finalize.out
+ echo "The script was invoked with recordings directory $RECORDINGS_DIR." >> /tmp/finalize.out
+ echo "You should put any finalize logic (renaming, uploading to a service" >> /tmp/finalize.out
+ echo "or storage provider, etc.) in this script" >> /tmp/finalize.out
+
+ exit 0
+ '';
+ description = ''
+ This script runs when jibri finishes recording a video of a conference.
+ '';
+ };
+
+ xmppEnvironments = mkOption {
+ description = ''
+ XMPP servers to connect to.
+ '';
+ default = { };
+ type = attrsOf (submodule ({ name, ... }: {
+ options = {
+ xmppServerHosts = mkOption {
+ type = listOf str;
+ example = [ "xmpp.example.org" ];
+ description = ''
+ Hostnames of the XMPP servers to connect to.
+ '';
+ };
+ xmppDomain = mkOption {
+ type = str;
+ example = "xmpp.example.org";
+ description = ''
+ The base XMPP domain.
+ '';
+ };
+ control.muc.domain = mkOption {
+ type = str;
+ description = ''
+ The domain part of the MUC to connect to for control.
+ '';
+ };
+ control.muc.roomName = mkOption {
+ type = str;
+ default = "JibriBrewery";
+ description = ''
+ The room name of the MUC to connect to for control.
+ '';
+ };
+ control.muc.nickname = mkOption {
+ type = str;
+ default = "jibri";
+ description = ''
+ The nickname for this Jibri instance in the MUC.
+ '';
+ };
+ control.login.domain = mkOption {
+ type = str;
+ description = ''
+ The domain part of the JID for this Jibri instance.
+ '';
+ };
+ control.login.username = mkOption {
+ type = str;
+ default = "jvb";
+ description = ''
+ User part of the JID.
+ '';
+ };
+ control.login.passwordFile = mkOption {
+ type = str;
+ example = "/run/keys/jibri-xmpp1";
+ description = ''
+ File containing the password for the user.
+ '';
+ };
+
+ call.login.domain = mkOption {
+ type = str;
+ example = "recorder.xmpp.example.org";
+ description = ''
+ The domain part of the JID for the recorder.
+ '';
+ };
+ call.login.username = mkOption {
+ type = str;
+ default = "recorder";
+ description = ''
+ User part of the JID for the recorder.
+ '';
+ };
+ call.login.passwordFile = mkOption {
+ type = str;
+ example = "/run/keys/jibri-recorder-xmpp1";
+ description = ''
+ File containing the password for the user.
+ '';
+ };
+ disableCertificateVerification = mkOption {
+ type = bool;
+ default = false;
+ description = ''
+ Whether to skip validation of the server's certificate.
+ '';
+ };
+
+ stripFromRoomDomain = mkOption {
+ type = str;
+ default = "0";
+ example = "conference.";
+ description = ''
+ The prefix to strip from the room's JID domain to derive the call URL.
+ '';
+ };
+ usageTimeout = mkOption {
+ type = str;
+ default = "0";
+ example = "1 hour";
+ description = ''
+ The duration that the Jibri session can be.
+ A value of zero means indefinitely.
+ '';
+ };
+ };
+
+ config =
+ let
+ nick = mkDefault (builtins.replaceStrings [ "." ] [ "-" ] (
+ config.networking.hostName + optionalString (config.networking.domain != null) ".${config.networking.domain}"
+ ));
+ in
+ {
+ call.login.username = nick;
+ control.muc.nickname = nick;
+ };
+ }));
+ };
+ };
+
+ config = mkIf cfg.enable {
+ users.groups.jibri = { };
+ users.users.jibri = {
+ isSystemUser = true;
+ group = "jibri";
+ home = "/var/lib/jibri";
+ extraGroups = [ "jitsi-meet" "adm" "audio" "video" "plugdev" ];
+ };
+
+ systemd.services.jibri-xorg = {
+ description = "Jitsi Xorg Process";
+
+ after = [ "network.target" ];
+ wantedBy = [ "jibri.service" "jibri-icewm.service" ];
+
+ preStart = ''
+ cp --no-preserve=mode,ownership ${pkgs.jibri}/etc/jitsi/jibri/* /var/lib/jibri
+ mv /var/lib/jibri/{,.}asoundrc
+ '';
+
+ environment.DISPLAY = ":0";
+ serviceConfig = {
+ Type = "simple";
+
+ User = "jibri";
+ Group = "jibri";
+ KillMode = "process";
+ Restart = "on-failure";
+ RestartPreventExitStatus = 255;
+
+ StateDirectory = "jibri";
+
+ 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";
+ };
+ };
+
+ systemd.services.jibri-icewm = {
+ description = "Jitsi Window Manager";
+
+ requires = [ "jibri-xorg.service" ];
+ after = [ "jibri-xorg.service" ];
+ wantedBy = [ "jibri.service" ];
+
+ environment.DISPLAY = ":0";
+ serviceConfig = {
+ Type = "simple";
+
+ User = "jibri";
+ Group = "jibri";
+ Restart = "on-failure";
+ RestartPreventExitStatus = 255;
+
+ StateDirectory = "jibri";
+
+ ExecStart = "${pkgs.icewm}/bin/icewm-session";
+ };
+ };
+
+ systemd.services.jibri = {
+ description = "Jibri Process";
+
+ requires = [ "jibri-icewm.service" "jibri-xorg.service" ];
+ after = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+
+ path = [ pkgs.chromedriver pkgs.chromium pkgs.ffmpeg-full ];
+
+ script = (concatStrings (mapAttrsToList
+ (name: env: ''
+ export ${toVarName "${name}_control"}=$(cat ${env.control.login.passwordFile})
+ export ${toVarName "${name}_call"}=$(cat ${env.call.login.passwordFile})
+ '')
+ cfg.xmppEnvironments))
+ + ''
+ ${pkgs.jre8_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
+ '';
+
+ environment.HOME = "/var/lib/jibri";
+
+ serviceConfig = {
+ Type = "simple";
+
+ User = "jibri";
+ Group = "jibri";
+ Restart = "always";
+ RestartPreventExitStatus = 255;
+
+ StateDirectory = "jibri";
+ };
+ };
+
+ systemd.tmpfiles.rules =
+ [
+ "d /var/log/jitsi/jibri 755 jibri"
+ ];
+
+
+
+ # Configure Chromium to not show the "Chrome is being controlled by automatic test software" message.
+ environment.etc."chromium/policies/managed/managed_policies.json".text = builtins.toJSON { CommandLineFlagSecurityWarningsEnabled = false; };
+
+ boot = {
+ extraModprobeConfig = ''
+ options snd-aloop enable=1,1,1,1,1,1,1,1
+ '';
+ kernelModules = [ "snd-aloop" ];
+ };
+ };
+
+ meta.maintainers = with lib.maintainers; [ ];
+}
diff --git a/nixos/modules/services/web-apps/jitsi-meet.nix b/nixos/modules/services/web-apps/jitsi-meet.nix
index 2eacd87ae6fd..8dc611c32e38 100644
--- a/nixos/modules/services/web-apps/jitsi-meet.nix
+++ b/nixos/modules/services/web-apps/jitsi-meet.nix
@@ -38,6 +38,10 @@ let
};
bosh = "//${cfg.hostName}/http-bind";
websocket = "wss://${cfg.hostName}/xmpp-websocket";
+
+ fileRecordingsEnabled = true;
+ liveStreamingEnabled = true;
+ hiddenDomain = "recorder.${cfg.hostName}";
};
in
{
@@ -130,6 +134,18 @@ in
'';
};
+ jibri.enable = mkOption {
+ type = bool;
+ default = false;
+ description = ''
+ Whether to enable a Jibri instance and configure it to connect to Prosody.
+
+ Although additional configuration is possible with , this is
+ currently not very supported and most users will only want to edit the finalize recordings
+ script at .
+ '';
+ };
+
nginx.enable = mkOption {
type = bool;
default = true;
@@ -229,6 +245,14 @@ in
key = "/var/lib/jitsi-meet/jitsi-meet.key";
};
};
+ virtualHosts."recorder.${cfg.hostName}" = {
+ enabled = true;
+ domain = "recorder.${cfg.hostName}";
+ extraConfig = ''
+ authentication = "internal_plain"
+ c2s_require_encryption = false
+ '';
+ };
};
systemd.services.prosody.serviceConfig = mkIf cfg.prosody.enable {
EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
@@ -243,12 +267,13 @@ in
systemd.services.jitsi-meet-init-secrets = {
wantedBy = [ "multi-user.target" ];
before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service");
+ path = [ config.services.prosody.package ];
serviceConfig = {
Type = "oneshot";
};
script = let
- secrets = [ "jicofo-component-secret" "jicofo-user-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
+ secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
in
''
@@ -267,9 +292,11 @@ in
chmod 640 secrets-env
''
+ optionalString cfg.prosody.enable ''
- ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
- ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
- ${config.services.prosody.package}/bin/prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
+ prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
+ prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
+ prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
+ prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
+ prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
# generate self-signed certificates
if [ ! -f /var/lib/jitsi-meet.crt ]; then
@@ -380,8 +407,42 @@ in
userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret";
componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
bridgeMuc = "jvbbrewery@internal.${cfg.hostName}";
- config = {
+ config = mkMerge [{
"org.jitsi.jicofo.ALWAYS_TRUST_MODE_ENABLED" = "true";
+ } (lib.mkIf cfg.jibri.enable {
+ "org.jitsi.jicofo.jibri.BREWERY" = "JibriBrewery@internal.${cfg.hostName}";
+ "org.jitsi.jicofo.jibri.PENDING_TIMEOUT" = "90";
+ })];
+ };
+
+ services.jibri = mkIf cfg.jibri.enable {
+ enable = true;
+
+ xmppEnvironments."jitsi-meet" = {
+ xmppServerHosts = [ "localhost" ];
+ xmppDomain = cfg.hostName;
+
+ control.muc = {
+ domain = "internal.${cfg.hostName}";
+ roomName = "JibriBrewery";
+ nickname = "jibri";
+ };
+
+ control.login = {
+ domain = "auth.${cfg.hostName}";
+ username = "jibri";
+ passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret";
+ };
+
+ call.login = {
+ domain = "recorder.${cfg.hostName}";
+ username = "recorder";
+ passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret";
+ };
+
+ usageTimeout = "0";
+ disableCertificateVerification = true;
+ stripFromRoomDomain = "conference.";
};
};
};