Jitsi: голосовой режим (custom-config.js), лимиты памяти JVB/jicofo, Jibri 800x480@5fps с finalize (только звук), muc_size, сеть crm-calls-bridge
This commit is contained in:
@@ -11,7 +11,12 @@
|
|||||||
# (docker-compose.calls.yml, https://crm-calls.volcanic.tes.gd): HS256, iss tvorymo-crm, aud jitsi,
|
# (docker-compose.calls.yml, https://crm-calls.volcanic.tes.gd): HS256, iss tvorymo-crm, aud jitsi,
|
||||||
# one room, context.user.moderator -> owner via prosody token_affiliation.
|
# one room, context.user.moderator -> owner via prosody token_affiliation.
|
||||||
#
|
#
|
||||||
# Secrets (JICOFO_AUTH_PASSWORD, JVB_AUTH_PASSWORD, JWT_APP_SECRET) live only in Coolify env vars.
|
# Voice-only calls: jitsi/custom-config.js is appended to the server config.js (browser and mobile app).
|
||||||
|
# Recording: jibri (one instance, started by the calls module over its HTTP API on the crm-calls-bridge network),
|
||||||
|
# jitsi/finalize.sh keeps only audio and deletes the video; files go to RECORDINGS_HOST_DIR shared with the module.
|
||||||
|
#
|
||||||
|
# Secrets (JICOFO_AUTH_PASSWORD, JVB_AUTH_PASSWORD, JWT_APP_SECRET, JIBRI_RECORDER_PASSWORD, JIBRI_XMPP_PASSWORD)
|
||||||
|
# live only in Coolify env vars.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
@@ -26,6 +31,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- jitsi-web-config:/config
|
- jitsi-web-config:/config
|
||||||
- jitsi-web-storage:/storage
|
- jitsi-web-storage:/storage
|
||||||
|
# Voice-only settings appended to config.js (see the file).
|
||||||
|
- ./jitsi/custom-config.js:/config/custom-config.js:ro
|
||||||
environment:
|
environment:
|
||||||
TZ: '${TZ:-Europe/Kyiv}'
|
TZ: '${TZ:-Europe/Kyiv}'
|
||||||
PUBLIC_URL: '${PUBLIC_URL:-https://meet.volcanic.tes.gd}'
|
PUBLIC_URL: '${PUBLIC_URL:-https://meet.volcanic.tes.gd}'
|
||||||
@@ -46,6 +53,10 @@ services:
|
|||||||
ENABLE_GUESTS: '0'
|
ENABLE_GUESTS: '0'
|
||||||
# The calls module shows its own "open in app / continue in browser" page.
|
# The calls module shows its own "open in app / continue in browser" page.
|
||||||
DISABLE_DEEP_LINKING: 'true'
|
DISABLE_DEEP_LINKING: 'true'
|
||||||
|
# Recording on: config gets hiddenDomain, so jibri can join as the hidden recorder.
|
||||||
|
ENABLE_RECORDING: '1'
|
||||||
|
START_AUDIO_ONLY: 'true'
|
||||||
|
START_WITH_VIDEO_MUTED: 'true'
|
||||||
# Only the calls module may embed Jitsi (call window). Jitsi's nginx sets no X-Frame-Options.
|
# Only the calls module may embed Jitsi (call window). Jitsi's nginx sets no X-Frame-Options.
|
||||||
CSP_HEADER: "frame-ancestors ${CALLS_ORIGIN:-https://crm-calls.volcanic.tes.gd}"
|
CSP_HEADER: "frame-ancestors ${CALLS_ORIGIN:-https://crm-calls.volcanic.tes.gd}"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -88,6 +99,16 @@ services:
|
|||||||
JWT_ACCEPTED_AUDIENCES: '${JWT_AUDIENCE:-jitsi}'
|
JWT_ACCEPTED_AUDIENCES: '${JWT_AUDIENCE:-jitsi}'
|
||||||
# context.user.moderator=true in the pass -> room owner (moderator); guests -> member.
|
# context.user.moderator=true in the pass -> room owner (moderator); guests -> member.
|
||||||
XMPP_MUC_MODULES: token_affiliation
|
XMPP_MUC_MODULES: token_affiliation
|
||||||
|
# HTTP /room?room=… (occupants) on prosody:5280 — the calls module polls it (concurrency limit, recording start/stop).
|
||||||
|
XMPP_MODULES: muc_size
|
||||||
|
ENABLE_RECORDING: '1'
|
||||||
|
JIBRI_RECORDER_PASSWORD: '${JIBRI_RECORDER_PASSWORD}'
|
||||||
|
JIBRI_XMPP_PASSWORD: '${JIBRI_XMPP_PASSWORD}'
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
calls-bridge:
|
||||||
|
aliases:
|
||||||
|
- jitsi-prosody
|
||||||
|
|
||||||
jicofo:
|
jicofo:
|
||||||
image: ghcr.io/jitsi/jicofo:${JITSI_IMAGE_VERSION:-stable-11248}
|
image: ghcr.io/jitsi/jicofo:${JITSI_IMAGE_VERSION:-stable-11248}
|
||||||
@@ -111,6 +132,10 @@ services:
|
|||||||
# Moderators come only from the pass (prosody token_affiliation); nobody becomes owner automatically.
|
# Moderators come only from the pass (prosody token_affiliation); nobody becomes owner automatically.
|
||||||
JICOFO_ENABLE_AUTH: '0'
|
JICOFO_ENABLE_AUTH: '0'
|
||||||
ENABLE_AUTO_OWNER: 'false'
|
ENABLE_AUTO_OWNER: 'false'
|
||||||
|
ENABLE_RECORDING: '1'
|
||||||
|
JIBRI_XMPP_PASSWORD: '${JIBRI_XMPP_PASSWORD}'
|
||||||
|
# Java heap (default 3072m — more than the whole server).
|
||||||
|
JICOFO_MAX_MEMORY: '${JICOFO_MAX_MEMORY:-256m}'
|
||||||
depends_on:
|
depends_on:
|
||||||
- prosody
|
- prosody
|
||||||
|
|
||||||
@@ -138,10 +163,65 @@ services:
|
|||||||
JVB_PORT: '${JVB_PORT:-10000}'
|
JVB_PORT: '${JVB_PORT:-10000}'
|
||||||
# Comma-separated public IP(s) announced to clients; set in Coolify env (164.90.233.156).
|
# Comma-separated public IP(s) announced to clients; set in Coolify env (164.90.233.156).
|
||||||
JVB_ADVERTISE_IPS: '${JVB_ADVERTISE_IPS:-164.90.233.156}'
|
JVB_ADVERTISE_IPS: '${JVB_ADVERTISE_IPS:-164.90.233.156}'
|
||||||
|
# Java heap (default 3072m — more than the whole server).
|
||||||
|
VIDEOBRIDGE_MAX_MEMORY: '${VIDEOBRIDGE_MAX_MEMORY:-512m}'
|
||||||
depends_on:
|
depends_on:
|
||||||
- prosody
|
- prosody
|
||||||
|
|
||||||
|
# One recorder (MAX_CONCURRENT_CALLS=1). Started/stopped by the calls module via http://jitsi-jibri:2222.
|
||||||
|
# Minimal video (800x480, 5 fps): only the audio is kept, see jitsi/finalize.sh.
|
||||||
|
jibri:
|
||||||
|
image: ghcr.io/jitsi/jibri:${JITSI_IMAGE_VERSION:-stable-11248}
|
||||||
|
restart: unless-stopped
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /run:size=256M,mode=1750,exec
|
||||||
|
- /tmp:size=256M,mode=1777,noexec
|
||||||
|
shm_size: '1gb'
|
||||||
|
volumes:
|
||||||
|
- jitsi-jibri-config:/config
|
||||||
|
- jitsi-jibri-storage:/storage
|
||||||
|
- '${RECORDINGS_HOST_DIR:-/data/crm-calls/recordings}:/recordings'
|
||||||
|
- ./jitsi/finalize.sh:/opt/crm-calls/finalize.sh:ro
|
||||||
|
environment:
|
||||||
|
TZ: '${TZ:-Europe/Kyiv}'
|
||||||
|
PUBLIC_URL: '${PUBLIC_URL:-https://meet.volcanic.tes.gd}'
|
||||||
|
XMPP_SERVER: prosody
|
||||||
|
XMPP_DOMAIN: meet.jitsi
|
||||||
|
XMPP_AUTH_DOMAIN: auth.meet.jitsi
|
||||||
|
XMPP_MUC_DOMAIN: muc.meet.jitsi
|
||||||
|
XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi
|
||||||
|
XMPP_HIDDEN_DOMAIN: hidden.meet.jitsi
|
||||||
|
JIBRI_RECORDER_PASSWORD: '${JIBRI_RECORDER_PASSWORD}'
|
||||||
|
JIBRI_XMPP_PASSWORD: '${JIBRI_XMPP_PASSWORD}'
|
||||||
|
JIBRI_HTTP_API_EXTERNAL_PORT: '2222'
|
||||||
|
JIBRI_RECORDING_DIR: /recordings
|
||||||
|
JIBRI_FINALIZE_RECORDING_SCRIPT_PATH: /opt/crm-calls/finalize.sh
|
||||||
|
JIBRI_RECORDING_RESOLUTION: '800x480'
|
||||||
|
JIBRI_RECORDING_FRAMERATE: '5'
|
||||||
|
JIBRI_RECORDING_CONSTANT_RATE_FACTOR: '40'
|
||||||
|
JIBRI_RECORDING_VIDEO_ENCODE_PRESET_RECORDING: ultrafast
|
||||||
|
# Jibri's own JVM: small heap. Voice calls: people may stay muted/silent for long —
|
||||||
|
# Jibri must not stop the recording on "no media"/"all muted"; the module stops it when the room empties.
|
||||||
|
JAVA_TOOL_OPTIONS: '-Xmx256m -Djibri.call-status-checks.no-media-timeout=4h -Djibri.call-status-checks.all-muted-timeout=4h -Djibri.call-status-checks.default-call-empty-timeout=60s'
|
||||||
|
CALLS_AUDIO_BITRATE: '${CALLS_AUDIO_BITRATE:-32k}'
|
||||||
|
CALLS_CHUNK_SECONDS: '${CALLS_CHUNK_SECONDS:-1200}'
|
||||||
|
depends_on:
|
||||||
|
- jicofo
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
calls-bridge:
|
||||||
|
aliases:
|
||||||
|
- jitsi-jibri
|
||||||
|
|
||||||
|
networks:
|
||||||
|
# Shared with the calls module app (docker-compose.calls.yml, external network of the same name).
|
||||||
|
calls-bridge:
|
||||||
|
name: crm-calls-bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
jitsi-jibri-config:
|
||||||
|
jitsi-jibri-storage:
|
||||||
jitsi-web-config:
|
jitsi-web-config:
|
||||||
jitsi-web-storage:
|
jitsi-web-storage:
|
||||||
jitsi-prosody-config:
|
jitsi-prosody-config:
|
||||||
|
|||||||
25
jitsi/custom-config.js
Normal file
25
jitsi/custom-config.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
// ---- CRM Tvorymo: лише голосові дзвінки (рішення власника 2026-10-15) ----
|
||||||
|
// Додається в кінець config.js сервера (docker-jitsi-meet: /config/custom-config.js),
|
||||||
|
// тож діє і в браузері (модуль «Дзвінки», прямий вхід), і в мобільному додатку Jitsi Meet,
|
||||||
|
// який завантажує config.js з нашого сервера.
|
||||||
|
config.startAudioOnly = true; // режим «лише звук»: відео не надсилається і не приймається
|
||||||
|
config.startWithVideoMuted = true;
|
||||||
|
config.startVideoMuted = 0;
|
||||||
|
config.disableShortcuts = true; // без «V» (камера) і «D» (екран)
|
||||||
|
config.toolbarButtons = [
|
||||||
|
'microphone', 'hangup', 'chat', 'raisehand', 'participants-pane',
|
||||||
|
'tileview', 'settings', 'fullscreen', 'noisesuppression', 'filmstrip'
|
||||||
|
];
|
||||||
|
config.settingsSections = [ 'devices', 'language', 'sounds', 'more' ];
|
||||||
|
config.disableSelfView = true;
|
||||||
|
config.disableAddingBackgroundImages = true;
|
||||||
|
config.disableVirtualBackground = true;
|
||||||
|
config.desktopSharingFrameRate = { min: 1, max: 1 };
|
||||||
|
config.prejoinConfig = Object.assign({}, config.prejoinConfig, {
|
||||||
|
hideExtraJoinButtons: [ 'no-audio', 'by-phone' ]
|
||||||
|
});
|
||||||
|
config.recordingService = Object.assign({}, config.recordingService, { hideStorageWarning: true });
|
||||||
|
// Запис запускає модуль «Дзвінки» на сервері (Jibri), учасникам кнопки запису не потрібні.
|
||||||
|
config.fileRecordingsServiceEnabled = false;
|
||||||
|
config.fileRecordingsServiceSharingEnabled = false;
|
||||||
64
jitsi/finalize.sh
Executable file
64
jitsi/finalize.sh
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Jibri finalize-скрипт (CRM Tvorymo, модуль «Дзвінки»).
|
||||||
|
# Jibri викликає його після завершення запису з каталогом сесії ($1), де лежить відео *.mp4.
|
||||||
|
# 1. Лише звук: моно, 16 кГц, низький бітрейт (mp3, якщо є libmp3lame, інакше opus).
|
||||||
|
# 2. Відео одразу видаляється.
|
||||||
|
# 3. Звук ріжеться на частини (CALLS_CHUNK_SECONDS) для Whisper (ліміт API 25 МБ).
|
||||||
|
# 4. meta.json: тривалості, частини, статистика якості; маркер ready.
|
||||||
|
# Далі файли забирає модуль «Дзвінки» (спільний каталог записів) і видаляє після успішної обробки.
|
||||||
|
set -u
|
||||||
|
DIR="${1:?каталог запису}"
|
||||||
|
LOG="$DIR/finalize.log"
|
||||||
|
exec >>"$LOG" 2>&1
|
||||||
|
echo "$(date -Is) finalize $DIR"
|
||||||
|
|
||||||
|
BITRATE="${CALLS_AUDIO_BITRATE:-32k}"
|
||||||
|
CHUNK="${CALLS_CHUNK_SECONDS:-1200}"
|
||||||
|
fail() { echo "$(date -Is) ПОМИЛКА: $*"; echo "$*" > "$DIR/failed"; exit 0; }
|
||||||
|
|
||||||
|
VIDEO="$(ls -1 "$DIR"/*.mp4 2>/dev/null | head -1)"
|
||||||
|
[ -n "$VIDEO" ] || fail "немає відеофайлу"
|
||||||
|
|
||||||
|
if ffmpeg -hide_banner -encoders 2>/dev/null | grep -q libmp3lame; then
|
||||||
|
EXT=mp3; MIME=audio/mpeg; CODEC=(-c:a libmp3lame -b:a "$BITRATE")
|
||||||
|
else
|
||||||
|
EXT=ogg; MIME=audio/ogg; CODEC=(-c:a libopus -application voip -b:a "$BITRATE")
|
||||||
|
fi
|
||||||
|
AUDIO="$DIR/audio.$EXT"
|
||||||
|
|
||||||
|
ffmpeg -nostdin -hide_banner -loglevel error -y -i "$VIDEO" -vn -ac 1 -ar 16000 "${CODEC[@]}" "$AUDIO" \
|
||||||
|
|| fail "ffmpeg не зміг витягти звук"
|
||||||
|
VIDEO_BYTES=$(stat -c %s "$VIDEO" 2>/dev/null || echo 0)
|
||||||
|
rm -f "$VIDEO" "$DIR"/*.mp4
|
||||||
|
echo "$(date -Is) відео видалено ($VIDEO_BYTES байт), звук: $AUDIO"
|
||||||
|
|
||||||
|
mkdir -p "$DIR/parts"
|
||||||
|
ffmpeg -nostdin -hide_banner -loglevel error -y -i "$AUDIO" -f segment -segment_time "$CHUNK" -reset_timestamps 1 -c copy \
|
||||||
|
"$DIR/parts/part-%03d.$EXT" || fail "ffmpeg не зміг розрізати звук"
|
||||||
|
|
||||||
|
dur() { ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$1" 2>/dev/null || echo 0; }
|
||||||
|
|
||||||
|
PARTS='[]'
|
||||||
|
for p in "$DIR"/parts/part-*."$EXT"; do
|
||||||
|
[ -f "$p" ] || continue
|
||||||
|
PARTS=$(jq -c --arg f "parts/$(basename "$p")" --argjson d "$(dur "$p")" --argjson s "$(stat -c %s "$p")" \
|
||||||
|
'. + [{file:$f, duration:$d, bytes:$s}]' <<<"$PARTS")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Якість: тиша (поріг -50 dB, від 2 с), гучність.
|
||||||
|
STATS=$(ffmpeg -nostdin -hide_banner -i "$AUDIO" -af silencedetect=n=-50dB:d=2,volumedetect -f null - 2>&1)
|
||||||
|
SIL_COUNT=$(grep -c 'silence_end' <<<"$STATS")
|
||||||
|
SIL_TOTAL=$(grep -o 'silence_duration: [0-9.]*' <<<"$STATS" | awk '{s+=$2} END {printf "%.1f", s+0}')
|
||||||
|
MEAN_DB=$(grep -o 'mean_volume: [-0-9.]*' <<<"$STATS" | awk '{print $2}')
|
||||||
|
MAX_DB=$(grep -o 'max_volume: [-0-9.]*' <<<"$STATS" | awk '{print $2}')
|
||||||
|
|
||||||
|
jq -n --arg audio "audio.$EXT" --arg mime "$MIME" --argjson duration "$(dur "$AUDIO")" \
|
||||||
|
--argjson bytes "$(stat -c %s "$AUDIO")" --argjson videoBytes "$VIDEO_BYTES" --argjson parts "$PARTS" \
|
||||||
|
--argjson silences "${SIL_COUNT:-0}" --argjson silenceSeconds "${SIL_TOTAL:-0}" \
|
||||||
|
--arg meanDb "${MEAN_DB:-}" --arg maxDb "${MAX_DB:-}" --arg finishedAt "$(date -Is)" \
|
||||||
|
'{audio:$audio, mime:$mime, duration:$duration, bytes:$bytes, videoBytes:$videoBytes, parts:$parts,
|
||||||
|
quality:{silences:$silences, silenceSeconds:$silenceSeconds, meanDb:$meanDb, maxDb:$maxDb}, finishedAt:$finishedAt}' \
|
||||||
|
> "$DIR/meta.json.tmp" && mv "$DIR/meta.json.tmp" "$DIR/meta.json" || fail "не вдалося записати meta.json"
|
||||||
|
chmod -R a+rwX "$DIR" 2>/dev/null
|
||||||
|
touch "$DIR/ready"
|
||||||
|
echo "$(date -Is) готово: $(cat "$DIR/meta.json")"
|
||||||
Reference in New Issue
Block a user