snapshot of https://github.com/jitsi/jibri.git @ 3b45602c539ec69d128ea152ae587d0b6582191a (2026-09-15)
Some checks failed
Java CI with Maven / Java 17 (push) Failing after 4m40s
Java CI with Maven / Java 21 (push) Failing after 4m41s

This commit is contained in:
upstream-mirror
2026-09-15 16:13:58 +05:00
commit 650a143b63
166 changed files with 14301 additions and 0 deletions

310
doc/local-run/README.md Normal file
View File

@@ -0,0 +1,310 @@
# Running jibri locally against a docker-jitsi-meet deployment
This directory contains a script and a docker-compose override that let you
run jibri **from this source tree** while the rest of the Jitsi stack
(prosody, jicofo, jvb, web) runs in docker via
[docker-jitsi-meet](https://github.com/jitsi/docker-jitsi-meet). Useful for
iterating on jibri code changes without rebuilding the docker image.
## Files
- `run-jibri-local.sh` — generates `jibri.conf` + `logging.properties` from
your `docker-jitsi-meet/.env`, builds jibri if needed, and launches it.
- `prosody.override.yml` — publishes prosody's port 5222 on `127.0.0.1` so
the host-side jibri can reach it. The default compose only `expose:`s
this port inside the docker network.
## Prerequisites
- `docker` + `docker compose`
- `mvn` and Java 17 (to build jibri)
- `ffmpeg` on `PATH` (jibri shells out to it for capture)
- A clone of [docker-jitsi-meet](https://github.com/jitsi/docker-jitsi-meet)
(defaults to `~/dev/docker-jitsi-meet`; override with `DOCKER_JITSI_DIR`)
- A `chromedriver` binary at the jibri repo root, matching your installed
Chrome milestone (download from <https://googlechromelabs.github.io/chrome-for-testing/>)
## Step by step
### 1. Set up docker-jitsi-meet
```sh
cd ~/dev/docker-jitsi-meet
cp env.example .env
./gen-passwords.sh
mkdir -p ~/.jitsi-meet-cfg/{web,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb,jigasi,jibri}
```
### 2. Edit `.env`: enable recording and set `PUBLIC_URL`
Two changes in `~/dev/docker-jitsi-meet/.env`:
**Enable recording.** This is what tells prosody to provision the `jibri`
and `recorder` accounts (using `JIBRI_XMPP_PASSWORD` /
`JIBRI_RECORDER_PASSWORD` from `gen-passwords.sh`) and what makes the web
UI surface the record button. Without it, jibri can't even log into
prosody.
```sh
ENABLE_RECORDING=true
```
**Point `PUBLIC_URL` at your LAN IP.** Jibri opens the conference URL in
Chrome from the host. `localhost` won't do, because Chrome's WebRTC needs
an address that resolves the same way for jibri *and* the other browsers
joining the call. Use the host's LAN address.
Find your local IP:
```sh
# macOS
ipconfig getifaddr en0
# Linux
hostname -I | awk '{print $1}'
```
Then set (HTTP/HTTPS port already exist in env.example — adjust if you
moved them):
```sh
HTTP_PORT=8000
HTTPS_PORT=8443
PUBLIC_URL=https://192.168.1.42:8443 # ← your IP and the HTTPS_PORT
```
The script copies `PUBLIC_URL` into jibri's `base-url`, so jibri opens
`https://192.168.1.42:8443/<room>` instead of the default `https://meet.jitsi/`.
### 3. Bring the stack up with prosody 5222 published
The host-side jibri talks to prosody over XMPP, so we need 5222 reachable
from the host. Apply the override from this directory:
```sh
cd ~/dev/docker-jitsi-meet
docker compose \
-f docker-compose.yml \
-f ~/dev/jibri/doc/local-run/prosody.override.yml \
up -d
```
Confirm prosody is listening on the host:
```sh
nc -zv 127.0.0.1 5222 # → "succeeded!"
```
### 4. Build jibri
```sh
cd ~/dev/jibri
mvn -B -DskipTests clean package
```
The script will also build automatically if no jar is found in `target/`.
### 5. Drop a matching `chromedriver` next to the jibri repo
```sh
# look up your Chrome major version
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version
# fetch the matching chromedriver and place it at ~/dev/jibri/chromedriver
# (see https://googlechromelabs.github.io/chrome-for-testing/)
chmod +x ~/dev/jibri/chromedriver
```
### 6. (Linux only) Set up audio routing for ffmpeg
Jibri's default ffmpeg config captures audio from the ALSA PCM `plug:bsnoop`,
which is a custom loopback device defined in `/etc/jitsi/jibri/asoundrc` and
backed by the kernel `snd-aloop` module. The .deb postinst sets this up for
the system `jibri` user; if you're running locally as your own user, you
need to do it yourself, or switch to PulseAudio capture.
**Option A — ALSA loopback (matches the deb):**
```sh
# 1. Load the kernel loopback module (creates ALSA card "Loopback")
sudo modprobe snd-aloop
# Persist across reboots
echo snd-aloop | sudo tee /etc/modules-load.d/snd-aloop.conf
# 2. Drop jibri's asoundrc as your user's ALSA config
cp ~/dev/jibri/resources/debian-package/etc/jitsi/jibri/asoundrc ~/.asoundrc
# Verify
aplay -L | grep bsnoop
arecord -D plug:bsnoop -f cd -d 1 /tmp/t.wav && echo OK
```
Caveat: this changes your default ALSA device to a loopback, and on a
modern Ubuntu desktop Chrome routes audio through PulseAudio/PipeWire — so
even with `bsnoop` defined, Chrome won't play into the loopback unless you
also force Chrome to ALSA (e.g. `PULSE_SERVER=/dev/null`).
**Option B — PulseAudio capture (recommended on a desktop dev box):**
Skip the asoundrc/snd-aloop setup entirely and override the audio knobs
when invoking the script:
```sh
JIBRI_AUDIO_SOURCE=pulse \
JIBRI_AUDIO_DEVICE=default \
~/dev/jibri/doc/local-run/run-jibri-local.sh
```
`default` reads from Pulse's default source. To capture system audio
specifically, find the right monitor source with `pactl list short sources`
and pass it as `JIBRI_AUDIO_DEVICE` (e.g. `alsa_output.pci-…analog-stereo.monitor`).
### 6b. (macOS only) Capture the screen instead of the camera
Jibri's default macOS ffmpeg input is `-i 0:0` — whatever avfoundation
device 0 is (usually a camera + the mic). To record the conference you
want a **screen** device for video and a **loopback** device for audio.
1. **Grant Screen Recording permission** to the terminal app you launch
the script from (System Settings → Privacy & Security → Screen
Recording), then fully quit and relaunch the terminal. Without it
avfoundation silently produces black frames.
2. **Install a loopback audio device**, e.g. [BlackHole](https://github.com/ExistentialAudio/BlackHole)
(`brew install blackhole-2ch`), and route the call audio into it:
Audio MIDI Setup → `+` → *Create Multi-Output Device* → check both
BlackHole and your speakers, then select that multi-output device as
the system sound output. (Selecting BlackHole alone also works, but
you won't hear anything.)
3. **Find the device indices**:
```sh
ffmpeg -f avfoundation -list_devices true -i ""
# e.g. video: [2] Capture screen 0 audio: [2] BlackHole 2ch
```
4. **Run with the indices**:
```sh
JIBRI_MAC_VIDEO_DEVICE=2 JIBRI_MAC_AUDIO_DEVICE=2 \
~/dev/jibri/doc/local-run/run-jibri-local.sh
```
Note avfoundation screen devices capture the *whole* display at native
(retina) resolution — there is no per-window capture in ffmpeg on macOS.
The script scales the output down to `JIBRI_RECORDING_RESOLUTION` width,
but whatever else is on that screen ends up in the recording, so keep the
jibri Chrome window in front (or point it at a secondary display, e.g.
`Capture screen 1`).
### 7. Run jibri
```sh
~/dev/jibri/doc/local-run/run-jibri-local.sh
```
You should see:
```
config: /Users/you/dev/jibri/.local/jibri.conf
jar: /Users/you/dev/jibri/target/jibri-*-jar-with-dependencies.jar
chromedriver: /Users/you/dev/jibri/chromedriver
xmpp: 127.0.0.1:5222 (domain=meet.jitsi)
… INFO: Jibri starting up …
… INFO: Connecting to xmpp environment on 127.0.0.1 …
… INFO: Joined MUC: jibribrewery@internal-muc.meet.jitsi
```
Now open `https://192.168.1.42:8443/<room>` in two browsers, click the
record button — jibri will pick up the start IQ and join the call.
## Where things land
Generated files live under `~/dev/jibri/.local/`:
| File | Purpose |
|---|---|
| `jibri.conf` | HOCON config built from `.env` |
| `logging.properties` | JUL config (console + ffmpeg/browser file handlers) |
| `recordings/<session-id>/*.mp4` | recorded files |
| `ffmpeg.0.txt` | ffmpeg stderr (JUL captures it; not on console) |
| `browser.0.txt` | chrome browser logs |
| `chromedriver.log` | chromedriver's own log |
## Configuration knobs
All overridable as env vars when invoking the script:
| Var | Default | Notes |
|---|---|---|
| `JIBRI_DIR` | resolved from script path | jibri repo root |
| `DOCKER_JITSI_DIR` | `$HOME/dev/docker-jitsi-meet` | source of `.env` |
| `XMPP_HOST` | `127.0.0.1` | where prosody is reachable |
| `XMPP_PORT` | `5222` | matches `.env` if set there |
| `JIBRI_INSTANCE_ID` | `jibri-local-$$` | shows up as MUC nickname |
| `CHROMEDRIVER` | `$JIBRI_DIR/chromedriver` | path to chromedriver binary |
| `JIBRI_AUDIO_SOURCE` | `alsa` | Linux: ffmpeg `-f` for audio (`alsa`, `pulse`) |
| `JIBRI_AUDIO_DEVICE` | `plug:bsnoop` | Linux: ffmpeg audio input device (e.g. `default` for pulse) |
| `JIBRI_MAC_VIDEO_DEVICE` | unset | macOS: avfoundation video index (use a `Capture screen N` device) |
| `JIBRI_MAC_AUDIO_DEVICE` | unset | macOS: avfoundation audio index (use a loopback device, e.g. BlackHole) |
## Troubleshooting
**`permission denied while trying to connect to the docker API at unix:///var/run/docker.sock`**
— Linux only; your user isn't in the `docker` group. Fix:
```sh
sudo usermod -aG docker $USER
newgrp docker # apply the new group in this shell, or log out/in
docker ps # verify
```
Avoid `sudo docker compose …` — files written to `~/.jitsi-meet-cfg` end up
root-owned and bite you later.
**`cannot reach prosody at 127.0.0.1:5222`** — you brought up the stack
without `prosody.override.yml`. Re-run step 3.
**`This version of ChromeDriver only supports Chrome version N`** — your
chromedriver and Chrome milestones don't match. Re-fetch chromedriver for
your Chrome version (step 5).
**Jibri opens `https://meet.jitsi/<room>` instead of your IP** — `PUBLIC_URL`
isn't set in `.env`, or you didn't restart the script after editing `.env`.
**`No such file or directory: /var/log/jitsi/jibri/chromedriver.log`** —
older jibri pinned that path unconditionally. Pull a tree where
`webdriver.chrome.logfile` is overridable, or `mkdir -p` the path with
write permissions.
**ffmpeg quits with `Unknown PCM bsnoop` / `cannot open audio device plug:bsnoop`**
on Linux — you don't have the ALSA loopback set up. Either follow step 6
Option A (snd-aloop + `~/.asoundrc`) or skip ALSA and use Pulse via
`JIBRI_AUDIO_SOURCE=pulse JIBRI_AUDIO_DEVICE=default`.
**ffmpeg quits abruptly with "Error opening output files: Invalid argument"**
on macOS — usually an avfoundation arg conflict. Inspect
`~/dev/jibri/.local/ffmpeg.0.txt` for the real ffmpeg stderr. Note that the
default macOS ffmpeg input is `-i 0:0` (camera + system mic), so the
resulting mp4 records your camera, **not** the chrome window — see step 6b
for screen capture via `JIBRI_MAC_VIDEO_DEVICE`/`JIBRI_MAC_AUDIO_DEVICE`.
**Black video on macOS** — the terminal app that launched the script lacks
the Camera (default `0:0` input) or Screen Recording (screen-capture input)
permission. avfoundation doesn't error on a denied device; it delivers
black frames. Grant the permission in System Settings → Privacy & Security
and fully restart the terminal app.
**Black video on Linux** — you're on a Wayland session
(`echo $XDG_SESSION_TYPE`), where `x11grab` cannot see the screen and
records black. Either log into an Xorg session, or run a dedicated virtual
display and point both Chrome and ffmpeg at it:
```sh
Xvfb :1 -screen 0 1280x720x24 &
DISPLAY=:1 ~/dev/jibri/doc/local-run/run-jibri-local.sh
```
(the second part — overriding ffmpeg's hardcoded `-i :0.0+0,0` grab target —
currently requires editing `input-linux` in the generated conf).

View File

@@ -0,0 +1,4 @@
services:
prosody:
ports:
- '127.0.0.1:${XMPP_PORT:-5222}:${XMPP_PORT:-5222}'

238
doc/local-run/run-jibri-local.sh Executable file
View File

@@ -0,0 +1,238 @@
#!/usr/bin/env bash
#
# Generate a jibri.conf from a docker-jitsi-meet .env and run jibri from
# this source tree, connecting it to the running docker deployment.
#
# Defaults:
# JIBRI_DIR -> repo root (resolved from this script's location)
# DOCKER_JITSI_DIR -> $HOME/dev/docker-jitsi-meet
#
# Pre-req: prosody port 5222 must be reachable from the host. The default
# docker-jitsi-meet compose only `expose:`s it, so apply prosody.override.yml
# from this directory:
#
# docker compose -f docker-compose.yml \
# -f /path/to/jibri/doc/local-run/prosody.override.yml up -d
#
# See README.md alongside this script for a full walkthrough.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
JIBRI_DIR="${JIBRI_DIR:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
DOCKER_JITSI_DIR="${DOCKER_JITSI_DIR:-$HOME/dev/docker-jitsi-meet}"
ENV_FILE="$DOCKER_JITSI_DIR/.env"
[ -f "$ENV_FILE" ] || { echo "missing $ENV_FILE (set DOCKER_JITSI_DIR)" >&2; exit 1; }
[ -d "$JIBRI_DIR" ] || { echo "missing $JIBRI_DIR" >&2; exit 1; }
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
# Defaults match jibri/rootfs/defaults/xmpp.conf and jibri.conf templates.
XMPP_DOMAIN="${XMPP_DOMAIN:-meet.jitsi}"
XMPP_AUTH_DOMAIN="${XMPP_AUTH_DOMAIN:-auth.meet.jitsi}"
XMPP_INTERNAL_MUC_DOMAIN="${XMPP_INTERNAL_MUC_DOMAIN:-internal-muc.meet.jitsi}"
XMPP_HIDDEN_DOMAIN="${XMPP_HIDDEN_DOMAIN:-hidden.meet.jitsi}"
XMPP_MUC_DOMAIN="${XMPP_MUC_DOMAIN:-muc.meet.jitsi}"
JIBRI_BREWERY_MUC="${JIBRI_BREWERY_MUC:-jibribrewery}"
JIBRI_RECORDER_USER="${JIBRI_RECORDER_USER:-recorder}"
JIBRI_XMPP_USER="${JIBRI_XMPP_USER:-jibri}"
JIBRI_USAGE_TIMEOUT="${JIBRI_USAGE_TIMEOUT:-0}"
XMPP_PORT="${XMPP_PORT:-5222}"
XMPP_HOST="${XMPP_HOST:-127.0.0.1}"
JIBRI_RECORDING_RESOLUTION="${JIBRI_RECORDING_RESOLUTION:-1280x720}"
JIBRI_AUDIO_SOURCE="${JIBRI_AUDIO_SOURCE:-alsa}"
JIBRI_AUDIO_DEVICE="${JIBRI_AUDIO_DEVICE:-plug:bsnoop}"
# macOS only: avfoundation device indices for screen + audio capture.
# Find them with: ffmpeg -f avfoundation -list_devices true -i ""
# When unset, jibri's default input ("0:0", usually camera + mic) is used.
JIBRI_MAC_VIDEO_DEVICE="${JIBRI_MAC_VIDEO_DEVICE:-}"
JIBRI_MAC_AUDIO_DEVICE="${JIBRI_MAC_AUDIO_DEVICE:-}"
JIBRI_INSTANCE_ID="${JIBRI_INSTANCE_ID:-jibri-local-$$}"
JIBRI_STRIP_DOMAIN_JID="${JIBRI_STRIP_DOMAIN_JID:-${XMPP_MUC_DOMAIN%%.*}}"
: "${JIBRI_XMPP_PASSWORD:?JIBRI_XMPP_PASSWORD not set in .env (run gen-passwords.sh)}"
: "${JIBRI_RECORDER_PASSWORD:?JIBRI_RECORDER_PASSWORD not set in .env}"
BASE_URL_LINE=""
[ -n "${PUBLIC_URL:-}" ] && BASE_URL_LINE=" base-url = \"$PUBLIC_URL\""
WORKDIR="$JIBRI_DIR/.local"
JIBRI_CONF="$WORKDIR/jibri.conf"
LOGGING="$WORKDIR/logging.properties"
RECORDINGS="$WORKDIR/recordings"
mkdir -p "$WORKDIR" "$RECORDINGS"
# Probe prosody on the host before generating config / starting jibri.
# Run the /dev/tcp probe in a subshell so neither fd 3 nor stderr leak back here.
if ! (exec 3<>"/dev/tcp/$XMPP_HOST/$XMPP_PORT") 2>/dev/null; then
cat >&2 <<EOF
cannot reach prosody at $XMPP_HOST:$XMPP_PORT.
The default compose only exposes 5222 inside the docker network. Apply the
override shipped next to this script and restart:
docker compose -f docker-compose.yml -f $SCRIPT_DIR/prosody.override.yml up -d
EOF
exit 1
fi
# macOS screen capture: avfoundation's screen devices only deliver the native
# (retina) resolution, so drop -video_size from the input and scale on the
# encoder side instead.
FFMPEG_MAC_OVERRIDES=""
if [ -n "$JIBRI_MAC_VIDEO_DEVICE" ]; then
[ -n "$JIBRI_MAC_AUDIO_DEVICE" ] || {
echo "JIBRI_MAC_VIDEO_DEVICE is set but JIBRI_MAC_AUDIO_DEVICE is not (pick one from: ffmpeg -f avfoundation -list_devices true -i \"\")" >&2
exit 1
}
FFMPEG_MAC_OVERRIDES=$(cat <<EOG
input-mac = [
"-thread_queue_size", "4096",
"-f", "avfoundation",
"-capture_cursor", "0",
"-framerate", "30",
"-i", "$JIBRI_MAC_VIDEO_DEVICE:$JIBRI_MAC_AUDIO_DEVICE"
]
video-params = [
"-vf", "scale=${JIBRI_RECORDING_RESOLUTION%%x*}:-2",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-r", "30",
"-crf", "25"
]
EOG
)
fi
FFMPEG_LINUX_OVERRIDES=$(cat <<EOG
input-linux = [
"-f", "x11grab",
"-i", "${DISPLAY:-:0}",
]
EOG
)
cat > "$JIBRI_CONF" <<EOF
jibri {
id = "$JIBRI_INSTANCE_ID"
single-use-mode = false
recording {
recordings-directory = "$RECORDINGS"
finalize-script = "$JIBRI_DIR/resources/finalize_recording.sh"
}
ffmpeg {
resolution = "$JIBRI_RECORDING_RESOLUTION"
audio-source = "$JIBRI_AUDIO_SOURCE"
audio-device = "$JIBRI_AUDIO_DEVICE"
$FFMPEG_MAC_OVERRIDES
$FFMPEG_LINUX_OVERRIDES
}
chrome {
flags = [
"--use-fake-ui-for-media-stream",
"--enabled",
"--autoplay-policy=no-user-gesture-required",
"--ignore-certificate-errors",
"--ozone-platform=x11" # Force X11, required for x11grab to capture video
]
}
stats {
prometheus.enabled = false
}
api {
xmpp {
environments = [
{
name = "docker-local"
xmpp-server-hosts = ["$XMPP_HOST"]
xmpp-domain = "$XMPP_DOMAIN"
$BASE_URL_LINE
control-muc {
domain = "$XMPP_INTERNAL_MUC_DOMAIN"
room-name = "$JIBRI_BREWERY_MUC"
nickname = "$JIBRI_INSTANCE_ID"
}
control-login {
domain = "$XMPP_AUTH_DOMAIN"
port = "$XMPP_PORT"
username = "$JIBRI_XMPP_USER"
password = "$JIBRI_XMPP_PASSWORD"
}
call-login {
domain = "$XMPP_HIDDEN_DOMAIN"
username = "$JIBRI_RECORDER_USER"
password = "$JIBRI_RECORDER_PASSWORD"
}
strip-from-room-domain = "$JIBRI_STRIP_DOMAIN_JID."
usage-timeout = "$JIBRI_USAGE_TIMEOUT"
trust-all-xmpp-certs = true
}
]
}
}
}
EOF
# Console-only logging. Root .level must allow FINE through to the handler.
cat > "$LOGGING" <<EOF
handlers = java.util.logging.ConsoleHandler
.level = FINE
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = org.jitsi.utils.logging2.JitsiLogFormatter
# ffmpeg/browser are attached to dedicated JUL loggers with parent propagation
# disabled, so their output never reaches the console. Pin their FileHandlers
# to the workdir so we can read them.
org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.pattern = $WORKDIR/ffmpeg.%g.txt
org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.formatter = org.jitsi.utils.logging2.JitsiLogFormatter
org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.limit = 10000000
org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.count = 5
org.jitsi.jibri.selenium.util.BrowserFileHandler.pattern = $WORKDIR/browser.%g.txt
org.jitsi.jibri.selenium.util.BrowserFileHandler.formatter = org.jitsi.utils.logging2.JitsiLogFormatter
org.jitsi.jibri.selenium.util.BrowserFileHandler.limit = 10000000
org.jitsi.jibri.selenium.util.BrowserFileHandler.count = 5
org.jitsi.level = FINE
org.jitsi.jibri.config.level = INFO
org.glassfish.level = INFO
jdk.internal.net.http.level = INFO
EOF
JAR="$(ls -t "$JIBRI_DIR"/target/jibri-*-jar-with-dependencies.jar 2>/dev/null | head -n1 || true)"
if [ -z "$JAR" ]; then
echo "building jibri..."
(cd "$JIBRI_DIR" && mvn -B -DskipTests clean package)
JAR="$(ls -t "$JIBRI_DIR"/target/jibri-*-jar-with-dependencies.jar | head -n1)"
fi
CHROMEDRIVER="${CHROMEDRIVER:-$JIBRI_DIR/chromedriver}"
[ -x "$CHROMEDRIVER" ] || { echo "missing chromedriver at $CHROMEDRIVER" >&2; exit 1; }
echo "config: $JIBRI_CONF"
echo "jar: $JAR"
echo "chromedriver: $CHROMEDRIVER"
echo "xmpp: $XMPP_HOST:$XMPP_PORT (domain=$XMPP_DOMAIN)"
echo
exec java \
-Djava.util.logging.config.file="$LOGGING" \
-Dconfig.file="$JIBRI_CONF" \
-Dwebdriver.chrome.driver="$CHROMEDRIVER" \
-Dwebdriver.chrome.logfile="$WORKDIR/chromedriver.log" \
-jar "$JAR"