nixos/jitsi-meet: add jibri.enable
This option enables a jibri service on the same host that is running jitsi-meet. It was written, along with the jibri module, by @puckipedia for nixcon-video-infra 2020. Co-authored-by: Puck Meerburg <puck@puck.moe>
This commit is contained in:
parent
f220223a16
commit
ff8ed90033
2 changed files with 419 additions and 5 deletions
353
nixos/modules/services/networking/jibri/default.nix
Normal file
353
nixos/modules/services/networking/jibri/default.nix
Normal file
|
@ -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 <option>services.jitsi-meet.jibri.enable</option>";
|
||||
config = mkOption {
|
||||
type = attrs;
|
||||
default = { };
|
||||
description = ''
|
||||
Jibri configuration.
|
||||
See <link xlink:href="https://github.com/jitsi/jibri/blob/master/src/main/resources/reference.conf" />
|
||||
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; [ ];
|
||||
}
|
|
@ -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 <option>services.jibri</option>, this is
|
||||
currently not very supported and most users will only want to edit the finalize recordings
|
||||
script at <option>services.jibri.finalizeScript</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
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.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue