├── debian ├── compat ├── source │ └── format ├── rules ├── changelog ├── copyright ├── control ├── jibri.install └── postinst ├── resources ├── debian-package │ ├── etc │ │ ├── jitsi │ │ │ └── jibri │ │ │ │ ├── jibri.conf │ │ │ │ ├── icewm.preferences │ │ │ │ ├── pjsua.config │ │ │ │ └── asoundrc │ │ └── systemd │ │ │ └── system │ │ │ ├── jibri-icewm.service │ │ │ ├── jibri-xorg.service │ │ │ └── jibri.service │ └── opt │ │ └── jitsi │ │ └── jibri │ │ ├── pjsua.sh │ │ ├── launch.sh │ │ ├── shutdown.sh │ │ ├── reload.sh │ │ ├── graceful_shutdown.sh │ │ ├── finalize_sip.sh │ │ ├── collect-dump-logs.sh │ │ └── wait_graceful_shutdown.sh ├── jenkins │ ├── build.sh │ └── release.sh ├── build.sh ├── add_git_pre_commit_script.sh └── finalize_recording.sh ├── .gitignore ├── src ├── test │ ├── resources │ │ ├── mockito-extensions │ │ │ └── org.mockito.plugins.MockMaker │ │ └── logback-test.xml │ └── kotlin │ │ └── org │ │ └── jitsi │ │ └── jibri │ │ ├── helpers │ │ ├── SeleniumMockHelper.kt │ │ ├── FinalizeMockHelper.kt │ │ └── Helpers.kt │ │ ├── selenium │ │ ├── status_checks │ │ │ ├── LocalParticipantKickedStatusCheckTest.kt │ │ │ ├── IceConnectionStatusCheckTest.kt │ │ │ └── EmptyCallStatusCheckTest.kt │ │ └── SeleniumStateMachineTest.kt │ │ ├── KotestProjectConfig.kt │ │ ├── sink │ │ └── impl │ │ │ └── FileSinkTest.kt │ │ ├── util │ │ ├── TailLogicTest.kt │ │ ├── LoggingUtilsKtTest.kt │ │ ├── FileUtilsKtTest.kt │ │ ├── XmppUtilsTest.kt │ │ ├── TeeLogicTest.kt │ │ └── JibriSubprocessTest.kt │ │ ├── api │ │ └── http │ │ │ └── internal │ │ │ └── InternalHttpApiTest.kt │ │ ├── service │ │ └── AppDataTest.kt │ │ ├── CallUrlInfoTest.kt │ │ └── capture │ │ └── ffmpeg │ │ └── executor │ │ └── OutputParserTest.kt └── main │ ├── kotlin │ └── org │ │ └── jitsi │ │ └── jibri │ │ ├── selenium │ │ ├── status_checks │ │ │ ├── CallStatusCheck.kt │ │ │ ├── LocalParticipantKickedStatusCheck.kt │ │ │ ├── StateTransitionTimeTracker.kt │ │ │ ├── IceConnectionStatusCheck.kt │ │ │ ├── EmptyCallStatusCheck.kt │ │ │ └── MediaReceivedStatusCheck.kt │ │ ├── util │ │ │ └── BrowserFileHandler.kt │ │ ├── pageobjects │ │ │ ├── HomePage.kt │ │ │ └── AbstractPageObject.kt │ │ ├── Errors.kt │ │ └── SeleniumStateMachine.kt │ │ ├── service │ │ ├── JibriServiceFinalizer.kt │ │ ├── Errors.kt │ │ ├── impl │ │ │ ├── StatefulJibriService.kt │ │ │ └── JibriServiceFinalizeCommandRunner.kt │ │ ├── JibriService.kt │ │ └── JibriServiceStateMachine.kt │ │ ├── sipgateway │ │ ├── pjsua │ │ │ └── util │ │ │ │ ├── PjsuaFileHandler.kt │ │ │ │ ├── Error.kt │ │ │ │ └── OutputParser.kt │ │ └── SipClient.kt │ │ ├── metrics │ │ ├── JibriMetricsContainer.kt │ │ ├── StatsConfig.kt │ │ └── StatsDEvents.kt │ │ ├── util │ │ ├── RandomUtils.kt │ │ ├── OsDetector.kt │ │ ├── NameableThreadFactory.kt │ │ ├── ProcessFactory.kt │ │ ├── FileUtils.kt │ │ ├── NotifyingStateMachine.kt │ │ ├── RegexUtils.kt │ │ ├── TaskPools.kt │ │ ├── extensions │ │ │ └── SchedulerExecutorServiceExts.kt │ │ ├── StatusPublisher.kt │ │ ├── ProcessState.kt │ │ ├── StateUtils.kt │ │ ├── LoggingUtils.kt │ │ ├── Tail.kt │ │ ├── XmppUtils.kt │ │ ├── Tee.kt │ │ └── JibriSubprocess.kt │ │ ├── sink │ │ ├── impl │ │ │ ├── StreamSink.kt │ │ │ └── FileSink.kt │ │ └── Sink.kt │ │ ├── status │ │ ├── ComponentBusyStatus.kt │ │ ├── ComponentHealthStatus.kt │ │ ├── ComponentStates.kt │ │ └── JibriStatusManager.kt │ │ ├── capture │ │ ├── ffmpeg │ │ │ ├── util │ │ │ │ └── FfmpegFileHandler.kt │ │ │ └── Errors.kt │ │ └── Capturer.kt │ │ ├── error │ │ └── JibriError.kt │ │ ├── webhooks │ │ └── v1 │ │ │ ├── Events.kt │ │ │ └── README.md │ │ ├── health │ │ └── JibriHealth.kt │ │ ├── config │ │ └── Config.kt │ │ ├── CallUrlInfo.kt │ │ └── api │ │ ├── xmpp │ │ ├── JibriIqHelper.kt │ │ └── JibriStatusExts.kt │ │ └── http │ │ └── internal │ │ └── InternalHttpApi.kt │ └── java │ └── org │ └── jitsi │ └── jibri │ └── RecordingSinkType.java ├── .editorconfig ├── SECURITY.md ├── doc ├── sip_gateway.md ├── debian_package.md ├── internal_http_api.md ├── apis.md ├── xmpp_api.md ├── development.md ├── example_xmpp_envs.conf └── http_api.md ├── lib ├── test-logging.properties └── logging.properties ├── .github ├── ISSUE_TEMPLATE │ └── bug-report.md └── workflows │ └── maven.yml └── checkstyle.xml /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /resources/debian-package/etc/jitsi/jibri/jibri.conf: -------------------------------------------------------------------------------- 1 | jibri { 2 | } 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | .kotlintest 4 | *~ 5 | *.sw? 6 | *.iml 7 | -------------------------------------------------------------------------------- /resources/debian-package/etc/jitsi/jibri/icewm.preferences: -------------------------------------------------------------------------------- 1 | ShowTaskBar = 0 2 | -------------------------------------------------------------------------------- /resources/jenkins/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | ./resources/build.sh 6 | -------------------------------------------------------------------------------- /src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /resources/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | mvn -B clean verify package "$@" 6 | -------------------------------------------------------------------------------- /resources/debian-package/opt/jitsi/jibri/pjsua.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec /usr/local/bin/pjsua --config-file /etc/jitsi/jibri/pjsua.config "$@" > /dev/null 4 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | override_dh_strip_nondeterminism: 4 | # Disables dh_strip_nondeterminism to speed up the debian package creation 5 | 6 | %: 7 | dh $@ 8 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | jibri (0.2.0-1) UNRELEASED; urgency=medium 2 | 3 | * Initial release. (Closes: #XXXXXX) 4 | 5 | -- brian Tue, 27 Mar 2018 14:04:40 -0700 6 | -------------------------------------------------------------------------------- /resources/debian-package/opt/jitsi/jibri/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec java -Djava.util.logging.config.file=/etc/jitsi/jibri/logging.properties -Dconfig.file="/etc/jitsi/jibri/jibri.conf" -jar /opt/jitsi/jibri/jibri.jar --config "/etc/jitsi/jibri/config.json" 4 | -------------------------------------------------------------------------------- /resources/debian-package/opt/jitsi/jibri/shutdown.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONF="/etc/jitsi/jibri/jibri.conf" 4 | 5 | PORT=$(hocon -f $CONF get jibri.api.http.internal-api-port 2>/dev/null || true) 6 | [[ -z "$PORT" ]] && PORT=3333 7 | 8 | curl -sX POST http://127.0.0.1:$PORT/jibri/api/internal/v1.0/shutdown 9 | -------------------------------------------------------------------------------- /resources/debian-package/opt/jitsi/jibri/reload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONF="/etc/jitsi/jibri/jibri.conf" 4 | 5 | PORT=$(hocon -f $CONF get jibri.api.http.internal-api-port 2>/dev/null || true) 6 | [[ -z "$PORT" ]] && PORT=3333 7 | 8 | curl -sX POST http://127.0.0.1:$PORT/jibri/api/internal/v1.0/notifyConfigChanged 9 | -------------------------------------------------------------------------------- /resources/add_git_pre_commit_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PRE_COMMIT_HOOK_FILE=".git/hooks/pre-commit" 4 | 5 | echo "#!/bin/bash" >> $PRE_COMMIT_HOOK_FILE 6 | echo "echo \"Running linter\"" >> $PRE_COMMIT_HOOK_FILE 7 | echo "mvn antrun:run@ktlint" >> $PRE_COMMIT_HOOK_FILE 8 | 9 | chmod +x $PRE_COMMIT_HOOK_FILE 10 | -------------------------------------------------------------------------------- /resources/debian-package/opt/jitsi/jibri/graceful_shutdown.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONF="/etc/jitsi/jibri/jibri.conf" 4 | 5 | PORT=$(hocon -f $CONF get jibri.api.http.internal-api-port 2>/dev/null || true) 6 | [[ -z "$PORT" ]] && PORT=3333 7 | 8 | curl -sX POST http://127.0.0.1:$PORT/jibri/api/internal/v1.0/gracefulShutdown 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | max_line_length=120 3 | ktlint_code_style = intellij_idea 4 | 5 | # I find trailing commas annoying 6 | ktlint_standard_trailing-comma-on-call-site = disabled 7 | ktlint_standard_trailing-comma-on-declaration-site = disabled 8 | 9 | # This forbids underscores in package names, which we use 10 | ktlint_standard_package-name = disabled 11 | -------------------------------------------------------------------------------- /resources/debian-package/etc/systemd/system/jibri-icewm.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Jibri Window Manager 3 | Requires=jibri-xorg.service 4 | After=jibri-xorg.service 5 | 6 | [Service] 7 | User=jibri 8 | Group=jibri 9 | Environment=DISPLAY=:0 10 | ExecStart=/usr/bin/icewm-session 11 | Restart=on-failure 12 | RestartPreventExitStatus=255 13 | Type=simple 14 | 15 | [Install] 16 | WantedBy=jibri.service 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Reporting security issuess 4 | 5 | We take security very seriously and develop all Jitsi projects to be secure and safe. 6 | 7 | If you find (or simply suspect) a security issue in any of the Jitsi projects, please send us an email to security@jitsi.org. 8 | 9 | **We encourage responsible disclosure for the sake of our users, so please reach out before posting in a public space.** 10 | -------------------------------------------------------------------------------- /resources/finalize_recording.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RECORDINGS_DIR=$1 4 | 5 | echo "This is a dummy finalize script" > /tmp/finalize.out 6 | echo "The script was invoked with recordings directory $RECORDINGS_DIR." >> /tmp/finalize.out 7 | echo "You should put any finalize logic (renaming, uploading to a service" >> /tmp/finalize.out 8 | echo "or storage provider, etc.) in this script" >> /tmp/finalize.out 9 | 10 | exit 0 11 | -------------------------------------------------------------------------------- /resources/debian-package/opt/jitsi/jibri/finalize_sip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LOG_DIR_PATH="/var/log/jitsi/jibri" 4 | LATEST_PJSUA_LOG_FILE="$LOG_DIR_PATH/pjsua.log" 5 | AGGREGATED_PJSUA_LOG_FILE="$LOG_DIR_PATH/pjsua_all.log" 6 | 7 | if [ -f "$LATEST_PJSUA_LOG_FILE" ]; then 8 | echo "Appending pjsua logs from $LATEST_PJSUA_LOG_FILE, to $AGGREGATED_PJSUA_LOG_FILE" 9 | cat "$LATEST_PJSUA_LOG_FILE" >> "$AGGREGATED_PJSUA_LOG_FILE" 10 | fi 11 | -------------------------------------------------------------------------------- /doc/sip_gateway.md: -------------------------------------------------------------------------------- 1 | # SIP Gateway 2 | Currently the XMPP API is the only officially supported API, so this doc will discuss how to configure Jibri to allow SIP gateway calls via the XMPP API. 3 | 4 | To use the SIP gateway functionality of Jibri, you'll need to set the `sip_control_muc` field in `config.json`, it can be found within the `XmppEnvironment` field. Jibri will join this MUC to announce itself of being capable of providing SIP gateway services to Jicofo. 5 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: Jibri 3 | Upstream-Contact: Emil Ivov 4 | Source: https://github.com/jitsi/jibri 5 | 6 | Files: * 7 | Copyright: 2018 Atlassian Pty Ltd 8 | License: Apache-2.0 9 | 10 | License: Apache-2.0 11 | On Debian systems, the full text of the Apache 12 | License version 2 can be found in the file 13 | '/usr/share/common-licenses/Apache-2.0'. 14 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: jibri 2 | Maintainer: dev@jitsi.org 3 | Section: net 4 | Priority: optional 5 | Standards-Version: 3.9.2 6 | Build-Depends: openjdk-11-jdk | openjdk-17-jdk, debhelper (>= 9) 7 | 8 | Package: jibri 9 | Architecture: all 10 | Depends: openjdk-11-jre-headless | openjdk-11-jre | openjdk-17-jre-headless | openjdk-17-jre, ffmpeg, curl, alsa-utils, icewm, xserver-xorg-video-dummy, procps, ruby-hocon 11 | Description: Jibri 12 | Jibri can be used to capture data from a Jitsi Meet conference and record it to a file or stream it to a url 13 | -------------------------------------------------------------------------------- /lib/test-logging.properties: -------------------------------------------------------------------------------- 1 | handlers = java.util.logging.ConsoleHandler 2 | 3 | java.util.logging.ConsoleHandler.level = FINE 4 | java.util.logging.ConsoleHandler.pattern = /var/log/jitsi/jibri/log.%g.txt 5 | java.util.logging.ConsoleHandler.formatter = org.jitsi.utils.logging2.JitsiLogFormatter 6 | java.util.logging.ConsoleHandler.count = 10 7 | java.util.logging.ConsoleHandler.limit = 10000000 8 | 9 | org.jitsi.level = FINE 10 | org.jitsi.jibri.config.level = INFO 11 | 12 | org.glassfish.level = INFO 13 | org.osgi.level = INFO 14 | org.jitsi.xmpp.level = INFO 15 | -------------------------------------------------------------------------------- /resources/debian-package/etc/systemd/system/jibri-xorg.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Jibri Xorg Process 3 | After=network.target 4 | 5 | [Service] 6 | User=jibri 7 | Group=jibri 8 | Environment=DISPLAY=:0 9 | ExecStart=/usr/bin/Xorg -nocursor -noreset +extension RANDR +extension RENDER -logfile /var/log/jitsi/jibri/xorg.log -config /etc/jitsi/jibri/xorg-video-dummy.conf :0 10 | ExecReload=/bin/kill -HUP $MAINPID 11 | KillMode=process 12 | Restart=on-failure 13 | RestartPreventExitStatus=255 14 | Type=simple 15 | 16 | [Install] 17 | WantedBy=jibri.service jibri-icewm.service 18 | -------------------------------------------------------------------------------- /resources/debian-package/etc/systemd/system/jibri.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Jibri Process 3 | Requires=jibri-icewm.service jibri-xorg.service 4 | After=network.target 5 | 6 | [Service] 7 | User=jibri 8 | Group=jibri 9 | PermissionsStartOnly=true 10 | ExecStartPre=sudo -u jibri /usr/bin/google-chrome --timeout=1000 --headless about:blank 11 | ExecStart=/opt/jitsi/jibri/launch.sh 12 | ExecStop=/opt/jitsi/jibri/graceful_shutdown.sh 13 | ExecReload=/opt/jitsi/jibri/reload.sh 14 | Restart=always 15 | RestartPreventExitStatus=255 16 | Type=simple 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /doc/debian_package.md: -------------------------------------------------------------------------------- 1 | # Building the Jibri debian package 2 | Building the debian package has been tested on Ubuntu Xenial. 3 | 4 | ### Steps 5 | * Install the prerequisites: 6 | * `sudo apt install git maven openjdk-8-jdk` 7 | * (`openjdk-8-jdk` specifically is not necessarily required, any java 8 jdk will probably work) 8 | * Clone the repo: 9 | * `git clone https://github.com/jitsi/jibri.git` 10 | * Create the jar: 11 | * `cd jibri` 12 | * `mvn package` 13 | * Call the `create_debian_package_script` and pass it the location of the jar: 14 | * `` export WORKSPACE=`pwd` `` 15 | * `resources/jenkins/release.sh Minor` 16 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/status_checks/CallStatusCheck.kt: -------------------------------------------------------------------------------- 1 | package org.jitsi.jibri.selenium.status_checks 2 | 3 | import org.jitsi.jibri.selenium.SeleniumEvent 4 | import org.jitsi.jibri.selenium.pageobjects.CallPage 5 | 6 | /** 7 | * [CallStatusCheck]s are executed periodically and perform checks via javascripe on the call page to make sure 8 | * everything is running correctly. 9 | */ 10 | interface CallStatusCheck { 11 | /** 12 | * Run a check via [callPage], return a [SeleniumEvent] if something notable has been detected (i.e. an error), 13 | * null if whatever the check is looking for was fine. 14 | */ 15 | fun run(callPage: CallPage): SeleniumEvent? 16 | } 17 | -------------------------------------------------------------------------------- /resources/debian-package/etc/jitsi/jibri/pjsua.config: -------------------------------------------------------------------------------- 1 | --capture-dev=11 2 | --playback-dev=14 3 | --video 4 | --vcapture-dev=1 5 | --no-color 6 | --log-level=5 7 | --app-log-level=5 8 | --auto-update-nat 0 9 | --disable-stun 10 | --no-tcp 11 | --dis-codec GSM 12 | --dis-codec H263 13 | --dis-codec iLBC 14 | --dis-codec G722 15 | --dis-codec speex 16 | --dis-codec pcmu 17 | --dis-codec pcma 18 | --dis-codec opus 19 | --add-codec pcmu 20 | --add-codec pcma 21 | --add-codec speex 22 | --add-codec G722 23 | --add-codec opus 24 | --no-vad 25 | --ec-tail 0 26 | --quality 10 27 | --max-calls=1 28 | --auto-keyframe=30 29 | --no-stderr 30 | --log-file=/var/log/jitsi/jibri/pjsua.log 31 | --id "jibri " 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Before posting, please make sure you check https://community.jitsi.org 4 | 5 | --- 6 | 7 | *This Issue tracker is only for reporting bugs and tracking code related issues.* 8 | 9 | Before posting, please make sure you check community.jitsi.org to see if the same or similar bugs have already been discussed. General questions, installation help, and feature requests can also be posted to community.jitsi.org. 10 | 11 | ## Description 12 | --- 13 | 14 | ## Current behavior 15 | --- 16 | 17 | ## Expected Behavior 18 | --- 19 | 20 | ## Possible Solution 21 | --- 22 | 23 | ## Steps to reproduce 24 | --- 25 | 26 | # Environment details 27 | --- 28 | -------------------------------------------------------------------------------- /resources/debian-package/etc/jitsi/jibri/asoundrc: -------------------------------------------------------------------------------- 1 | pcm.amix { 2 | type dmix 3 | ipc_key 219345 4 | slave.pcm "hw:Loopback,0,0" 5 | } 6 | 7 | pcm.asnoop { 8 | type dsnoop 9 | ipc_key 219346 10 | slave.pcm "hw:Loopback_1,1,0" 11 | } 12 | 13 | pcm.aduplex { 14 | type asym 15 | playback.pcm "amix" 16 | capture.pcm "asnoop" 17 | } 18 | 19 | pcm.bmix { 20 | type dmix 21 | ipc_key 219347 22 | slave.pcm "hw:Loopback_1,0,0" 23 | } 24 | 25 | pcm.bsnoop { 26 | type dsnoop 27 | ipc_key 219348 28 | slave.pcm "hw:Loopback,1,0" 29 | } 30 | 31 | pcm.bduplex { 32 | type asym 33 | playback.pcm "bmix" 34 | capture.pcm "bsnoop" 35 | } 36 | 37 | pcm.pjsua { 38 | type plug 39 | slave.pcm "bduplex" 40 | } 41 | 42 | pcm.!default { 43 | type plug 44 | slave.pcm "aduplex" 45 | } 46 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/service/JibriServiceFinalizer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2021 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.service 18 | 19 | interface JibriServiceFinalizer { 20 | 21 | fun doFinalize() 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/util/BrowserFileHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.selenium.util 18 | 19 | import java.util.logging.FileHandler 20 | 21 | class BrowserFileHandler : FileHandler() 22 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v4 21 | with: 22 | distribution: temurin 23 | java-version: 11 24 | cache: maven 25 | - name: Build with Maven 26 | run: ./resources/build.sh -Pcoverage 27 | - name: Upload coverage report 28 | uses: codecov/codecov-action@v4 29 | with: 30 | token: ${{ secrets.CODECOV_TOKEN }} 31 | 32 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/sipgateway/pjsua/util/PjsuaFileHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.sipgateway.pjsua.util 18 | 19 | import java.util.logging.FileHandler 20 | 21 | class PjsuaFileHandler : FileHandler() 22 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/metrics/JibriMetricsContainer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2024-Present 8x8, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.metrics 18 | 19 | import org.jitsi.metrics.MetricsContainer 20 | 21 | object JibriMetricsContainer : MetricsContainer(namespace = "jitsi_jibri") 22 | -------------------------------------------------------------------------------- /doc/internal_http_api.md: -------------------------------------------------------------------------------- 1 | # Jibri Internal HTTP API 2 | At startup, Jibri reads from a configuration file to determine which (if any) xmpp enviroments to connect to and to read some other configuration data. Jibri takes a simple approach of only reading this file at startup, so if changes are made to the file and you want Jibri to read them, Jibri needs to be restarted. Obviously one would prefer not to restart Jibri while it's busy, so Jibri has an internal API to notify it of config file changes. When the API is called, Jibri will schedule a shutdown for the next time it's idle. It is up for whatever is managing Jibri to restart it. 3 | 4 | ##### URL 5 | `/jibri/api/internal/v1.0/notifyConfigChanged` 6 | ##### Method 7 | `POST` 8 | ##### URL Params 9 | None 10 | ##### Data Params 11 | None 12 | ##### Response 13 | If Jibri is currently idle, no respose will be sent as Jibri will shutdown immediately. If Jibri is currently busy, it will respond with a `200` 14 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/RandomUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2024 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.jitsi.jibri.util 17 | 18 | import kotlin.random.Random 19 | 20 | val alphaNum = ('a'..'z') + ('0'..'9') 21 | fun randomAlphaNum(len: Int): String { 22 | return List(len) { alphaNum[Random.nextInt(0, alphaNum.size)] }.joinToString("") 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/status_checks/LocalParticipantKickedStatusCheck.kt: -------------------------------------------------------------------------------- 1 | package org.jitsi.jibri.selenium.status_checks 2 | 3 | import org.jitsi.jibri.selenium.SeleniumEvent 4 | import org.jitsi.jibri.selenium.pageobjects.CallPage 5 | import org.jitsi.utils.logging2.Logger 6 | import org.jitsi.utils.logging2.createChildLogger 7 | 8 | class LocalParticipantKickedStatusCheck( 9 | parentLogger: Logger 10 | ) : CallStatusCheck { 11 | private val logger = createChildLogger(parentLogger) 12 | 13 | init { 14 | logger.info("Starting local participant kicked out call check") 15 | } 16 | 17 | override fun run(callPage: CallPage): SeleniumEvent? { 18 | return if (callPage.isLocalParticipantKicked()) { 19 | logger.info("Local participant was kicked, returning LocalParticipantKicked event") 20 | SeleniumEvent.LocalParticipantKicked 21 | } else { 22 | null 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/sink/impl/StreamSink.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.sink.impl 19 | 20 | import org.jitsi.jibri.sink.Sink 21 | 22 | /** 23 | * [StreamSink] represents a sink which will write to a network stream 24 | */ 25 | class StreamSink(val url: String) : Sink { 26 | override val path: String = url 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/status/ComponentBusyStatus.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.status 18 | 19 | enum class ComponentBusyStatus { 20 | BUSY, 21 | IDLE, 22 | 23 | /** 24 | * This Jibri has exhausted its 'use' and needs action 25 | * (e.g. a restart) before it can be used again 26 | */ 27 | EXPIRED 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/service/Errors.kt: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* 4 | * Copyright @ 2018 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | @file:Suppress("ktlint:standard:filename") 20 | 21 | package org.jitsi.jibri.service 22 | 23 | import org.jitsi.jibri.error.JibriError 24 | import org.jitsi.jibri.status.ErrorScope 25 | 26 | object ErrorSettingPresenceFields : JibriError(ErrorScope.SESSION, "Unable to set presence values") 27 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/capture/ffmpeg/util/FfmpegFileHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.capture.ffmpeg.util 19 | 20 | import java.util.logging.FileHandler 21 | 22 | /** 23 | * A distinct [FileHandler] so that we can configure the file 24 | * Ffmpeg logs to separately in the logging config 25 | */ 26 | class FfmpegFileHandler : FileHandler() 27 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/error/JibriError.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.error 18 | 19 | import org.jitsi.jibri.status.ErrorScope 20 | 21 | open class JibriError( 22 | val scope: ErrorScope, 23 | val detail: String 24 | ) { 25 | open fun shouldRetry(): Boolean = true 26 | override fun toString(): String = "Error: ${this::class.java.simpleName} $scope $detail" 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/pageobjects/HomePage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.selenium.pageobjects 19 | 20 | import org.openqa.selenium.remote.RemoteWebDriver 21 | 22 | /** 23 | * This class represents a page object for the home page (i.e. on the domain 24 | * but not in a call) for a jitsi-meet server 25 | */ 26 | class HomePage(driver: RemoteWebDriver) : AbstractPageObject(driver) 27 | -------------------------------------------------------------------------------- /doc/apis.md: -------------------------------------------------------------------------------- 1 | # Jibri APIs 2 | Jibri has two types of APIs: 3 | 1. One is for 'external' use and is used to control stopping and starting of Jibri services, as well as querying Jibri's health. 4 | 1. The other is 'internal' and is used to notify Jibri that there has been a change to its config file. 5 | 6 | There is an HTTP implementation of the 'internal' API which lives in `InternalHttpApi.kt`, documentation for it can be found [here](internal_http_api.md). 7 | 8 | The external API has both XMPP and HTTP implementations, however the HTTP implementation is not fully developed (though it's close to complete functionality). Detailed documentation for the XMPP API can be found [here](xmpp_api.md), and for the HTTP API [here](http_api.md). 9 | 10 | In general, the 'external' APIs boil down to the following available actions: 11 | 1. Start a service 12 | 1. Stop a service 13 | 1. Query the current 'status' of Jibri (is it busy or is it available to start a new service) 14 | 15 | At this time, Jibri only runs a single service at a time, so if it has one running it will consider itself "busy". 16 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/sipgateway/pjsua/util/Error.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.sipgateway.pjsua.util 18 | 19 | import org.jitsi.jibri.error.JibriError 20 | import org.jitsi.jibri.status.ErrorScope 21 | 22 | object RemoteSipClientBusy : JibriError(ErrorScope.SESSION, "Remote side busy") 23 | 24 | class PjsuaExitedPrematurely(exitCode: Int) : 25 | JibriError(ErrorScope.SESSION, "Pjsua exited with code $exitCode") 26 | -------------------------------------------------------------------------------- /doc/xmpp_api.md: -------------------------------------------------------------------------------- 1 | # Jibri XMPP API 2 | 3 | Jibri can be configured to join multiple XMPP environments which can be used as a control surface for leveraging Jibri services. The configuration for these environments lives in config.json (a documented sample of config.json can be seen in the code [here](TODO)). For a given XMPP environment, Jibri will: 4 | * Connect to the provided XMPP domain on the provided XMPP host 5 | * Login to a given auth domain with the given credentials 6 | * Join a MUC on the given MUC domain with the given MUC jid using the given MUC nickname 7 | * Publish its status (defined by the status packet extension [here](https://github.com/jitsi/jitsi/blob/master/src/net/java/sip/communicator/impl/protocol/jabber/extensions/jibri/JibriStatusPacketExt.java) 8 | 9 | At this point it will await an IQ message (defined by the custom IQ [here](https://github.com/jitsi/jitsi/blob/master/src/net/java/sip/communicator/impl/protocol/jabber/extensions/jibri/JibriIq.java)) asking it to start or stop a given service. Whenever Jibri's status changes, it will send a new presence to reflect its current state. 10 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/OsDetector.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.util 18 | 19 | enum class OsType { 20 | MAC, 21 | LINUX, 22 | UNSUPPORTED 23 | } 24 | 25 | class OsDetector { 26 | fun getOsType(): OsType { 27 | return when (System.getProperty("os.name")) { 28 | "Mac OS X" -> OsType.MAC 29 | "Linux" -> OsType.LINUX 30 | else -> OsType.UNSUPPORTED 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/Errors.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.selenium 18 | 19 | import org.jitsi.jibri.error.JibriError 20 | import org.jitsi.jibri.status.ErrorScope 21 | 22 | object FailedToJoinCall : JibriError(ErrorScope.SESSION, "Failed to join the call") 23 | object ChromeHung : JibriError(ErrorScope.SESSION, "Chrome hung") 24 | object NoMediaReceived : JibriError(ErrorScope.SESSION, "No media received") 25 | object IceFailed : JibriError(ErrorScope.SESSION, "ICE failed") 26 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/sipgateway/pjsua/util/OutputParser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.sipgateway.pjsua.util 18 | 19 | import org.jitsi.jibri.util.ProcessWrapper 20 | 21 | enum class PjsuaStatus { 22 | HEALTHY, 23 | EXITED 24 | } 25 | 26 | fun ProcessWrapper.getPjsuaStatus(): Pair { 27 | val mostRecentLine = getMostRecentLine() 28 | val status = if (isAlive) PjsuaStatus.HEALTHY else PjsuaStatus.EXITED 29 | return Pair(status, mostRecentLine) 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/NameableThreadFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import java.util.concurrent.ThreadFactory 21 | 22 | /** 23 | * A helper to create a [ThreadFactory] where all threads will 24 | * be given [name] 25 | */ 26 | class NameableThreadFactory(private val name: String) : ThreadFactory { 27 | private var threadNum = 1 28 | override fun newThread(r: Runnable?): Thread { 29 | return Thread(r, "$name-${threadNum++}") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/helpers/SeleniumMockHelper.kt: -------------------------------------------------------------------------------- 1 | package org.jitsi.jibri.helpers 2 | 3 | import io.mockk.Runs 4 | import io.mockk.every 5 | import io.mockk.just 6 | import io.mockk.mockk 7 | import org.jitsi.jibri.error.JibriError 8 | import org.jitsi.jibri.selenium.JibriSelenium 9 | import org.jitsi.jibri.status.ComponentState 10 | 11 | class SeleniumMockHelper { 12 | private val eventHandlers = mutableListOf<(ComponentState) -> Boolean>() 13 | 14 | val mock: JibriSelenium = mockk(relaxed = true) { 15 | every { addTemporaryHandler(capture(eventHandlers)) } just Runs 16 | every { addStatusHandler(captureLambda()) } answers { 17 | // This behavior mimics what's done in StatusPublisher#addStatusHandler 18 | eventHandlers.add { 19 | lambda<(ComponentState) -> Unit>().captured(it) 20 | true 21 | } 22 | } 23 | } 24 | 25 | fun startSuccessfully() { 26 | eventHandlers.forEach { it(ComponentState.Running) } 27 | } 28 | 29 | fun error(error: JibriError) { 30 | eventHandlers.forEach { it(ComponentState.Error(error)) } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /doc/development.md: -------------------------------------------------------------------------------- 1 | # Jibri Development 2 | ## Style 3 | Code should follow the Kotlin style guide. This style is enforced with ktlint in the project itself, linting can be executed by running `mvn verify` (which will build, run tests and lint) or `mvn antrun:run@ktlint` (which will run just the linting). Jibri is a Kotlin codebase, so Kotlin should be used for development (save for extreme circumstances where falling back to Java is acceptable). The linter can be run automatically by installing a pre-commit script, you can run [this script](resources/add_git_pre_commit_script.sh) to create/install this hook automatically. 4 | 5 | ## Versioning 6 | Jibri uses (annotated) tagged versions and follows [semantic versioning](https://semver.org/). Adding an annotated tag is done as follows: 7 | ``` 8 | git tag -a v1.4 -m "my version 1.4" 9 | ``` 10 | Tags are not pushed by default when doing `git push`, but a tag can be pushed like a remote branch: 11 | ``` 12 | git push origin v1.4 13 | ``` 14 | NOTE: Tagging should not be done as part of a PR, it will be handled separately. 15 | 16 | More can be read about git tagging [here](https://git-scm.com/book/en/v2/Git-Basics-Tagging). 17 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/ProcessFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.util 18 | 19 | import org.jitsi.utils.logging2.Logger 20 | 21 | class ProcessFactory { 22 | fun createProcess( 23 | command: List, 24 | parentLogger: Logger, 25 | environment: Map = mapOf(), 26 | processBuilder: ProcessBuilder = ProcessBuilder() 27 | ): ProcessWrapper { 28 | return ProcessWrapper(command, parentLogger, environment, processBuilder) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/webhooks/v1/Events.kt: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* 4 | * Copyright @ 2018 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | @file:Suppress("ktlint:standard:filename") 20 | 21 | package org.jitsi.jibri.webhooks.v1 22 | 23 | import org.jitsi.jibri.status.JibriSessionStatus 24 | import org.jitsi.jibri.status.JibriStatus 25 | 26 | sealed class JibriEvent(val jibriId: String) { 27 | class HealthEvent(jibriId: String, val status: JibriStatus) : JibriEvent(jibriId) 28 | class SessionEvent(jibriId: String, val session: JibriSessionStatus) : JibriEvent(jibriId) 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/FileUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.util 18 | 19 | import org.jitsi.utils.logging2.Logger 20 | import java.nio.file.Files 21 | import java.nio.file.Path 22 | 23 | fun createIfDoesNotExist(path: Path, logger: Logger? = null): Boolean { 24 | if (!Files.exists(path)) { 25 | try { 26 | Files.createDirectories(path) 27 | } catch (e: Exception) { 28 | logger?.error("Error creating directory", e) 29 | return false 30 | } 31 | } 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /resources/jenkins/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # This script should be run after the build/tests have successfully completed 5 | 6 | # Prune any local tags that aren't on the remote 7 | git fetch --prune origin "+refs/tags/*:refs/tags/*" 8 | 9 | cd $WORKSPACE 10 | 11 | # Let's get version from maven 12 | MVNVER=`xmllint --xpath "/*[local-name()='project']/*[local-name()='version']/text()" pom.xml` 13 | TAG_NAME="v${MVNVER/-SNAPSHOT/}" 14 | echo "Current tag name: $TAG_NAME" 15 | 16 | if ! git rev-parse $TAG_NAME >/dev/null 2>&1 17 | then 18 | git tag -a $TAG_NAME -m "Tagged automatically by Jenkins" 19 | git push origin $TAG_NAME 20 | else 21 | echo "Tag: $TAG_NAME already exists." 22 | fi 23 | 24 | VERSION_FULL=`git describe --match "v[0-9\.]*" --long` 25 | echo "Full version: ${VERSION_FULL}" 26 | 27 | VERSION=${VERSION_FULL:1} 28 | echo "Package version: ${VERSION}" 29 | 30 | REV=$(git log --pretty=format:'%h' -n 1) 31 | 32 | # bulding the debian package expects the file target/jibri.jar 33 | mv target/jibri-${MVNVER}-jar-with-dependencies.jar target/jibri.jar 34 | 35 | dch -v "${VERSION}-1" "Built from git. $REV" 36 | dch -D unstable -r "" 37 | 38 | dpkg-buildpackage -A -rfakeroot -us -uc 39 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/sink/Sink.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.sink 19 | 20 | /** 21 | * [Sink] describes a class which data will be 'written to'. It contains 22 | * a destination (via [path]), a format (via [format]) and a set 23 | * of options which each [Sink] implementation may provide. 24 | * TODO: currently this is modeled as generic, but really it's an 25 | * "FfmpegSink", so maybe it should be named as such? 26 | */ 27 | interface Sink { 28 | /** 29 | * The path to which this [Sink] has been designated to write 30 | */ 31 | val path: String 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/NotifyingStateMachine.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.util 18 | 19 | import org.jitsi.jibri.status.ComponentState 20 | 21 | abstract class NotifyingStateMachine { 22 | private val stateTranstionHandlers = mutableListOf<(ComponentState, ComponentState) -> Unit>() 23 | 24 | protected fun notify(fromState: ComponentState, toState: ComponentState) { 25 | stateTranstionHandlers.forEach { handler -> 26 | handler(fromState, toState) 27 | } 28 | } 29 | 30 | fun onStateTransition(handler: (ComponentState, ComponentState) -> Unit) { 31 | stateTranstionHandlers.add(handler) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/capture/Capturer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.capture 19 | 20 | import org.jitsi.jibri.sink.Sink 21 | 22 | class UnsupportedOsException(override var message: String = "Jibri does not support this OS") : Exception() 23 | class UnsupportedSinkTypeException(sink: Sink) : Exception("Unsupported sink type: ${sink::class.simpleName}") 24 | 25 | /** 26 | * [Capturer] represents a process which will capture media. 27 | */ 28 | interface Capturer { 29 | /** 30 | * Start the capturer with the given [Sink]. 31 | */ 32 | fun start(sink: Sink) 33 | 34 | /** 35 | * Stop the capturer 36 | */ 37 | fun stop() 38 | } 39 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /debian/jibri.install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/dh-exec 2 | 3 | resources/debian-package/etc/jitsi/jibri/asoundrc etc/jitsi/jibri 4 | resources/debian-package/etc/jitsi/jibri/jibri.conf etc/jitsi/jibri 5 | resources/debian-package/etc/jitsi/jibri/icewm.preferences etc/jitsi/jibri 6 | resources/debian-package/etc/jitsi/jibri/xorg-video-dummy.conf etc/jitsi/jibri 7 | resources/debian-package/etc/jitsi/jibri/pjsua.config etc/jitsi/jibri 8 | 9 | resources/debian-package/etc/systemd/system/jibri-icewm.service etc/systemd/system 10 | resources/debian-package/etc/systemd/system/jibri.service etc/systemd/system 11 | resources/debian-package/etc/systemd/system/jibri-xorg.service etc/systemd/system 12 | 13 | resources/debian-package/opt/jitsi/jibri/launch.sh opt/jitsi/jibri 14 | resources/debian-package/opt/jitsi/jibri/graceful_shutdown.sh opt/jitsi/jibri 15 | resources/debian-package/opt/jitsi/jibri/wait_graceful_shutdown.sh opt/jitsi/jibri 16 | resources/debian-package/opt/jitsi/jibri/reload.sh opt/jitsi/jibri 17 | resources/debian-package/opt/jitsi/jibri/shutdown.sh opt/jitsi/jibri 18 | resources/debian-package/opt/jitsi/jibri/collect-dump-logs.sh opt/jitsi/jibri 19 | resources/debian-package/opt/jitsi/jibri/pjsua.sh opt/jitsi/jibri 20 | resources/debian-package/opt/jitsi/jibri/finalize_sip.sh opt/jitsi/jibri 21 | 22 | target/jibri.jar opt/jitsi/jibri 23 | lib/logging.properties etc/jitsi/jibri 24 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/metrics/StatsConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2024-Present 8x8, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.metrics 18 | 19 | import org.jitsi.jibri.config.Config 20 | import org.jitsi.metaconfig.config 21 | 22 | object StatsConfig { 23 | val enableStatsD: Boolean by config { 24 | "JibriConfig::enableStatsD" { Config.legacyConfigSource.enabledStatsD!! } 25 | "jibri.stats.enable-stats-d".from(Config.configSource) 26 | } 27 | 28 | val statsdHost: String by config { 29 | "jibri.stats.host".from(Config.configSource) 30 | } 31 | 32 | val statsdPort: Int by config { 33 | "jibri.stats.port".from(Config.configSource) 34 | } 35 | 36 | val enablePrometheus: Boolean by config { 37 | "jibri.stats.prometheus.enabled".from(Config.configSource) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/capture/ffmpeg/Errors.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.capture.ffmpeg 18 | 19 | import org.jitsi.jibri.error.JibriError 20 | import org.jitsi.jibri.status.ErrorScope 21 | 22 | open class FfmpegError(scope: ErrorScope, detail: String) : JibriError(scope, detail) 23 | object FfmpegFailedToStart : FfmpegError(ErrorScope.SYSTEM, "Ffmpeg failed to start") 24 | class FfmpegUnexpectedSignal(outputLine: String) : FfmpegError(ErrorScope.SESSION, outputLine) 25 | class BadRtmpUrl(outputLine: String) : FfmpegError(ErrorScope.SESSION, outputLine) { 26 | override fun shouldRetry(): Boolean = false 27 | } 28 | class BrokenPipe(outputLine: String) : FfmpegError(ErrorScope.SESSION, outputLine) 29 | class QuitUnexpectedly(outputLine: String) : FfmpegError(ErrorScope.SESSION, outputLine) 30 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/pageobjects/AbstractPageObject.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.selenium.pageobjects 19 | 20 | import org.jitsi.utils.logging2.createLogger 21 | import org.openqa.selenium.remote.RemoteWebDriver 22 | import kotlin.time.measureTime 23 | 24 | /** 25 | * [AbstractPageObject] is a page object class containing logic common to 26 | * all page object instances 27 | */ 28 | open class AbstractPageObject(protected val driver: RemoteWebDriver) { 29 | private val logger = createLogger() 30 | 31 | open fun visit(url: String): Boolean { 32 | logger.info("Visiting url $url") 33 | 34 | val totalTime = measureTime { 35 | driver.get(url) 36 | } 37 | 38 | logger.info("Waited $totalTime for driver to load page") 39 | return true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/webhooks/v1/README.md: -------------------------------------------------------------------------------- 1 | # Jibri 'Webhooks' 2 | 3 | Jibri supports being configured with a list of base URLs on which it will hit certain endpoints with data. These are 4 | not "true" webhooks as the endpoints are hard-coded, instead Jibri defines a "contract" which it expect a subscriber 5 | to implement at the given base URL. Information about this contract is below. 6 | 7 | ### Status updates 8 | Jibri pushes status updates consisting of its "busy status" (whether it is busy or idle) and its health. These updates 9 | are sent periodically every minute and every time the status changes. 10 | 11 | URL: `/v1/status` 12 | 13 | method: `POST` 14 | 15 | Data constraints: 16 | ```$json 17 | { 18 | "jibriId":"[String]", 19 | "status":{ 20 | "busyStatus":"[a String value of ComponentBusyStatus: (BUSY|IDLE|EXPIRED)]", 21 | "health": { 22 | "healthStatus":"[a String value of ComponentHealthStatus: (HEALTHY|UNHEALTHY)", 23 | "details": [A map of String to ComponentHealthDetails giving optional details of sub-component's health] 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | Data example: 30 | ```$json 31 | { 32 | "jibriId":"jibri_id", 33 | "status":{ 34 | "busyStatus":"IDLE", 35 | "health":{ 36 | "healthStatus":"HEALTHY", 37 | "details":{} 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | Success response: 44 | 45 | Code: `200 OK` 46 | 47 | Data: body will be ignored 48 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/RegexUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | // Regex definitions for parsing an ffmpeg output line 21 | const val DIGIT = """\d""" 22 | const val ONE_OR_MORE_DIGITS = "$DIGIT+" 23 | 24 | // "1" is treated as a valid decimal (the decimal point and any trailing numbers are not required) 25 | const val DECIMAL = """$ONE_OR_MORE_DIGITS(\.$ONE_OR_MORE_DIGITS)?""" 26 | const val STRING = """[a-zA-Z]+""" 27 | const val DATA_SIZE = "$ONE_OR_MORE_DIGITS$STRING" 28 | const val TIMESTAMP = """$ONE_OR_MORE_DIGITS\:$ONE_OR_MORE_DIGITS\:$ONE_OR_MORE_DIGITS\.$ONE_OR_MORE_DIGITS""" 29 | const val BITRATE = """$DECIMAL$STRING\/$STRING""" 30 | const val SPEED = "${DECIMAL}x" 31 | const val SPACE = """\s""" 32 | const val NON_SPACE = """\S""" 33 | const val ZERO_OR_MORE_SPACES = "$SPACE*" 34 | const val ONE_OR_MORE_NON_SPACES = "$NON_SPACE+" 35 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/status/ComponentHealthStatus.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.status 19 | 20 | /** 21 | * A simple model of whether or not a component is healthy or not. This enum is used to represent health at 22 | * multiple levels: sub-components within Jibri and Jibri's overall health. 23 | */ 24 | enum class ComponentHealthStatus { 25 | HEALTHY, 26 | UNHEALTHY; 27 | 28 | /** 29 | * Performs a logical 'and' of statuses, where: 30 | * HEALTHY.and(HEALTHY) -> HEALTHY 31 | * HEALTHY.and(UNHEALTHY) -> UNHEALTHY 32 | * UNHEALTHY.and(HEALTHY) -> UNHEALTHY 33 | * UNHEALTHY.and(UNHEALTHY) -> UNHEALTHY 34 | */ 35 | fun and(other: ComponentHealthStatus): ComponentHealthStatus { 36 | if (this == HEALTHY && other == HEALTHY) { 37 | return HEALTHY 38 | } 39 | return UNHEALTHY 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/health/JibriHealth.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.health 19 | 20 | import com.fasterxml.jackson.annotation.JsonAutoDetect 21 | import com.fasterxml.jackson.annotation.JsonInclude 22 | import org.jitsi.jibri.status.JibriStatus 23 | 24 | @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) 25 | data class EnvironmentContext( 26 | private val name: String 27 | ) 28 | 29 | @JsonInclude(JsonInclude.Include.NON_NULL) 30 | @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) 31 | data class JibriHealth( 32 | /** 33 | * Whether or not this Jibri is "busy". See [JibriManager#busy] 34 | */ 35 | private val status: JibriStatus, 36 | /** 37 | * Context for the environment Jibri is currently active on 38 | * (only present if [busy] is true) 39 | */ 40 | private val environmentContext: EnvironmentContext? = null 41 | ) 42 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/selenium/status_checks/LocalParticipantKickedStatusCheckTest.kt: -------------------------------------------------------------------------------- 1 | package org.jitsi.jibri.selenium.status_checks 2 | 3 | import io.kotest.core.spec.IsolationMode 4 | import io.kotest.core.spec.style.ShouldSpec 5 | import io.kotest.matchers.shouldBe 6 | import io.mockk.every 7 | import io.mockk.mockk 8 | import org.jitsi.jibri.selenium.SeleniumEvent 9 | import org.jitsi.jibri.selenium.pageobjects.CallPage 10 | import org.jitsi.utils.logging2.Logger 11 | 12 | class LocalParticipantKickedStatusCheckTest : ShouldSpec() { 13 | override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf 14 | 15 | private val callPage: CallPage = mockk() 16 | private val logger: Logger = mockk(relaxed = true) 17 | 18 | private val check = LocalParticipantKickedStatusCheck(logger) 19 | 20 | init { 21 | context("when local participant is kicked") { 22 | every { callPage.isLocalParticipantKicked() } returns true 23 | context("the check") { 24 | should("return LocalParticipantKicked immediately") { 25 | check.run(callPage) shouldBe SeleniumEvent.LocalParticipantKicked 26 | } 27 | } 28 | } 29 | context("when local participant is not kicked") { 30 | every { callPage.isLocalParticipantKicked() } returns false 31 | context("the check") { 32 | should("returns null state") { 33 | check.run(callPage) shouldBe null 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/KotestProjectConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri 18 | 19 | import io.kotest.core.config.AbstractProjectConfig 20 | import io.kotest.core.extensions.Extension 21 | import io.kotest.extensions.junitxml.JunitXmlReporter 22 | 23 | class KotestProjectConfig : AbstractProjectConfig() { 24 | override fun extensions(): List = listOf( 25 | /* 26 | * The JunitXmlReporter writes a junit5 compatible unit test output 27 | * but with the full scope of tests as their name, unlike the default 28 | * one which only includes the 'should' block from kotest tests as 29 | * the name. See https://kotest.io/docs/extensions/junit_xml.html. 30 | */ 31 | JunitXmlReporter( 32 | includeContainers = false, 33 | useTestPathAsName = true, 34 | outputDir = "full-test-name-test-results" 35 | ) 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | die () { 4 | echo >&2 "$@" 5 | exit 1 6 | } 7 | 8 | case "$1" in 9 | configure) 10 | 11 | if ! getent group jitsi > /dev/null ; then 12 | groupadd jitsi 13 | fi 14 | 15 | if ! getent passwd jibri > /dev/null ; then 16 | echo "Creating jibri user and group" 17 | # Create the Jibri user and group 18 | useradd --system --create-home jibri 19 | echo "Done creating jibri user and group" 20 | usermod -a -G jibri,jitsi,audio,video jibri 21 | echo "jibri user added to audio, video and jibri groups" 22 | fi 23 | 24 | # Make the directory for the logs and set the permissions 25 | mkdir -p /var/log/jitsi/jibri 26 | chgrp jitsi /var/log/jitsi/jibri 27 | chmod g+w /var/log/jitsi/jibri 28 | 29 | # Move the asoundrc file to the jibri home directory 30 | if [ -f /etc/jitsi/jibri/asoundrc ] ; then 31 | mv /etc/jitsi/jibri/asoundrc /home/jibri/.asoundrc 32 | fi 33 | 34 | # Make the directory for the icewm preferences and move the preferences file into it 35 | if [ -f /etc/jitsi/jibri/icewm.preferences ] ; then 36 | mkdir -p /home/jibri/.icewm 37 | mv /etc/jitsi/jibri/icewm.preferences /home/jibri/.icewm/preferences 38 | fi 39 | ;; 40 | 41 | abort-upgrade|abort-remove|abort-deconfigure) 42 | ;; 43 | 44 | *) 45 | echo "postinst called with unknown argument \`$1'" >&2 46 | exit 1 47 | ;; 48 | esac 49 | 50 | exit 0 51 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/metrics/StatsDEvents.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.metrics 18 | 19 | internal const val ASPECT_START = "start" 20 | internal const val ASPECT_STOP = "stop" 21 | internal const val ASPECT_BUSY = "busy" 22 | internal const val ASPECT_ERROR = "error" 23 | 24 | internal const val XMPP_CONNECTED = "xmpp-connected" 25 | internal const val XMPP_RECONNECTING = "xmpp-reconnecting" 26 | internal const val XMPP_RECONNECTION_FAILED = "xmpp-reconnection-failed" 27 | internal const val XMPP_PING_FAILED = "xmpp-ping-failed" 28 | internal const val XMPP_CLOSED = "xmpp-closed" 29 | internal const val XMPP_CLOSED_ON_ERROR = "xmpp-closed-on-error" 30 | 31 | // A recording session was stopped because XMPP disconnected. 32 | internal const val STOPPED_ON_XMPP_CLOSED = "stopped-on-xmpp-closed" 33 | 34 | internal const val TAG_SERVICE_RECORDING = "recording" 35 | internal const val TAG_SERVICE_LIVE_STREAM = "live_stream" 36 | internal const val TAG_SERVICE_SIP_GATEWAY = "sip_gateway" 37 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/status/ComponentStates.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.status 18 | 19 | import org.jitsi.jibri.error.JibriError 20 | 21 | sealed class ComponentState { 22 | object StartingUp : ComponentState() { 23 | override fun toString(): String = "Starting up" 24 | } 25 | object Running : ComponentState() { 26 | override fun toString(): String = "Running" 27 | } 28 | class Error(val error: JibriError) : ComponentState() { 29 | override fun toString(): String = error.toString() 30 | } 31 | object Finished : ComponentState() { 32 | override fun toString(): String = "Finished" 33 | } 34 | } 35 | 36 | enum class ErrorScope { 37 | /** 38 | * [SESSION] errors are errors which only affect the current session. A session error still leaves Jibri as a 39 | * whole 'healthy' 40 | */ 41 | SESSION, 42 | 43 | /** 44 | * [SYSTEM] errors are unrecoverable, and will put Jibri in an unhealthy state 45 | */ 46 | SYSTEM 47 | } 48 | -------------------------------------------------------------------------------- /resources/debian-package/opt/jitsi/jibri/collect-dump-logs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # script that creates an archive in current folder 4 | # containing the heap and thread dump and the current log file 5 | 6 | JAVA_HEAPDUMP_PATH="/tmp/java_*.hprof" 7 | STAMP=`date +%Y-%m-%d-%H%M` 8 | jibri_USER="jibri" 9 | jibri_UID=`id -u $jibri_USER` 10 | RUNNING="" 11 | unset PID 12 | 13 | #Find any crashes in /var/crash from our user in the past 20 minutes, if they exist 14 | CRASH_FILES=$(find /var/crash -name '*.crash' -uid $jibri_UID -mmin -20 -type f) 15 | PID=$(systemctl show -p MainPID jibri 2>/dev/null | cut -d= -f2) 16 | 17 | if [ ! -z $PID ]; then 18 | ps -p $PID | grep -q java 19 | [ $? -eq 0 ] && RUNNING="true" 20 | fi 21 | if [ ! -z $RUNNING ]; then 22 | echo "jibri pid $PID" 23 | THREADS_FILE="/tmp/stack-${STAMP}-${PID}.threads" 24 | HEAP_FILE="/tmp/heap-${STAMP}-${PID}.bin" 25 | sudo -u $jibri_USER jstack ${PID} > ${THREADS_FILE} 26 | sudo -u $jibri_USER jmap -dump:live,format=b,file=${HEAP_FILE} ${PID} 27 | tar zcvf jibri-dumps-${STAMP}-${PID}.tgz ${THREADS_FILE} ${HEAP_FILE} ${CRASH_FILES} /var/log/jitsi/jibri/* /tmp/hs_err_* 28 | rm ${HEAP_FILE} ${THREADS_FILE} 29 | else 30 | ls $JAVA_HEAPDUMP_PATH >/dev/null 2>&1 31 | if [ $? -eq 0 ]; then 32 | echo "jibri not running, but previous heap dump found." 33 | tar zcvf jibri-dumps-${STAMP}-crash.tgz $JAVA_HEAPDUMP_PATH ${CRASH_FILES} /var/log/jitsi/jibri/* /tmp/hs_err_* 34 | rm ${JAVA_HEAPDUMP_PATH} 35 | else 36 | echo "jibri not running, no previous dump found. Including logs only." 37 | tar zcvf jibri-dumps-${STAMP}-crash.tgz ${CRASH_FILES} /var/log/jitsi/jibri/* /tmp/hs_err_* 38 | fi 39 | fi 40 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/TaskPools.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.util 18 | 19 | import java.util.concurrent.ExecutorService 20 | import java.util.concurrent.Executors 21 | import java.util.concurrent.ScheduledExecutorService 22 | 23 | /** 24 | * Stores task pools to be used for various tasks at a global level. Globals are not great 25 | * for lots of reasons, but I think for the uses here they are appropriate. Plus, the 26 | * fact that the variables here are 'vars' allows them to be overwritten by tests if 27 | * they want to sub in mock executors. 28 | */ 29 | class TaskPools { 30 | companion object { 31 | val DefaultIoPool: ExecutorService = 32 | Executors.newCachedThreadPool(NameableThreadFactory("IO Pool")) 33 | val DefaultRecurringTaskPool: ScheduledExecutorService = 34 | Executors.newSingleThreadScheduledExecutor(NameableThreadFactory("Recurring Tasks Pool")) 35 | 36 | var ioPool: ExecutorService = DefaultIoPool 37 | var recurringTasksPool: ScheduledExecutorService = DefaultRecurringTaskPool 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/extensions/SchedulerExecutorServiceExts.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util.extensions 19 | 20 | import java.util.concurrent.ScheduledExecutorService 21 | import java.util.concurrent.ScheduledFuture 22 | import java.util.concurrent.TimeUnit 23 | 24 | /** 25 | * A version of [ScheduledExecutorService.scheduleAtFixedRate] that takes 26 | * a lambda instead of requiring a [Runnable] (and moves it to the last 27 | * argument) 28 | */ 29 | fun ScheduledExecutorService.scheduleAtFixedRate( 30 | period: Long, 31 | unit: TimeUnit, 32 | delay: Long = 0, 33 | action: () -> Unit 34 | ): ScheduledFuture<*> { 35 | return this.scheduleAtFixedRate(action, delay, period, unit) 36 | } 37 | 38 | /** 39 | * A version of [ScheduledExecutorService.schedule] that takes a lambda 40 | * instead of requiring a [Runnable] (and moves it to the last argument) 41 | */ 42 | fun ScheduledExecutorService.schedule( 43 | delay: Long = 0, 44 | unit: TimeUnit = TimeUnit.SECONDS, 45 | action: () -> Unit 46 | ): ScheduledFuture<*> { 47 | return this.schedule(action, delay, unit) 48 | } 49 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/helpers/FinalizeMockHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2021 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.helpers 18 | 19 | import io.mockk.every 20 | import io.mockk.mockk 21 | import org.jitsi.jibri.util.ProcessWrapper 22 | import java.io.PipedInputStream 23 | import java.io.PipedOutputStream 24 | 25 | fun createFinalizeProcessMock(shouldSucceed: Boolean): ProcessWrapper { 26 | val op = PipedOutputStream() 27 | val stdOut = PipedInputStream(op) 28 | return mockk { 29 | every { getOutput() } returns stdOut 30 | every { waitFor() } answers { 31 | if (shouldSucceed) { 32 | 0 33 | } else { 34 | 1 35 | } 36 | } 37 | every { exitValue } answers { 38 | if (shouldSucceed) { 39 | 0 40 | } else { 41 | 1 42 | } 43 | } 44 | 45 | every { start() } answers { 46 | // Finish instantly and close the output stream so the task waiting on it to finish logging 47 | // doesn't have to block for long. 48 | op.close() 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/logging.properties: -------------------------------------------------------------------------------- 1 | handlers = java.util.logging.FileHandler 2 | 3 | java.util.logging.FileHandler.level = FINE 4 | java.util.logging.FileHandler.pattern = /var/log/jitsi/jibri/log.%g.txt 5 | java.util.logging.FileHandler.formatter = org.jitsi.utils.logging2.JitsiLogFormatter 6 | java.util.logging.FileHandler.count = 10 7 | java.util.logging.FileHandler.limit = 10000000 8 | 9 | org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.level = FINE 10 | org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.pattern = /var/log/jitsi/jibri/ffmpeg.%g.txt 11 | org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.formatter = org.jitsi.utils.logging2.JitsiLogFormatter 12 | org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.count = 10 13 | org.jitsi.jibri.capture.ffmpeg.util.FfmpegFileHandler.limit = 10000000 14 | 15 | org.jitsi.jibri.sipgateway.pjsua.util.PjsuaFileHandler.level = FINE 16 | org.jitsi.jibri.sipgateway.pjsua.util.PjsuaFileHandler.pattern = /var/log/jitsi/jibri/pjsua.%g.txt 17 | org.jitsi.jibri.sipgateway.pjsua.util.PjsuaFileHandler.formatter = org.jitsi.utils.logging2.JitsiLogFormatter 18 | org.jitsi.jibri.sipgateway.pjsua.util.PjsuaFileHandler.count = 10 19 | org.jitsi.jibri.sipgateway.pjsua.util.PjsuaFileHandler.limit = 10000000 20 | 21 | org.jitsi.jibri.selenium.util.BrowserFileHandler.level = FINE 22 | org.jitsi.jibri.selenium.util.BrowserFileHandler.pattern = /var/log/jitsi/jibri/browser.%g.txt 23 | org.jitsi.jibri.selenium.util.BrowserFileHandler.formatter = org.jitsi.utils.logging2.JitsiLogFormatter 24 | org.jitsi.jibri.selenium.util.BrowserFileHandler.count = 10 25 | org.jitsi.jibri.selenium.util.BrowserFileHandler.limit = 10000000 26 | 27 | org.jitsi.level = FINE 28 | org.jitsi.jibri.config.level = INFO 29 | 30 | org.glassfish.level = INFO 31 | org.osgi.level = INFO 32 | org.jitsi.xmpp.level = INFO 33 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/status_checks/StateTransitionTimeTracker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.selenium.status_checks 18 | 19 | import java.time.Clock 20 | import java.time.Duration 21 | import java.time.Instant 22 | 23 | /** 24 | * Track the most recent timestamp at which we transitioned from 25 | * an event having not occurred to when it did occur. Note this 26 | * tracks the timestamp of that *transition*, not the most recent 27 | * time the event itself occurred. 28 | */ 29 | class StateTransitionTimeTracker(private val clock: Clock) { 30 | var timestampTransitionOccured: Instant? = null 31 | private set 32 | 33 | fun maybeUpdate(eventOccurred: Boolean) { 34 | if (eventOccurred && timestampTransitionOccured == null) { 35 | timestampTransitionOccured = clock.instant() 36 | } else if (!eventOccurred) { 37 | timestampTransitionOccured = null 38 | } 39 | } 40 | 41 | fun exceededTimeout(timeout: Duration): Boolean { 42 | return timestampTransitionOccured?.let { 43 | Duration.between(it, clock.instant()) > timeout 44 | } ?: false 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/jibri/RecordingSinkType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri; 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | 22 | /** 23 | * @author bbaldino 24 | * NOTE: this file had to be in java, I couldn't get it to recognize the 25 | * custom JsonCreator constructor to handle enum-case-agnosticisim in kotlin 26 | */ 27 | public enum RecordingSinkType 28 | { 29 | STREAM("stream"), 30 | FILE("file"), 31 | //TODO: putting gateway in here doesn't feel great (and isn't what the xmpp uses anyway). 32 | // we need some top-level param that denotes what we're doing (recording a file, 33 | // streaming to youtube, or doing a sipgateway) 34 | GATEWAY("gateway"); 35 | 36 | @SuppressWarnings("FieldCanBeLocal") 37 | private final String recordingSinkType; 38 | 39 | RecordingSinkType(final String recordingMode) { 40 | this.recordingSinkType = recordingMode; 41 | } 42 | 43 | @JsonCreator 44 | public static RecordingSinkType fromString(@JsonProperty("sinkType") final String text) { 45 | return RecordingSinkType.valueOf(text.toUpperCase()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/config/Config.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.config 18 | 19 | import com.typesafe.config.ConfigFactory 20 | import org.jitsi.config.TypesafeConfigSource 21 | import org.jitsi.metaconfig.ConfigSource 22 | import org.jitsi.metaconfig.MapConfigSource 23 | 24 | class Config { 25 | companion object { 26 | val ConfigFromFile = TypesafeConfigSource("config", ConfigFactory.load()) 27 | 28 | /** 29 | * The 'new' config source 30 | */ 31 | var configSource = ConfigFromFile 32 | 33 | /** 34 | * The 'legacy' config sources: we parsed a JSON file into a [JibriConfig] instance. 35 | * Unfortunately we can't parse the JSON file via the new config library because it 36 | * contains comments (which the new library doesn't support). 37 | */ 38 | var legacyConfigSource = JibriConfig() 39 | 40 | /** 41 | * We also accepted config parameters via the command line, so this config source 42 | * is set to a [ConfigSource] containing those values. 43 | */ 44 | var commandLineArgs: ConfigSource = MapConfigSource("command line args") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/status_checks/IceConnectionStatusCheck.kt: -------------------------------------------------------------------------------- 1 | package org.jitsi.jibri.selenium.status_checks 2 | 3 | import org.jitsi.jibri.config.Config 4 | import org.jitsi.jibri.selenium.SeleniumEvent 5 | import org.jitsi.jibri.selenium.pageobjects.CallPage 6 | import org.jitsi.metaconfig.config 7 | import org.jitsi.utils.logging2.Logger 8 | import org.jitsi.utils.logging2.createChildLogger 9 | import java.time.Clock 10 | import java.time.Duration 11 | 12 | /** 13 | * A check for the ICE connection. If ICE is not in the "connected" state for more than [iceConnectionTimeout] then a 14 | * [SeleniumEvent.IceFailedEvent] is fired. 15 | */ 16 | class IceConnectionStatusCheck( 17 | parentLogger: Logger, 18 | private val clock: Clock = Clock.systemDefaultZone() 19 | ) : CallStatusCheck { 20 | private val logger = createChildLogger(parentLogger) 21 | 22 | // The last timestamp when ICE was connected. Initialized to give the same timeout for the initial connection. 23 | private var timeOfLastSuccess = clock.instant() 24 | 25 | override fun run(callPage: CallPage): SeleniumEvent? { 26 | val now = clock.instant() 27 | 28 | if (callPage.isCallEmpty() || callPage.isIceConnected()) { 29 | // If there are no other participants we don't expect to have an ICE connection. 30 | timeOfLastSuccess = now 31 | return null 32 | } 33 | 34 | if (Duration.between(timeOfLastSuccess, now) > iceConnectionTimeout) { 35 | logger.warn("ICE has failed and not recovered in $iceConnectionTimeout.") 36 | return SeleniumEvent.IceFailedEvent 37 | } 38 | return null 39 | } 40 | 41 | companion object { 42 | val iceConnectionTimeout: Duration by config { 43 | "jibri.call-status-checks.ice-connection-timeout".from(Config.configSource) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/service/impl/StatefulJibriService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.service.impl 18 | 19 | import org.jitsi.jibri.service.JibriService 20 | import org.jitsi.jibri.service.JibriServiceStateMachine 21 | import org.jitsi.jibri.service.toJibriServiceEvent 22 | import org.jitsi.jibri.status.ComponentState 23 | import org.jitsi.jibri.util.StatusPublisher 24 | import org.jitsi.utils.logging2.createLogger 25 | 26 | abstract class StatefulJibriService(private val name: String) : JibriService() { 27 | private val stateMachine = JibriServiceStateMachine() 28 | 29 | /** 30 | * The [Logger] for this class 31 | */ 32 | protected val logger = createLogger() 33 | 34 | init { 35 | stateMachine.onStateTransition { oldState, newState -> this.onServiceStateChange(oldState, newState) } 36 | } 37 | 38 | private fun onServiceStateChange(oldState: ComponentState, newState: ComponentState) { 39 | logger.info("$name service transitioning from state $oldState to $newState") 40 | publishStatus(newState) 41 | } 42 | 43 | protected fun registerSubComponent(subComponentId: String, subComponent: StatusPublisher) { 44 | stateMachine.registerSubComponent(subComponentId) 45 | subComponent.addStatusHandler { stateUpdate -> 46 | stateMachine.transition(stateUpdate.toJibriServiceEvent(subComponentId)) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/CallUrlInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri 19 | 20 | import com.fasterxml.jackson.annotation.JsonIgnore 21 | import java.util.Objects 22 | 23 | /** 24 | * We assume the 'baseUrl' represents a sort of landing page (on the same 25 | * domain) where we can set the necessary local storage values. The call 26 | * url will be created by joining [baseUrl] and [callName] with a "/". If 27 | * set, a list of [urlParams] will be concatenated after the call name with 28 | * a "#" in between. 29 | */ 30 | data class CallUrlInfo( 31 | val baseUrl: String = "", 32 | val callName: String = "", 33 | private val urlParams: List = listOf() 34 | ) { 35 | @get:JsonIgnore 36 | val callUrl: String 37 | get() { 38 | return if (urlParams.isNotEmpty()) { 39 | "$baseUrl/$callName#${urlParams.joinToString("&")}" 40 | } else { 41 | "$baseUrl/$callName" 42 | } 43 | } 44 | 45 | override fun equals(other: Any?): Boolean { 46 | return when { 47 | other == null -> false 48 | this === other -> true 49 | javaClass != other.javaClass -> false 50 | else -> hashCode() == other.hashCode() 51 | } 52 | } 53 | 54 | override fun hashCode(): Int { 55 | // Purposefully ignore urlParams here 56 | return Objects.hash(baseUrl.lowercase(), callName.lowercase()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/StatusPublisher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import java.util.concurrent.CopyOnWriteArrayList 21 | 22 | /** 23 | * Class for publishing and subscribing to status. Handles the management 24 | * of all subscribers (via [addStatusHandler]) and pushes updates to 25 | * those subscribers (synchronously) in [publishStatus]. Classes 26 | * interested in publishing their status should inherit from 27 | * [StatusPublisher]. 28 | */ 29 | open class StatusPublisher { 30 | private val handlers: MutableList<(T) -> Boolean> = CopyOnWriteArrayList() 31 | 32 | /** 33 | * Add a status handler for this [StatusPublisher]. Handlers 34 | * will be notified synchronously in the order they were added. 35 | */ 36 | fun addStatusHandler(handler: (T) -> Unit) { 37 | handlers.add { status -> 38 | handler(status) 39 | true 40 | } 41 | } 42 | 43 | fun addTemporaryHandler(handler: (T) -> Boolean) { 44 | handlers.add(handler) 45 | } 46 | 47 | /** 48 | * The function a [StatusPublisher] subclass should call when it has 49 | * a new status to publish. Note that handlers are notified synchronously 50 | * in the context of the thread which calls [publishStatus]. 51 | */ 52 | protected fun publishStatus(status: T) { 53 | // This will run all handlers, but only keep the ones that return true 54 | handlers.retainAll { it(status) } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/ProcessState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.util 18 | 19 | /** 20 | * Models 2 things about a process: 21 | * 1) Whether or not it is 'alive' (vs. having exited) and, if it has exited, what its exit code was 22 | * 2) Its most recent line of output (via stdout/stderr) 23 | */ 24 | data class ProcessState( 25 | val runningState: AliveState, 26 | val mostRecentOutput: String 27 | ) 28 | 29 | /** 30 | * Models whether a process is alive or has exited 31 | */ 32 | sealed class AliveState(val runningStatus: RunningStatus) { 33 | override fun toString(): String = with(StringBuffer()) { 34 | append("$runningStatus") 35 | toString() 36 | } 37 | } 38 | 39 | class ProcessRunning : AliveState(RunningStatus.RUNNING) 40 | 41 | class ProcessExited(val exitCode: Int) : AliveState(RunningStatus.EXITED) { 42 | override fun toString(): String = buildString { 43 | append("${super.toString()} exit code: $exitCode") 44 | } 45 | } 46 | 47 | class ProcessFailedToStart() : AliveState(RunningStatus.FAILED) 48 | 49 | enum class RunningStatus { 50 | /** 51 | * The component is currently 'running'. Note that this does not mean it _should_ still be running, i.e. its 52 | * state could be [Status.FINISHED] and it's now ready to be shutdown cleanly. 53 | */ 54 | RUNNING, 55 | 56 | /** 57 | * The process has exited 58 | */ 59 | EXITED, 60 | 61 | /** 62 | * The process failed to start 63 | */ 64 | FAILED 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/sink/impl/FileSink.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.sink.impl 19 | 20 | import org.jitsi.jibri.config.Config 21 | import org.jitsi.jibri.sink.Sink 22 | import org.jitsi.metaconfig.config 23 | import org.jitsi.metaconfig.from 24 | import java.nio.file.Path 25 | import java.time.LocalDateTime 26 | import java.time.format.DateTimeFormatter 27 | 28 | /** 29 | * [FileSink] represents a sink which will write to a media file on the 30 | * filesystem. A maximum length of 125 characters is enforced for the filename. 31 | * NOTE: I considered letting the maximum length be configurable, but we require that we 32 | * are able to append a timestamp to the filename, so we can't give the caller full control 33 | * over the value anyway. Because of that I just made the value hard-coded. 34 | */ 35 | class FileSink(recordingsDirectory: Path, callName: String) : Sink { 36 | val file: Path 37 | init { 38 | val suffix = "_${LocalDateTime.now().format(TIMESTAMP_FORMATTER)}.$recordingExtension" 39 | val filename = "${callName.take(MAX_FILENAME_LENGTH - suffix.length)}$suffix" 40 | file = recordingsDirectory.resolve(filename) 41 | } 42 | override val path: String = file.toString() 43 | 44 | companion object { 45 | val recordingExtension: String by config("jibri.ffmpeg.recording-extension".from(Config.configSource)) 46 | private val TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss") 47 | const val MAX_FILENAME_LENGTH = 125 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/status_checks/EmptyCallStatusCheck.kt: -------------------------------------------------------------------------------- 1 | package org.jitsi.jibri.selenium.status_checks 2 | 3 | import org.jitsi.jibri.config.Config 4 | import org.jitsi.jibri.selenium.SeleniumEvent 5 | import org.jitsi.jibri.selenium.pageobjects.CallPage 6 | import org.jitsi.metaconfig.config 7 | import org.jitsi.utils.logging2.Logger 8 | import org.jitsi.utils.logging2.createChildLogger 9 | import java.time.Clock 10 | import java.time.Duration 11 | 12 | /** 13 | * Verify that there are other participants in the call; if the call is 14 | * empty for more than [callEmptyTimeout], then return [SeleniumEvent.CallEmpty]. 15 | * 16 | * NOTE: this class doesn't perform the check automatically, it will only 17 | * check for call empty state when [run] is called. 18 | */ 19 | class EmptyCallStatusCheck( 20 | parentLogger: Logger, 21 | private val callEmptyTimeout: Duration = defaultCallEmptyTimeout, 22 | private val clock: Clock = Clock.systemUTC() 23 | ) : CallStatusCheck { 24 | private val logger = createChildLogger(parentLogger) 25 | init { 26 | logger.info("Starting empty call check with a timeout of $callEmptyTimeout") 27 | } 28 | 29 | // The timestamp at which we last saw the call transition from 30 | // non-empty to empty 31 | private val callWentEmptyTime = StateTransitionTimeTracker(clock) 32 | override fun run(callPage: CallPage): SeleniumEvent? { 33 | val now = clock.instant() 34 | callWentEmptyTime.maybeUpdate(callPage.isCallEmpty()) 35 | 36 | return when (callWentEmptyTime.exceededTimeout(callEmptyTimeout)) { 37 | true -> { 38 | logger.info( 39 | "Call has been empty since " + 40 | "${callWentEmptyTime.timestampTransitionOccured} " + 41 | "(${Duration.between(callWentEmptyTime.timestampTransitionOccured, now)} ago). " + 42 | "Returning CallEmpty event" 43 | ) 44 | SeleniumEvent.CallEmpty 45 | } 46 | false -> null 47 | } 48 | } 49 | 50 | companion object { 51 | val defaultCallEmptyTimeout: Duration by config { 52 | "jibri.call-status-checks.default-call-empty-timeout".from(Config.configSource) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/StateUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @file:Suppress("ktlint:standard:filename") 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import org.jitsi.jibri.status.ComponentState 21 | 22 | /** 23 | * These helpers make it easy to do work based on a component transitioning to a particular state. 24 | * For example, if we have subcomponents Foo and Bar, and Bar shouldn't start until Foo has reached 25 | * [ComponentState.Running], you can do: 26 | * 27 | * whenever(bar).transitionsTo(ComponentState.Running) { 28 | * foo.start() 29 | * } 30 | */ 31 | class ComponentStateTransitioner(private val statusPublisher: StatusPublisher) { 32 | init { 33 | } 34 | 35 | fun transitionsTo(desiredState: ComponentState, block: () -> Unit) { 36 | statusPublisher.addTemporaryHandler { state -> 37 | if (state == desiredState) { 38 | block() 39 | return@addTemporaryHandler false 40 | } 41 | true 42 | } 43 | } 44 | 45 | /** 46 | * We need a special method for the error state, since the error state is stateful and we don't use a single 47 | * instance to model the state. 48 | */ 49 | fun transitionsToError(block: () -> Unit) { 50 | statusPublisher.addTemporaryHandler { state -> 51 | if (state is ComponentState.Error) { 52 | block() 53 | return@addTemporaryHandler false 54 | } 55 | true 56 | } 57 | } 58 | } 59 | 60 | fun whenever(statusPublisher: StatusPublisher): ComponentStateTransitioner = 61 | ComponentStateTransitioner(statusPublisher) 62 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/service/JibriService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.service 19 | 20 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 21 | import com.fasterxml.jackson.annotation.JsonProperty 22 | import org.jitsi.jibri.status.ComponentState 23 | import org.jitsi.jibri.util.StatusPublisher 24 | 25 | enum class JibriServiceStatus { 26 | FINISHED, 27 | ERROR 28 | } 29 | 30 | /** 31 | * Arbitrary Jibri specific data that can be passed in the 32 | * [JibriIq#appData] field. This entire structure will be parsed 33 | * from a JSON-encoded string (the [JibriIq#appData] field). 34 | */ 35 | @JsonIgnoreProperties(ignoreUnknown = true) 36 | data class AppData( 37 | /** 38 | * A JSON map representing arbitrary data to be written 39 | * to the metadata file when doing a recording. 40 | */ 41 | @JsonProperty("file_recording_metadata") 42 | val fileRecordingMetadata: Map? 43 | ) 44 | 45 | /** 46 | * Parameters needed for starting any [JibriService] 47 | */ 48 | data class ServiceParams( 49 | val usageTimeoutMinutes: Int, 50 | val appData: AppData? = null 51 | ) 52 | 53 | typealias JibriServiceStatusHandler = (ComponentState) -> Unit 54 | 55 | /** 56 | * Interface implemented by all implemented [JibriService]s. A [JibriService] 57 | * is responsible for an entire feature of Jibri, such as recording a call 58 | * to a file or streaming a call to a url. 59 | */ 60 | abstract class JibriService : StatusPublisher() { 61 | /** 62 | * Starts this [JibriService] 63 | */ 64 | abstract fun start() 65 | 66 | /** 67 | * Stops this [JibriService] 68 | */ 69 | abstract fun stop() 70 | } 71 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/sink/impl/FileSinkTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.sink.impl 19 | 20 | import com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder 21 | import io.kotest.core.spec.IsolationMode 22 | import io.kotest.core.spec.style.ShouldSpec 23 | import io.kotest.matchers.shouldBe 24 | import io.kotest.matchers.string.shouldContain 25 | import io.kotest.matchers.string.shouldStartWith 26 | import kotlin.random.Random 27 | 28 | internal class FileSinkTest : ShouldSpec() { 29 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 30 | 31 | private val fs = MemoryFileSystemBuilder.newLinux().build() 32 | 33 | init { 34 | context("when created") { 35 | val sink = FileSink(fs.getPath("/tmp/xxx"), "callname") 36 | should("have the correct path") { 37 | sink.path.shouldStartWith("/tmp/xxx") 38 | sink.path.shouldContain("callname") 39 | } 40 | } 41 | context("when created with a really long call name") { 42 | val reallyLongCallName = String.randomAlphas(200) 43 | val sink = FileSink(fs.getPath("/tmp/xxx"), reallyLongCallName) 44 | should("not generate a filename longer than the max file length") { 45 | sink.file.fileName.toString().length shouldBe FileSink.MAX_FILENAME_LENGTH 46 | } 47 | } 48 | } 49 | 50 | // Generates a random string of lower-case a-z letters with the given size 51 | private fun String.Companion.randomAlphas(size: Int): String { 52 | val chars: List = ('a'..'z').toList() 53 | return (1..size) 54 | .map { Random.nextInt(0, chars.size) } 55 | .map(chars::get) 56 | .joinToString("") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /doc/example_xmpp_envs.conf: -------------------------------------------------------------------------------- 1 | // An example file showing the fields for the XMPP environment config. 2 | { 3 | environments = [ 4 | { 5 | // A user-friendly name for this environment 6 | name = "env name" 7 | 8 | // A list of XMPP server hosts to which we'll connect 9 | xmpp-server-hosts = [ "host1", "host2" ] 10 | 11 | // The base XMPP domain 12 | xmpp-domain = "xmpp-domain" 13 | 14 | // An (optional) base url the Jibri will join if it is set 15 | // base-url = "https://meet.example.com" 16 | 17 | // The MUC we'll join to announce our presence for 18 | // recording and streaming services 19 | control-muc { 20 | domain = "domain" 21 | room-name = "room-name" 22 | nickname = "nickname" 23 | } 24 | 25 | // The login information for the control MUC 26 | control-login { 27 | domain = "domain" 28 | // Optional port, defaults to 5222. 29 | port = 6222 30 | username = "username" 31 | password = "password" 32 | // Whether to use `username` as is or add a random suffix to it. 33 | randomize-username = false 34 | } 35 | 36 | // An (optional) MUC configuration where we'll 37 | // join to announce SIP gateway services 38 | sip-control-muc { 39 | domain = "domain" 40 | room-name = "room-name" 41 | nickname = "nickname" 42 | } 43 | 44 | // The login information the selenium web client will use 45 | call-login { 46 | domain = "domain" 47 | username = "username" 48 | password = "password" 49 | } 50 | 51 | // The value we'll strip from the room JID domain to derive 52 | // the call URL 53 | strip-from-room-domain = "" 54 | 55 | // How long Jibri sessions will be allowed to last before 56 | // they are stopped. A value of 0 allows them to go on 57 | // indefinitely 58 | usage-timeout = 1 hour 59 | 60 | // Whether or not we'll automatically trust any cert on 61 | // this XMPP domain 62 | trust-all-xmpp-certs = true 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/api/xmpp/JibriIqHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.api.xmpp 19 | 20 | import org.jitsi.xmpp.extensions.jibri.JibriIq 21 | import org.jivesoftware.smack.packet.IQ 22 | import org.jxmpp.jid.Jid 23 | 24 | // TODO: this functionality should be added to JibriIq 25 | 26 | /** 27 | * Create a result iq from the given [JibriIq] 28 | */ 29 | class JibriIqHelper { 30 | companion object { 31 | fun create(from: Jid, type: IQ.Type = IQ.Type.set, status: JibriIq.Status = JibriIq.Status.UNDEFINED): JibriIq { 32 | val jibriIq = JibriIq() 33 | jibriIq.to = from 34 | jibriIq.type = type 35 | jibriIq.status = status 36 | 37 | return jibriIq 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Return a result IQ for this [JibriIq], setting a few fields and then 44 | * applying [block] 45 | */ 46 | fun JibriIq.createResult(block: JibriIq.() -> Unit): JibriIq { 47 | return JibriIq().apply { 48 | type = IQ.Type.result 49 | to = this@createResult.from 50 | from = this@createResult.to 51 | stanzaId = this@createResult.stanzaId 52 | sipAddress = this@createResult.sipAddress 53 | block() 54 | } 55 | } 56 | 57 | enum class JibriMode(val mode: String) { 58 | FILE(JibriIq.RecordingMode.FILE.toString()), 59 | STREAM(JibriIq.RecordingMode.STREAM.toString()), 60 | SIPGW("sipgw"), 61 | UNDEFINED(JibriIq.RecordingMode.UNDEFINED.toString()) 62 | } 63 | 64 | fun JibriIq.mode(): JibriMode { 65 | if (!sipAddress.isNullOrBlank()) { 66 | return JibriMode.SIPGW 67 | } 68 | return when (recordingMode) { 69 | JibriIq.RecordingMode.FILE -> JibriMode.FILE 70 | JibriIq.RecordingMode.STREAM -> JibriMode.STREAM 71 | else -> JibriMode.UNDEFINED 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/util/TailLogicTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import io.kotest.core.spec.style.ShouldSpec 21 | import io.kotest.matchers.shouldBe 22 | import java.io.PipedInputStream 23 | import java.io.PipedOutputStream 24 | 25 | class TailLogicTest : ShouldSpec() { 26 | private lateinit var outputStream: PipedOutputStream 27 | private lateinit var inputStream: PipedInputStream 28 | private lateinit var tail: TailLogic 29 | 30 | private fun writeLine(data: String) { 31 | val line = if (data.endsWith("\n")) data else data + "\n" 32 | line.toByteArray().forEach { 33 | outputStream.write(it.toInt()) 34 | } 35 | } 36 | 37 | init { 38 | beforeTest { 39 | outputStream = PipedOutputStream() 40 | inputStream = PipedInputStream(outputStream) 41 | tail = TailLogic(inputStream) 42 | } 43 | context("mostRecentLine") { 44 | context("initially") { 45 | should("equal an empty string") { 46 | tail.mostRecentLine shouldBe "" 47 | } 48 | } 49 | context("after writing once") { 50 | should("equal that line") { 51 | writeLine("hello, world") 52 | tail.readLine() 53 | tail.mostRecentLine shouldBe "hello, world" 54 | } 55 | } 56 | context("after writing multiple times") { 57 | should("equal the most recent line") { 58 | writeLine("hello, world") 59 | tail.readLine() 60 | writeLine("goodbye, world") 61 | tail.readLine() 62 | tail.mostRecentLine shouldBe "goodbye, world" 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/selenium/status_checks/IceConnectionStatusCheckTest.kt: -------------------------------------------------------------------------------- 1 | package org.jitsi.jibri.selenium.status_checks 2 | 3 | import io.kotest.core.spec.style.ShouldSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.mockk.every 6 | import io.mockk.mockk 7 | import io.mockk.spyk 8 | import org.jitsi.jibri.selenium.SeleniumEvent 9 | import org.jitsi.jibri.selenium.pageobjects.CallPage 10 | import org.jitsi.utils.logging2.Logger 11 | import org.jitsi.utils.secs 12 | import org.jitsi.utils.time.FakeClock 13 | 14 | class IceConnectionStatusCheckTest : ShouldSpec() { 15 | private val clock: FakeClock = spyk() 16 | private val callPage: CallPage = mockk { 17 | every { isCallEmpty() } returns false 18 | } 19 | private val logger: Logger = mockk(relaxed = true) 20 | 21 | private val check = IceConnectionStatusCheck(logger, clock) 22 | 23 | init { 24 | context("When ICE connects") { 25 | every { callPage.isIceConnected() } returns true 26 | should("not report any event") { 27 | repeat(10) { 28 | check.run(callPage) shouldBe null 29 | clock.elapse(1.secs) 30 | } 31 | } 32 | } 33 | context("When ICE disconnects") { 34 | every { callPage.isIceConnected() } returns false 35 | should("not fire an event immediately") { 36 | repeat(10) { 37 | check.run(callPage) shouldBe null 38 | clock.elapse(1.secs) 39 | } 40 | } 41 | should("fire an event eventually") { 42 | clock.elapse(20.secs) 43 | check.run(callPage) shouldBe SeleniumEvent.IceFailedEvent 44 | } 45 | } 46 | context("When ICE disconnects but recovers") { 47 | every { callPage.isIceConnected() } returns true 48 | check.run(callPage) shouldBe null 49 | every { callPage.isIceConnected() } returns false 50 | should("not fire an event immediately") { 51 | repeat(10) { 52 | check.run(callPage) shouldBe null 53 | clock.elapse(1.secs) 54 | } 55 | } 56 | every { callPage.isIceConnected() } returns true 57 | should("not fire an event") { 58 | repeat(10) { 59 | check.run(callPage) shouldBe null 60 | clock.elapse(10.secs) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /doc/http_api.md: -------------------------------------------------------------------------------- 1 | # Jibri HTTP API 2 | 3 | Jibri has a somewhat-implemented HTTP API to mirror the XMPP API with the following endpoints: 4 | 5 | ##### URL 6 | `/jibri/api/v1.0/health` 7 | ##### Method 8 | `GET` 9 | ##### URL Params 10 | None 11 | ##### Data Params 12 | None 13 | ##### Response 14 | Code: `200` 15 | Body: 16 | ``` 17 | { 18 | "status":{ 19 | "busyStatus": String // "IDLE", "BUSY" or "EXPIRED" 20 | "health":{ 21 | "healthStatus": String // "HEALTHY" or "UNHEALTHY" 22 | "details": Map // Hash of component -> healthStatus above - only valid is "JibriManager" at present. 23 | } 24 | } 25 | } 26 | ``` 27 | This call should always respond with a `200` and the status encoded in the response body. The lack of any response would represent an error of some sort 28 | 29 | 30 | ##### URL 31 | `/jibri/api/v1.0/startService` 32 | ##### Method 33 | `POST` 34 | ##### URL Params 35 | None 36 | ##### Data Params 37 | ``` 38 | { 39 | "sessionId": String, // the recording operation session (e.g. RecordTest) 40 | "callParams": { 41 | "callUrlInfo": { 42 | "baseUrl": String, // the base url of the call (e.g. https://meet.jit.si) 43 | "callName": String // the call name to be appended to the base url 44 | } 45 | }, 46 | "callLoginParams": { 47 | "domain": String, // The xmpp domain the Jibri client should log into when joining the call 48 | "username": String, // The username to use for logging in to the above domain 49 | "password": String // The password to use for logging in to the above domain 50 | }, 51 | "sinkType": String, // "stream" for streaming, "file" for recording 52 | "youTubeStreamKey": String // If using "stream" above, this is the YouTube stream key to use 53 | } 54 | ``` 55 | ##### Success Response 56 | Code: `200` 57 | Body: None 58 | ##### Error Response 59 | Code: `412 Precondition Failed` // When the Jibri is already busy 60 | Body: None 61 | Code: `500 Internal Server Error` // When an internal error occurs 62 | Body: None 63 | 64 | ##### URL 65 | `/jibri/api/v1.0/stopService` 66 | ##### Method 67 | `POST` 68 | ##### URL Params 69 | None 70 | ##### Data Params 71 | None 72 | ##### Response 73 | Code: `200` 74 | Body: None 75 | 76 | This call should always respond with a `200`. The lack of any response would represent an error of some sort. 77 | 78 | Known HTTP API limitations: 79 | * No push status updates. 80 | * No way currently exists for interested parties to be notified of Jibri status changes. Queries must be made to the `health` endpoint. 81 | * No way of passing Jitsi Meet call credentials. 82 | * When Jibri joins a call in order to capture the media, it can use a special set of credentials so that it doesn't show up like a normal participant in the meeting. The HTTP API does not currently have a way of passing-in or configuring these credentials. 83 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/service/impl/JibriServiceFinalizeCommandRunner.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2021 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.service.impl 18 | 19 | import org.jitsi.jibri.service.JibriServiceFinalizer 20 | import org.jitsi.jibri.util.LoggingUtils 21 | import org.jitsi.jibri.util.ProcessFactory 22 | import org.jitsi.utils.logging2.createLogger 23 | import java.util.concurrent.TimeUnit 24 | import java.util.concurrent.TimeoutException 25 | 26 | class JibriServiceFinalizeCommandRunner( 27 | private val processFactory: ProcessFactory = ProcessFactory(), 28 | private val finalizeCommand: List 29 | ) : JibriServiceFinalizer { 30 | 31 | private val logger = createLogger() 32 | 33 | /** 34 | * Helper to execute the finalize script and wait for its completion. 35 | * NOTE that this will block for however long the finalize script takes 36 | * to complete (by design) 37 | */ 38 | override fun doFinalize() { 39 | if (finalizeCommand.isEmpty()) { 40 | logger.debug("Finalize command is empty, there is nothing to be run") 41 | return 42 | } 43 | 44 | logger.info("Finalizing the jibri service operation using command $finalizeCommand") 45 | try { 46 | with(processFactory.createProcess(finalizeCommand, logger)) { 47 | start() 48 | val streamDone = LoggingUtils.logOutputOfProcess(this, logger) 49 | waitFor() 50 | // Make sure we get all the logs 51 | try { 52 | streamDone.get(10, TimeUnit.SECONDS) 53 | } catch (e: TimeoutException) { 54 | logger.error("Timed out waiting for process logger task to complete") 55 | streamDone.cancel(true) 56 | } catch (e: Exception) { 57 | logger.error("Exception while waiting for process logger task to complete", e) 58 | streamDone.cancel(true) 59 | } 60 | logger.info("Finalize script finished with exit value $exitValue") 61 | } 62 | } catch (e: Exception) { 63 | logger.error("Failed to run finalize script", e) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/LoggingUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.util 18 | 19 | import org.jitsi.utils.logging2.Logger 20 | import org.jitsi.utils.logging2.LoggerImpl 21 | import java.io.BufferedReader 22 | import java.io.InputStream 23 | import java.io.InputStreamReader 24 | import java.util.concurrent.Callable 25 | import java.util.concurrent.Future 26 | import java.util.logging.FileHandler 27 | 28 | class LoggingUtils { 29 | companion object { 30 | var createPublishingTail: (InputStream, (String) -> Unit) -> PublishingTail = ::PublishingTail 31 | 32 | /** 33 | * The default implementation of the process output logger 34 | */ 35 | val OutputLogger: (ProcessWrapper, Logger) -> Future = { process, logger -> 36 | TaskPools.ioPool.submit( 37 | Callable { 38 | val reader = BufferedReader(InputStreamReader(process.getOutput())) 39 | 40 | while (true) { 41 | val line = reader.readLine() ?: break 42 | logger.info(line) 43 | } 44 | 45 | return@Callable true 46 | } 47 | ) 48 | } 49 | 50 | /** 51 | * A variable pointing to the current impl of the process output logger function. 52 | * Overridable so tests can change it. 53 | */ 54 | var logOutput = OutputLogger 55 | 56 | /** 57 | * A helper function to log a given [ProcessWrapper]'s output to the given [Logger]. 58 | * A future is returned which will be completed when the end of the given 59 | * stream is reached. 60 | */ 61 | fun logOutputOfProcess(process: ProcessWrapper, logger: Logger): Future { 62 | return logOutput(process, logger) 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * Create a logger with [name] and all of its inherited 69 | * handlers cleared, adding only the given handler 70 | */ 71 | fun getLoggerWithHandler(name: String, handler: FileHandler): Logger { 72 | return LoggerImpl(name).apply { 73 | setUseParentHandlers(false) 74 | addHandler(handler) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/Tail.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import java.io.BufferedReader 21 | import java.io.InputStream 22 | import java.io.InputStreamReader 23 | import java.util.concurrent.Future 24 | 25 | /** 26 | * Read from an infinite [InputStream] and make available the most 27 | * recent line that was read via [mostRecentLine]. NOTE: this class 28 | * will not read from the stream automatically, its [readLine] 29 | * method must be called. 30 | */ 31 | class TailLogic(inputStream: InputStream) { 32 | private val reader = BufferedReader(InputStreamReader(inputStream)) 33 | 34 | @Volatile var mostRecentLine: String = "" 35 | 36 | fun readLine() { 37 | mostRecentLine = reader.readLine() 38 | } 39 | } 40 | 41 | /** 42 | * A wrapper around [TailLogic] which Spins up a thread to constantly 43 | * read from the given [InputStream] and save the most-recently-read 44 | * line as [mostRecentLine] to be read by whomever is interested. 45 | */ 46 | class Tail(inputStream: InputStream) { 47 | private val tailLogic = TailLogic(inputStream) 48 | private var task: Future<*> 49 | val mostRecentLine: String 50 | get() { 51 | return tailLogic.mostRecentLine 52 | } 53 | 54 | init { 55 | task = TaskPools.ioPool.submit { 56 | while (true) { 57 | tailLogic.readLine() 58 | } 59 | } 60 | } 61 | 62 | fun stop() { 63 | task.cancel(true) 64 | } 65 | } 66 | 67 | /** 68 | * Read every line from the given [InputStream] and invoke the 69 | * [onOutput] handler with each line. 70 | */ 71 | class PublishingTail( 72 | inputStream: InputStream, 73 | private val onOutput: (String) -> Unit 74 | ) { 75 | private val tailLogic = TailLogic(inputStream) 76 | private var task: Future<*> 77 | 78 | init { 79 | task = TaskPools.ioPool.submit { 80 | while (true) { 81 | tailLogic.readLine() 82 | onOutput(tailLogic.mostRecentLine) 83 | } 84 | } 85 | } 86 | 87 | fun stop() { 88 | task.cancel(true) 89 | } 90 | 91 | val mostRecentLine: String 92 | get() { 93 | return tailLogic.mostRecentLine 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/XmppUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import org.jitsi.jibri.CallUrlInfo 21 | import org.jitsi.xmpp.extensions.jibri.JibriIq 22 | import org.jxmpp.jid.EntityBareJid 23 | 24 | /** 25 | * When we get a start [JibriIq] message, the room is given to us an [EntityBareJid] that we need to translate 26 | * into a URL for selenium to join. This method translates that jid into the url. 27 | */ 28 | fun getCallUrlInfoFromJid( 29 | roomJid: EntityBareJid, 30 | stripFromRoomDomain: String, 31 | xmppDomain: String, 32 | baseUrl: String? 33 | ): CallUrlInfo { 34 | try { 35 | // The url domain is pulled from the xmpp domain of the connection sending the request 36 | var domain = roomJid.domain.toString() 37 | // But the room jid domain may have a subdomain that shouldn't be applied to the url, so strip out any 38 | // string we've been told to remove from the domain 39 | domain = domain.replaceFirst(stripFromRoomDomain, "", ignoreCase = true) 40 | // Now we need to extract a potential call subdomain, which will be anything that's left in the domain 41 | // at this point before the configured xmpp domain. 42 | val subdomain = domain.subSequence(0, domain.indexOf(xmppDomain, ignoreCase = true)).trim('.') 43 | // Now just grab the call name 44 | val callName = roomJid.localpart.toString() 45 | 46 | // The call url is constructed from the baseCallUrl (base-url if any, or the xmpp domain), an optional subdomain, and a callname like so: 47 | // https://baseCallUrl/subdomain/callName 48 | var baseCallUrl = "https://$xmppDomain" 49 | if (!baseUrl.isNullOrEmpty()) { 50 | baseCallUrl = baseUrl 51 | } 52 | 53 | return when { 54 | subdomain.isEmpty() -> CallUrlInfo(baseCallUrl, callName) 55 | else -> CallUrlInfo("$baseCallUrl/$subdomain", callName) 56 | } 57 | } catch (e: Exception) { 58 | throw CallUrlInfoFromJidException( 59 | "Unable to extract call url info from Jid $roomJid (stripFromRoomDomain = $stripFromRoomDomain, " + 60 | "xmppDomain = $xmppDomain)" 61 | ) 62 | } 63 | } 64 | 65 | class CallUrlInfoFromJidException(message: String) : Exception(message) 66 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/selenium/SeleniumStateMachineTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2019 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.selenium 18 | 19 | import io.kotest.assertions.throwables.shouldThrow 20 | import io.kotest.core.spec.IsolationMode 21 | import io.kotest.core.spec.style.ShouldSpec 22 | import io.kotest.matchers.collections.haveSize 23 | import io.kotest.matchers.should 24 | import io.kotest.matchers.shouldBe 25 | import io.kotest.matchers.types.beInstanceOf 26 | import org.jitsi.jibri.status.ComponentState 27 | 28 | internal class SeleniumStateMachineTest : ShouldSpec() { 29 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 30 | 31 | private val stateUpdates = mutableListOf>() 32 | private val seleniumStateMachine = SeleniumStateMachine() 33 | 34 | init { 35 | beforeSpec { 36 | seleniumStateMachine.onStateTransition { fromState, toState -> 37 | stateUpdates.add((fromState to toState)) 38 | } 39 | } 40 | 41 | context("When starting up") { 42 | context("and the call is joined") { 43 | seleniumStateMachine.transition(SeleniumEvent.CallJoined) 44 | should("transition to running") { 45 | stateUpdates should haveSize(1) 46 | stateUpdates.first() shouldBe (ComponentState.StartingUp to ComponentState.Running) 47 | } 48 | } 49 | context("and an error occurs") { 50 | seleniumStateMachine.transition(SeleniumEvent.FailedToJoinCall) 51 | should("transition to error") { 52 | stateUpdates should haveSize(1) 53 | stateUpdates.first().second should beInstanceOf() 54 | } 55 | context("and then another event occurs") { 56 | seleniumStateMachine.transition(SeleniumEvent.CallEmpty) 57 | should("not fire another update") { 58 | stateUpdates should haveSize(1) 59 | } 60 | } 61 | } 62 | context("and an invalid event occurs") { 63 | should("throw an exception") { 64 | shouldThrow { 65 | seleniumStateMachine.transition(SeleniumEvent.NoMediaReceived) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/api/xmpp/JibriStatusExts.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.api.xmpp 18 | 19 | import org.jitsi.jibri.status.ComponentBusyStatus 20 | import org.jitsi.jibri.status.ComponentHealthStatus 21 | import org.jitsi.jibri.status.JibriStatus 22 | import org.jitsi.xmpp.extensions.health.HealthStatusPacketExt 23 | import org.jitsi.xmpp.extensions.jibri.JibriBusyStatusPacketExt 24 | import org.jitsi.xmpp.extensions.jibri.JibriStatusPacketExt 25 | import java.lang.RuntimeException 26 | 27 | /** 28 | * Translate the Jibri busy status enum to the jitsi-protocol-jabber version 29 | */ 30 | private fun ComponentBusyStatus.toBusyStatusExt(): JibriBusyStatusPacketExt.BusyStatus { 31 | return when (this) { 32 | ComponentBusyStatus.BUSY -> JibriBusyStatusPacketExt.BusyStatus.BUSY 33 | ComponentBusyStatus.IDLE -> JibriBusyStatusPacketExt.BusyStatus.IDLE 34 | ComponentBusyStatus.EXPIRED -> throw RuntimeException("'EXPIRED' not supported in JibriBusyStatusPacketExt") 35 | } 36 | } 37 | 38 | /** 39 | * Translate the Jibri health status enum to the jitsi-protocol-jabber version 40 | */ 41 | private fun ComponentHealthStatus.toHealthStatusExt(): HealthStatusPacketExt.Health { 42 | return when (this) { 43 | ComponentHealthStatus.HEALTHY -> HealthStatusPacketExt.Health.HEALTHY 44 | ComponentHealthStatus.UNHEALTHY -> HealthStatusPacketExt.Health.UNHEALTHY 45 | } 46 | } 47 | 48 | /** 49 | * Convert a [JibriStatus] to [JibriStatusPacketExt] 50 | * [shouldBeSentToMuc] should be called before calling this function to ensure the 51 | * status can be translated. 52 | */ 53 | fun JibriStatus.toJibriStatusExt(): JibriStatusPacketExt { 54 | val jibriStatusExt = JibriStatusPacketExt() 55 | 56 | val jibriBusyStatusExt = JibriBusyStatusPacketExt() 57 | jibriBusyStatusExt.status = this.busyStatus.toBusyStatusExt() 58 | jibriStatusExt.busyStatus = jibriBusyStatusExt 59 | 60 | val jibriHealthStatusExt = HealthStatusPacketExt() 61 | jibriHealthStatusExt.status = this.health.healthStatus.toHealthStatusExt() 62 | jibriStatusExt.healthStatus = jibriHealthStatusExt 63 | 64 | return jibriStatusExt 65 | } 66 | 67 | /** 68 | * 'Expired' is not a state we reflect in the control MUC (and isn't 69 | * defined by [JibriStatusPacketExt]), it's used only for the 70 | * internal health status so for now we don't see it to the MUC. 71 | */ 72 | fun JibriStatus.shouldBeSentToMuc(): Boolean { 73 | return when (this.busyStatus) { 74 | ComponentBusyStatus.EXPIRED -> false 75 | else -> true 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/helpers/Helpers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.helpers 18 | 19 | import io.mockk.every 20 | import io.mockk.mockk 21 | import org.jitsi.jibri.util.LoggingUtils 22 | import org.jitsi.jibri.util.ProcessWrapper 23 | import org.jitsi.jibri.util.TaskPools 24 | import org.jitsi.utils.logging2.Logger 25 | import java.time.Duration 26 | import java.util.concurrent.CompletableFuture 27 | import java.util.concurrent.ExecutorService 28 | import java.util.concurrent.Future 29 | import java.util.concurrent.ScheduledExecutorService 30 | 31 | /** 32 | * Custom version of kotlin.test's [io.kotlintest.eventually] which uses milliseconds 33 | * and adds a wait between checks (and gives me much more consistent results than 34 | * kotlin.test's version) 35 | */ 36 | fun within(duration: Duration, func: () -> T): T { 37 | val end = System.currentTimeMillis() + duration.toMillis() 38 | var times = 0 39 | while (System.currentTimeMillis() < end) { 40 | try { 41 | return func() 42 | } catch (e: Throwable) { 43 | if (!AssertionError::class.java.isAssignableFrom(e.javaClass)) { 44 | // Not the kind of exception we were prepared to tolerate 45 | throw e 46 | } 47 | // else ignore and continue 48 | } 49 | times++ 50 | Thread.sleep(500) 51 | } 52 | throw AssertionError("Test failed after ${duration.seconds} seconds; attempted $times times") 53 | } 54 | 55 | val Int.seconds: Duration 56 | get() = Duration.ofSeconds(this.toLong()) 57 | val Int.minutes: Duration 58 | get() = Duration.ofMinutes(this.toLong()) 59 | 60 | fun LoggingUtils.Companion.setTestOutputLogger(outputLogger: (ProcessWrapper, Logger) -> Future) { 61 | logOutput = outputLogger 62 | } 63 | 64 | fun LoggingUtils.Companion.resetOutputLogger() { 65 | logOutput = OutputLogger 66 | } 67 | 68 | fun TaskPools.Companion.setIoPool(pool: ExecutorService) { 69 | ioPool = pool 70 | } 71 | 72 | // Execute tasks in place (in the current thread, blocking) 73 | val inPlaceExecutor: ExecutorService = mockk { 74 | every { submit(any()) } answers { 75 | firstArg().run() 76 | CompletableFuture() 77 | } 78 | every { execute(any()) } answers { 79 | firstArg().run() 80 | } 81 | } 82 | 83 | fun TaskPools.Companion.resetIoPool() { 84 | ioPool = DefaultIoPool 85 | } 86 | 87 | fun TaskPools.Companion.setScheduledPool(pool: ScheduledExecutorService) { 88 | recurringTasksPool = pool 89 | } 90 | 91 | fun TaskPools.Companion.resetScheduledPool() { 92 | recurringTasksPool = DefaultRecurringTaskPool 93 | } 94 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/api/http/internal/InternalHttpApiTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.api.http.internal 19 | 20 | import io.kotest.core.spec.IsolationMode 21 | import io.kotest.core.spec.style.FunSpec 22 | import io.kotest.matchers.shouldBe 23 | import io.ktor.client.request.post 24 | import io.ktor.http.HttpStatusCode 25 | import io.ktor.server.testing.ApplicationTestBuilder 26 | import io.ktor.server.testing.testApplication 27 | 28 | class InternalHttpApiTest : FunSpec() { 29 | 30 | private var gracefulShutdownHandlerCalls = 0 31 | private var shutdownHandlerCalls = 0 32 | private var configChangedHandlerCalls = 0 33 | 34 | private val internalApi = InternalHttpApi( 35 | { configChangedHandlerCalls++ }, 36 | { gracefulShutdownHandlerCalls++ }, 37 | { shutdownHandlerCalls++ } 38 | ) 39 | 40 | init { 41 | isolationMode = IsolationMode.InstancePerLeaf 42 | 43 | test("gracefulShutdown should return a 200 and invoke the graceful shutdown handler") { 44 | apiTest { 45 | val response = client.post("/jibri/api/internal/v1.0/gracefulShutdown") 46 | response.status shouldBe HttpStatusCode.OK 47 | gracefulShutdownHandlerCalls shouldBe 1 48 | configChangedHandlerCalls shouldBe 0 49 | shutdownHandlerCalls shouldBe 0 50 | } 51 | } 52 | 53 | test("notifyConfigChanged should return a 200 and invoke the config changed handler") { 54 | apiTest { 55 | val response = client.post("/jibri/api/internal/v1.0/notifyConfigChanged") 56 | response.status shouldBe HttpStatusCode.OK 57 | gracefulShutdownHandlerCalls shouldBe 0 58 | configChangedHandlerCalls shouldBe 1 59 | shutdownHandlerCalls shouldBe 0 60 | } 61 | } 62 | 63 | test("shutdown should return a 200 and invoke the shutdown handler") { 64 | apiTest { 65 | val response = client.post("/jibri/api/internal/v1.0/shutdown") 66 | response.status shouldBe HttpStatusCode.OK 67 | gracefulShutdownHandlerCalls shouldBe 0 68 | configChangedHandlerCalls shouldBe 0 69 | shutdownHandlerCalls shouldBe 1 70 | } 71 | } 72 | } 73 | private fun apiTest(block: suspend ApplicationTestBuilder.() -> Unit) { 74 | with(internalApi) { 75 | testApplication { 76 | application { 77 | internalApiModule() 78 | } 79 | block() 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/sipgateway/SipClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.sipgateway 19 | 20 | import org.jitsi.jibri.status.ComponentState 21 | import org.jitsi.jibri.util.StatusPublisher 22 | 23 | data class SipClientParams( 24 | /** 25 | * The SIP address we'll be connecting to 26 | */ 27 | val sipAddress: String = "", 28 | 29 | /** 30 | * The display name used by pjsua as identity when listening for or sending an invite 31 | * For sending an invite, this should be the name of the entity initiating the invite 32 | */ 33 | val displayName: String = "", 34 | 35 | /** 36 | * Whether auto-answer is enabled, if it is, the client will listen for 37 | * incoming invites and will auto answer the first one. 38 | */ 39 | val autoAnswer: Boolean = false, 40 | 41 | /** 42 | * The optional auto-answer-timer in seconds. 43 | * If auto-answer is enabled, the client will listen for incoming invites 44 | * during this time. 45 | */ 46 | val autoAnswerTimer: Long? = 30, 47 | 48 | /** 49 | * The username to use if registration is needed. 50 | */ 51 | val userName: String? = null, 52 | 53 | /** 54 | * The password to use if registration is needed. 55 | */ 56 | val password: String? = null, 57 | 58 | /** 59 | * The optional contact address the invitee will be connecting to 60 | */ 61 | val contact: String? = null, 62 | 63 | /** 64 | * The optional address of proxy server 65 | */ 66 | val proxy: String? = null 67 | ) 68 | 69 | abstract class SipClient : StatusPublisher() { 70 | /** 71 | * Start the [SipClient] 72 | */ 73 | abstract fun start() 74 | 75 | /** 76 | * Stop the [SipClient] 77 | */ 78 | abstract fun stop() 79 | } 80 | 81 | /** 82 | * A SIP address is written in user@domain.tld format in a similar fashion to an email address. 83 | * An address like: sip:1-999-123-4567@voip-provider.example.net 84 | */ 85 | fun String.getSipAddress(): String { 86 | if (this.isNotEmpty() && this.hasSipSchemeEmbedded()) { 87 | return this.substringAfter(":") 88 | } 89 | return this 90 | } 91 | 92 | /** 93 | * Valid options would be `sip` or `sips` 94 | * The default sip scheme is `sip` if none is specified 95 | */ 96 | fun String.getSipScheme(): String { 97 | if (this.isNotEmpty() && this.hasSipSchemeEmbedded()) { 98 | return this.substringBefore(":") 99 | } 100 | return "sip" 101 | } 102 | 103 | private fun String.hasSipSchemeEmbedded(): Boolean = 104 | contains("sip:", ignoreCase = true) || contains("sips:", ignoreCase = true) 105 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/util/LoggingUtilsKtTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import io.kotest.core.spec.IsolationMode 21 | import io.kotest.core.spec.style.FunSpec 22 | import io.kotest.matchers.shouldBe 23 | import io.mockk.clearMocks 24 | import io.mockk.every 25 | import io.mockk.mockk 26 | import io.mockk.verify 27 | import org.jitsi.jibri.helpers.seconds 28 | import org.jitsi.jibri.helpers.within 29 | import org.jitsi.utils.logging2.Logger 30 | import java.io.PipedInputStream 31 | import java.io.PipedOutputStream 32 | import kotlin.concurrent.thread 33 | 34 | internal class LoggingUtilsKtTest : FunSpec() { 35 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 36 | 37 | private val process: ProcessWrapper = mockk() 38 | private lateinit var pipedOutputStream: PipedOutputStream 39 | private lateinit var inputStream: PipedInputStream 40 | private val logger: Logger = mockk(relaxed = true) 41 | 42 | init { 43 | beforeTest { 44 | pipedOutputStream = PipedOutputStream() 45 | inputStream = PipedInputStream(pipedOutputStream) 46 | clearMocks(logger) 47 | every { process.getOutput() } returns inputStream 48 | } 49 | 50 | test("logStream should write log lines to the given logger") { 51 | LoggingUtils.logOutputOfProcess(process, logger) 52 | thread { 53 | for (i in 0..4) { 54 | pipedOutputStream.write("$i\n".toByteArray()) 55 | } 56 | } 57 | 58 | val logLines = mutableListOf() 59 | within(5.seconds) { 60 | verify(exactly = 5) { logger.info(capture(logLines)) } 61 | } 62 | logLines.forEachIndexed { index, value -> 63 | index.toString() shouldBe value 64 | } 65 | } 66 | 67 | test("logStream should complete the task when EOF is reached") { 68 | val streamClosed = LoggingUtils.logOutputOfProcess(process, logger) 69 | thread { 70 | for (i in 0..4) { 71 | pipedOutputStream.write("$i\n".toByteArray()) 72 | } 73 | pipedOutputStream.close() 74 | } 75 | val logLines = mutableListOf() 76 | within(5.seconds) { 77 | verify(exactly = 5) { logger.info(capture(logLines)) } 78 | } 79 | logLines.forEachIndexed { index, value -> 80 | index.toString() shouldBe value 81 | } 82 | within(5.seconds) { 83 | streamClosed.isDone shouldBe true 84 | } 85 | streamClosed.get() shouldBe true 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/SeleniumStateMachine.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.selenium 18 | 19 | import com.tinder.StateMachine 20 | import org.jitsi.jibri.status.ComponentState 21 | import org.jitsi.jibri.util.NotifyingStateMachine 22 | 23 | sealed class SeleniumEvent { 24 | object CallJoined : SeleniumEvent() 25 | object FailedToJoinCall : SeleniumEvent() 26 | object CallEmpty : SeleniumEvent() 27 | object NoMediaReceived : SeleniumEvent() 28 | object IceFailedEvent : SeleniumEvent() 29 | object ChromeHung : SeleniumEvent() 30 | object LocalParticipantKicked : SeleniumEvent() 31 | } 32 | 33 | sealed class SideEffect 34 | 35 | class SeleniumStateMachine : NotifyingStateMachine() { 36 | private val stateMachine = StateMachine.create { 37 | initialState(ComponentState.StartingUp) 38 | 39 | state { 40 | on { 41 | transitionTo(ComponentState.Running) 42 | } 43 | on { 44 | transitionTo(ComponentState.Error(FailedToJoinCall)) 45 | } 46 | on { 47 | transitionTo(ComponentState.Error(ChromeHung)) 48 | } 49 | } 50 | 51 | state { 52 | on { 53 | transitionTo(ComponentState.Finished) 54 | } 55 | on { 56 | transitionTo(ComponentState.Error(NoMediaReceived)) 57 | } 58 | on { 59 | transitionTo(ComponentState.Error(IceFailed)) 60 | } 61 | on { 62 | transitionTo(ComponentState.Finished) 63 | } 64 | on { 65 | transitionTo(ComponentState.Error(ChromeHung)) 66 | } 67 | } 68 | 69 | state { 70 | on(any()) { 71 | dontTransition() 72 | } 73 | } 74 | 75 | state { 76 | on(any()) { 77 | dontTransition() 78 | } 79 | } 80 | 81 | onTransition { 82 | val validTransition = it as? StateMachine.Transition.Valid ?: run { 83 | throw Exception("Invalid state transition: $it") 84 | } 85 | if (validTransition.fromState::class != validTransition.toState::class) { 86 | notify(validTransition.fromState, validTransition.toState) 87 | } 88 | } 89 | } 90 | 91 | fun transition(event: SeleniumEvent): StateMachine.Transition<*, *, *> = stateMachine.transition(event) 92 | } 93 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/util/FileUtilsKtTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import com.github.marschall.memoryfilesystem.MemoryFileSystemBuilder 21 | import io.kotest.core.spec.IsolationMode 22 | import io.kotest.core.spec.style.ShouldSpec 23 | import io.kotest.matchers.shouldBe 24 | import java.nio.file.Files 25 | import java.nio.file.Path 26 | import java.nio.file.attribute.PosixFilePermissions 27 | 28 | internal class FileUtilsKtTest : ShouldSpec() { 29 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 30 | 31 | private val fs = MemoryFileSystemBuilder.newLinux().build() 32 | 33 | private fun setPerms(permsStr: String, p: Path) { 34 | val perms = PosixFilePermissions.fromString(permsStr) 35 | Files.setPosixFilePermissions(p, perms) 36 | } 37 | private fun createPath(pathStr: String): Path = createPath(fs.getPath(pathStr)) 38 | private fun createPath(path: Path): Path { 39 | // Just check for the presence of a "." to determine if it's a file or a dir 40 | if (path.fileName.toString().contains(".")) { 41 | Files.createFile(path) 42 | } else { 43 | Files.createDirectories(path) 44 | } 45 | return path 46 | } 47 | private fun Path.withPerms(permString: String): Path { 48 | setPerms(permString, this) 49 | return this 50 | } 51 | 52 | init { 53 | context("createIfDoesNotExist") { 54 | context("with the proper permissions") { 55 | should("create a directory") { 56 | val dir = fs.getPath("/xxx/dir") 57 | createIfDoesNotExist(dir) shouldBe true 58 | Files.exists(dir) shouldBe true 59 | } 60 | should("create nested directories") { 61 | val dir = fs.getPath("/test/dir") 62 | createIfDoesNotExist(dir) shouldBe true 63 | Files.exists(dir) shouldBe true 64 | } 65 | should("return true if the dir already exists") { 66 | val dir = fs.getPath("dir") 67 | Files.createDirectory(dir) 68 | createIfDoesNotExist(dir) shouldBe true 69 | Files.exists(dir) shouldBe true 70 | } 71 | } 72 | context("without permissions to create in a directory") { 73 | val baseDir = fs.getPath("/noperms") 74 | Files.createDirectory(baseDir).withPerms("r--r--r--") 75 | should("fail to create a single dir") { 76 | val newDir = baseDir.resolve("test") 77 | createIfDoesNotExist(newDir) shouldBe false 78 | } 79 | should("fail to create nested dirs") { 80 | val newDir = baseDir.resolve("test1/test2") 81 | createIfDoesNotExist(newDir) shouldBe false 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/Tee.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import java.io.BufferedReader 21 | import java.io.InputStream 22 | import java.io.InputStreamReader 23 | import java.io.OutputStream 24 | import java.io.PipedInputStream 25 | import java.io.PipedOutputStream 26 | import java.util.concurrent.CopyOnWriteArrayList 27 | import java.util.concurrent.Future 28 | 29 | class EndOfStreamException : Exception() 30 | 31 | private const val EOF = -1 32 | 33 | /** 34 | * Reads from the given [InputStream] and mirrors the read 35 | * data to all of the created 'branches' off of it. 36 | * All branches will 'receive' all data from the original 37 | * [InputStream] starting at the the point of 38 | * the branch's creation. 39 | * NOTE: This class will not read from the given [InputStream] 40 | * automatically, its [read] must be invoked 41 | * to read the data from the original stream and write it to 42 | * the branches 43 | */ 44 | class TeeLogic(inputStream: InputStream) { 45 | private val reader = BufferedReader(InputStreamReader(inputStream)) 46 | private var branches = CopyOnWriteArrayList() 47 | 48 | /** 49 | * Reads a byte from the original [InputStream] and 50 | * writes it to all of the branches. If EOF is detected, 51 | * all branches will be closed and [EndOfStreamException] 52 | * will be thrown, so that any callers can know not 53 | * to bother calling again. 54 | */ 55 | fun read() { 56 | val c = reader.read() 57 | if (c == EOF) { 58 | branches.forEach(OutputStream::close) 59 | throw EndOfStreamException() 60 | } else { 61 | branches.forEach { it.write(c) } 62 | } 63 | } 64 | 65 | /** 66 | * If you want to close the Tee before the underlying 67 | * [InputStream] closes, you'll need to call [close] to 68 | * properly close all downstream branches. Note that 69 | * calling [read] after [close] when there are branches 70 | * will result in [java.io.IOException]. 71 | */ 72 | fun close() { 73 | branches.forEach(OutputStream::close) 74 | } 75 | 76 | /** 77 | * Returns an [InputStream] that will receive 78 | * all data from the original [InputStream] 79 | * starting from the time of its creation 80 | */ 81 | fun addBranch(): InputStream { 82 | with(PipedOutputStream()) { 83 | branches.add(this) 84 | return PipedInputStream(this) 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * A wrapper around [TeeLogic] which spins up its own thread 91 | * to do the reading automatically 92 | */ 93 | class Tee(inputStream: InputStream) { 94 | private val teeLogic = TeeLogic(inputStream) 95 | private val task: Future<*> 96 | 97 | init { 98 | task = TaskPools.ioPool.submit { 99 | while (true) { 100 | teeLogic.read() 101 | } 102 | } 103 | } 104 | 105 | fun addBranch(): InputStream { 106 | return teeLogic.addBranch() 107 | } 108 | 109 | fun stop() { 110 | task.cancel(true) 111 | teeLogic.close() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/util/XmppUtilsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import io.kotest.core.spec.style.ShouldSpec 21 | import io.kotest.matchers.shouldBe 22 | import org.jitsi.jibri.CallUrlInfo 23 | import org.jxmpp.jid.impl.JidCreate 24 | 25 | class XmppUtilsTest : ShouldSpec() { 26 | private val baseDomain = "brian.jitsi.net" 27 | private val baseUrl = "http://brian.jitsi.net" 28 | 29 | init { 30 | context("getCallUrlInfoFromJid") { 31 | context("a basic room jid with no baseUrl set") { 32 | val expected = CallUrlInfo("https://$baseDomain", "roomName") 33 | val jid = JidCreate.entityBareFrom("${expected.callName}@$baseDomain") 34 | should("convert to a call url correctly") { 35 | getCallUrlInfoFromJid(jid, "", baseDomain, null) shouldBe expected 36 | } 37 | } 38 | context("a basic room jid with the baseUrl set") { 39 | val expected = CallUrlInfo(baseUrl, "roomName") 40 | val jid = JidCreate.entityBareFrom("${expected.callName}@$baseDomain") 41 | should("convert to a call url correctly") { 42 | getCallUrlInfoFromJid(jid, "", baseDomain, baseUrl) shouldBe expected 43 | } 44 | } 45 | context("a basic room jid with empty baseUrl") { 46 | val expected = CallUrlInfo("https://$baseDomain", "roomName") 47 | val jid = JidCreate.entityBareFrom("${expected.callName}@$baseDomain") 48 | should("convert to a call url correctly") { 49 | getCallUrlInfoFromJid(jid, "", baseDomain, "") shouldBe expected 50 | } 51 | } 52 | context("a roomjid with a subdomain that should be stripped") { 53 | val expected = CallUrlInfo("https://$baseDomain", "roomName") 54 | val jid = JidCreate.entityBareFrom("${expected.callName}@mucdomain.$baseDomain") 55 | should("convert to a call url correctly") { 56 | getCallUrlInfoFromJid(jid, "mucdomain.", baseDomain, "") shouldBe expected 57 | } 58 | } 59 | context("a roomjid with a call subdomain") { 60 | val expected = CallUrlInfo("https://$baseDomain/subdomain", "roomName") 61 | val jid = JidCreate.entityBareFrom("${expected.callName}@mucdomain.subdomain.$baseDomain") 62 | getCallUrlInfoFromJid(jid, "mucdomain.", baseDomain, "") shouldBe expected 63 | } 64 | context("a basic muc room jid, domain contains part to be stripped") { 65 | // domain contains 'conference' 66 | val conferenceBaseDomain = "conference.$baseDomain" 67 | val expected = CallUrlInfo("https://$conferenceBaseDomain", "roomName") 68 | val jid = JidCreate.entityBareFrom("${expected.callName}@conference.$conferenceBaseDomain") 69 | should("convert to a call url correctly") { 70 | // we want to strip the first conference from the jid 71 | getCallUrlInfoFromJid(jid, "conference", conferenceBaseDomain, "") shouldBe expected 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/service/AppDataTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.service 19 | 20 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 21 | import com.fasterxml.jackson.module.kotlin.readValue 22 | import io.kotest.core.spec.IsolationMode 23 | import io.kotest.core.spec.style.ShouldSpec 24 | import io.kotest.matchers.maps.shouldContainExactly 25 | import io.kotest.matchers.maps.shouldContainKey 26 | import io.kotest.matchers.shouldNotBe 27 | 28 | internal class AppDataTest : ShouldSpec() { 29 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 30 | 31 | init { 32 | context("a json-encoded app data structure") { 33 | val appDataJsonStr = """ 34 | { 35 | "file_recording_metadata": 36 | { 37 | "upload_credentials": 38 | { 39 | "service_name":"dropbox", 40 | "token":"XXXXXXXXYYYYYYYYYZZZZZZAAAAAAABBBBBBCCCDDD" 41 | } 42 | } 43 | } 44 | """.trimIndent() 45 | should("be parsed correctly") { 46 | val appData = jacksonObjectMapper().readValue(appDataJsonStr) 47 | appData.fileRecordingMetadata shouldNotBe null 48 | appData.fileRecordingMetadata?.shouldContainKey("upload_credentials") 49 | appData.fileRecordingMetadata?.get("upload_credentials") shouldNotBe null 50 | @Suppress("UNCHECKED_CAST") 51 | (appData.fileRecordingMetadata?.get("upload_credentials") as Map) 52 | .shouldContainExactly( 53 | mapOf( 54 | "service_name" to "dropbox", 55 | "token" to "XXXXXXXXYYYYYYYYYZZZZZZAAAAAAABBBBBBCCCDDD" 56 | ) 57 | ) 58 | } 59 | } 60 | context("a json-encoded app data structure with an extra top-level field") { 61 | val appDataJsonStr = """ 62 | { 63 | "file_recording_metadata": 64 | { 65 | "upload_credentials": 66 | { 67 | "service_name":"dropbox", 68 | "token":"XXXXXXXXYYYYYYYYYZZZZZZAAAAAAABBBBBBCCCDDD" 69 | } 70 | }, 71 | "other_new_field": "hello" 72 | } 73 | """.trimIndent() 74 | should("be parsed correctly and ignore unknown fields") { 75 | val appData = jacksonObjectMapper().readValue(appDataJsonStr) 76 | appData.fileRecordingMetadata shouldNotBe null 77 | appData.fileRecordingMetadata?.shouldContainKey("upload_credentials") 78 | appData.fileRecordingMetadata?.get("upload_credentials") shouldNotBe null 79 | @Suppress("UNCHECKED_CAST") 80 | (appData.fileRecordingMetadata?.get("upload_credentials") as Map) 81 | .shouldContainExactly( 82 | mapOf( 83 | "service_name" to "dropbox", 84 | "token" to "XXXXXXXXYYYYYYYYYZZZZZZAAAAAAABBBBBBCCCDDD" 85 | ) 86 | ) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/util/JibriSubprocess.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.util 18 | 19 | import org.jitsi.utils.logging2.Logger 20 | import org.jitsi.utils.logging2.createChildLogger 21 | import java.time.Duration 22 | import java.util.concurrent.Future 23 | import java.util.concurrent.TimeUnit 24 | import java.util.concurrent.TimeoutException 25 | 26 | class JibriSubprocess( 27 | parentLogger: Logger, 28 | private val name: String, 29 | private val processOutputLogger: Logger? = null, 30 | private val processFactory: ProcessFactory = ProcessFactory(), 31 | processStatePublisherProvider: ((ProcessWrapper) -> ProcessStatePublisher)? = null 32 | ) : StatusPublisher() { 33 | private val logger = createChildLogger(parentLogger) 34 | private val processStatePublisherProvider = 35 | processStatePublisherProvider ?: { process -> ProcessStatePublisher(logger, name, process) } 36 | private var processLoggerTask: Future? = null 37 | 38 | private var process: ProcessWrapper? = null 39 | private var processStatePublisher: ProcessStatePublisher? = null 40 | 41 | fun launch(command: List, env: Map = mapOf()) { 42 | logger.info("Starting $name with command ${command.joinToString(separator = " ")} ($command)") 43 | process = processFactory.createProcess(command, logger, env) 44 | try { 45 | process?.let { 46 | it.start() 47 | processStatePublisher = processStatePublisherProvider(it) 48 | processStatePublisher!!.addStatusHandler(this::publishStatus) 49 | if (processOutputLogger != null) { 50 | processLoggerTask = LoggingUtils.logOutputOfProcess(it, processOutputLogger) 51 | } 52 | } ?: run { 53 | throw Exception("Process was null") 54 | } 55 | } catch (t: Throwable) { 56 | logger.error("Error starting $name") 57 | process = null 58 | publishStatus(ProcessState(ProcessFailedToStart(), "")) 59 | } 60 | } 61 | 62 | private fun waitForProcessLoggerTaskToFinish(timeout: Duration) { 63 | try { 64 | processLoggerTask?.get(timeout.toMillis(), TimeUnit.MILLISECONDS) 65 | } catch (e: TimeoutException) { 66 | logger.error("Timed out waiting for process logger task to complete") 67 | processLoggerTask?.cancel(true) 68 | } catch (e: Exception) { 69 | logger.error("Exception while waiting for process logger task to complete", e) 70 | processLoggerTask?.cancel(true) 71 | } 72 | } 73 | 74 | fun stop() { 75 | logger.info("Stopping $name process") 76 | processStatePublisher?.stop() 77 | 78 | process?.apply { 79 | if (!stopAndWaitFor(Duration.ofSeconds(10))) { 80 | logger.error("Error trying to gracefully stop $name, destroying forcibly") 81 | if (!destroyForciblyAndWaitFor(Duration.ofSeconds(10))) { 82 | logger.error("Error trying to destroy $name forcibly") 83 | } 84 | } 85 | } 86 | waitForProcessLoggerTaskToFinish(Duration.ofSeconds(10)) 87 | 88 | try { 89 | logger.info("$name exited with value ${process?.exitValue}") 90 | } catch (e: IllegalThreadStateException) { 91 | logger.error("$name has still not exited! Unable to stop it") 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/api/http/internal/InternalHttpApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.api.http.internal 18 | 19 | import io.ktor.http.HttpStatusCode 20 | import io.ktor.serialization.jackson.jackson 21 | import io.ktor.server.application.Application 22 | import io.ktor.server.application.install 23 | import io.ktor.server.plugins.contentnegotiation.ContentNegotiation 24 | import io.ktor.server.response.respond 25 | import io.ktor.server.routing.RoutingContext 26 | import io.ktor.server.routing.post 27 | import io.ktor.server.routing.route 28 | import io.ktor.server.routing.routing 29 | import kotlinx.coroutines.CompletableDeferred 30 | import kotlinx.coroutines.coroutineScope 31 | import kotlinx.coroutines.launch 32 | import org.jitsi.jibri.config.Config 33 | import org.jitsi.metaconfig.config 34 | import org.jitsi.utils.logging2.createLogger 35 | 36 | class InternalHttpApi( 37 | private val configChangedHandler: () -> Unit, 38 | private val gracefulShutdownHandler: () -> Unit, 39 | private val shutdownHandler: () -> Unit 40 | ) { 41 | private val logger = createLogger() 42 | 43 | fun Application.internalApiModule() { 44 | install(ContentNegotiation) { 45 | jackson {} 46 | } 47 | 48 | routing { 49 | route("/jibri/api/internal/v1.0") { 50 | /** 51 | * Signal this Jibri to shutdown gracefully, meaning shut down when 52 | * it is idle (i.e. finish any currently running service). Returns a 200 53 | * and schedules a shutdown for when it becomes idle. 54 | */ 55 | post("gracefulShutdown") { 56 | logger.info("Jibri gracefully shutting down") 57 | respondOkAndRun(gracefulShutdownHandler) 58 | } 59 | /** 60 | * Signal this Jibri to reload its config file at the soonest opportunity 61 | * (when it does not have a currently running service). Returns a 200. 62 | */ 63 | post("notifyConfigChanged") { 64 | logger.info("Config file changed") 65 | respondOkAndRun(configChangedHandler) 66 | } 67 | /** 68 | * Signal this Jibri to (cleanly) stop any services that are 69 | * running and shutdown. Returns a 200. 70 | */ 71 | post("shutdown") { 72 | logger.info("Jibri is forcefully shutting down") 73 | respondOkAndRun(shutdownHandler) 74 | } 75 | } 76 | } 77 | } 78 | companion object { 79 | val port: Int by config { 80 | "internal_http_port" 81 | .from(Config.commandLineArgs).softDeprecated("use jibri.api.http.internal-api-port") 82 | "jibri.api.http.internal-api-port".from(Config.configSource) 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Responds with [HttpStatusCode.OK] and then runs the given block 89 | */ 90 | private suspend fun RoutingContext.respondOkAndRun(block: () -> Unit) { 91 | val latch = CompletableDeferred() 92 | coroutineScope { 93 | launch { 94 | latch.join() 95 | block() 96 | } 97 | try { 98 | call.respond(HttpStatusCode.OK) 99 | } finally { 100 | latch.cancel() 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/CallUrlInfoTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri 19 | 20 | import io.kotest.core.spec.IsolationMode 21 | import io.kotest.core.spec.style.ShouldSpec 22 | import io.kotest.data.forAll 23 | import io.kotest.data.headers 24 | import io.kotest.data.row 25 | import io.kotest.data.table 26 | import io.kotest.matchers.shouldBe 27 | import io.kotest.matchers.shouldNotBe 28 | 29 | class CallUrlInfoTest : ShouldSpec() { 30 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 31 | 32 | init { 33 | context("creating a CallUrlInfo") { 34 | context("without url params") { 35 | val info = CallUrlInfo("baseUrl", "callName") 36 | should("assign the fields correctly") { 37 | info.baseUrl shouldBe "baseUrl" 38 | info.callName shouldBe "callName" 39 | info.callUrl shouldBe "baseUrl/callName" 40 | } 41 | } 42 | context("with url params") { 43 | val info = CallUrlInfo("baseUrl", "callName", listOf("one", "two", "three")) 44 | should("assign the fields correctly") { 45 | info.baseUrl shouldBe "baseUrl" 46 | info.callName shouldBe "callName" 47 | info.callUrl shouldBe "baseUrl/callName#one&two&three" 48 | } 49 | } 50 | } 51 | context("a nullable CallUrlInfo instance") { 52 | should("not equal null") { 53 | val nullableInfo: CallUrlInfo? = CallUrlInfo("baseUrl", "callName") 54 | nullableInfo shouldNotBe null 55 | } 56 | } 57 | context("equality and hashcode") { 58 | val info = CallUrlInfo("baseUrl", "callName") 59 | context("a CallUrlInfo instance") { 60 | should("not equal another type") { 61 | @Suppress("ReplaceCallWithBinaryOperator") 62 | info.equals("string") shouldBe false 63 | } 64 | context("when compared to other variations") { 65 | should("be equal/not equal where appropriate") { 66 | val duplicateInfo = CallUrlInfo("baseUrl", "callName") 67 | val differentBaseUrl = CallUrlInfo("differentBaseUrl", "callName") 68 | val differentCallName = CallUrlInfo("differentUrl", "differentCallName") 69 | val differentBaseUrlCase = CallUrlInfo("BASEURL", "callName") 70 | val differentCallNameCase = CallUrlInfo("baseUrl", "CALLNAME") 71 | val withUrlParams = CallUrlInfo("baseUrl", "callName", listOf("one", "two", "three")) 72 | 73 | val t = table( 74 | headers("left", "right", "shouldEqual"), 75 | row(info, info, true), 76 | row(info, duplicateInfo, true), 77 | row(info, differentBaseUrl, false), 78 | row(info, differentCallName, false), 79 | row(info, differentBaseUrlCase, true), 80 | row(info, differentCallNameCase, true), 81 | row(info, withUrlParams, true) 82 | ) 83 | forAll(t) { left, right, shouldEqual -> 84 | if (shouldEqual) { 85 | left shouldBe right 86 | left.hashCode() shouldBe right.hashCode() 87 | } else { 88 | left shouldNotBe right 89 | left.hashCode() shouldNotBe right.hashCode() 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/util/TeeLogicTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.util 19 | 20 | import io.kotest.assertions.throwables.shouldThrow 21 | import io.kotest.core.spec.IsolationMode 22 | import io.kotest.core.spec.style.ShouldSpec 23 | import io.kotest.matchers.shouldBe 24 | import java.io.BufferedReader 25 | import java.io.InputStreamReader 26 | import java.io.PipedInputStream 27 | import java.io.PipedOutputStream 28 | import kotlin.time.Duration.Companion.seconds 29 | import kotlin.time.ExperimentalTime 30 | 31 | internal class TeeLogicTest : ShouldSpec() { 32 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 33 | 34 | private val outputStream = PipedOutputStream() 35 | private val inputStream = PipedInputStream(outputStream) 36 | private val tee = TeeLogic(inputStream) 37 | 38 | private fun pushIncomingData(data: Byte) { 39 | outputStream.write(data.toInt()) 40 | tee.read() 41 | } 42 | 43 | private fun pushIncomingData(data: String) { 44 | data.toByteArray().forEach { 45 | pushIncomingData(it) 46 | } 47 | } 48 | 49 | init { 50 | context("data written") { 51 | context("after the creation of a branch") { 52 | should("be received by that branch") { 53 | val branch = tee.addBranch() 54 | pushIncomingData("hello, world\n") 55 | val reader = BufferedReader(InputStreamReader(branch)) 56 | reader.readLine() shouldBe "hello, world" 57 | } 58 | } 59 | context("before the creation of a branch") { 60 | should("not be received by that branch") { 61 | pushIncomingData("hello, world\n") 62 | val branch = tee.addBranch() 63 | pushIncomingData("goodbye, world\n") 64 | val reader = BufferedReader(InputStreamReader(branch)) 65 | reader.readLine() shouldBe "goodbye, world" 66 | } 67 | } 68 | } 69 | context("end of stream") { 70 | should("throw EndOfStreamException") { 71 | outputStream.close() 72 | shouldThrow { 73 | tee.read() 74 | } 75 | } 76 | should("close all branches") { 77 | val branch1 = tee.addBranch() 78 | val branch2 = tee.addBranch() 79 | outputStream.close() 80 | shouldThrow { 81 | tee.read() 82 | } 83 | val reader1 = BufferedReader(InputStreamReader(branch1)) 84 | reader1.readLine() shouldBe null 85 | 86 | val reader2 = BufferedReader(InputStreamReader(branch2)) 87 | reader2.readLine() shouldBe null 88 | } 89 | } 90 | } 91 | } 92 | 93 | @ExperimentalTime 94 | internal class TeeTest : ShouldSpec() { 95 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 96 | 97 | private val outputStream = PipedOutputStream() 98 | private val inputStream = PipedInputStream(outputStream) 99 | private val tee = Tee(inputStream) 100 | 101 | init { 102 | val branch = tee.addBranch() 103 | context("stop") { 104 | should("close downstream readers").config(timeout = 5.seconds) { 105 | tee.stop() 106 | // This is testing that it returns the proper value (to signal 107 | // the stream was closed). But the timeout in the config tests 108 | // is also critical because if the stream wasn't closed, then 109 | // the read call will block indefinitely 110 | branch.read() shouldBe -1 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /resources/debian-package/opt/jitsi/jibri/wait_graceful_shutdown.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # 1. The script issues shutdown command via the graceful shutdown command 4 | # If unsuccessful then it exits with 1. 5 | # 2. If the code is ok then it checks if jibri has exited. 6 | # 3. If not then it polls jibri statistics until recording status is not true 7 | # 4. Gives some time for jibri to shutdown. If it does not quit after that 8 | # time then it kills the process. If the process was successfully killed 0 is 9 | # returned and 1 otherwise. 10 | # 11 | # NOTE: script depends on the tool jq, used to parse json, and curl to access URLs 12 | # 13 | 14 | # Initialize arguments 15 | CURL_BIN="/usr/bin/curl" 16 | 17 | # URL for jibri status 18 | STATUS_URL="http://localhost:2222/jibri/api/v1.0/health" 19 | # URL to POST to signal jibri stop/cleanup 20 | STOP_URL="http://localhost:2222/jibri/api/v1.0/stopService" 21 | 22 | # wait this long before failing stop curl command 23 | STOP_TIMEOUT=3600 24 | # wait this long before failing status curl command 25 | STATUS_TIMEOUT=30 26 | 27 | # sleep this long between checking jibri status 28 | SLEEP_TIME=10 29 | # delay shutdown AT MOST 6 hours = 21600 seconds 30 | TERMINATION_DELAY_TIMEOUT=21600 31 | 32 | function getJibriStatus() { 33 | $CURL_BIN --max-time $STATUS_TIMEOUT $STATUS_URL 2>/dev/null 34 | } 35 | 36 | function stopJibri() { 37 | $CURL_BIN -H 'Content-Type: application/json' -d '{}' --max-time $STOP_TIMEOUT $STOP_URL 2>/dev/null 38 | } 39 | 40 | function isRecording() { 41 | STATUS=`getJibriStatus` 42 | echo $STATUS | jq -r ".status.busyStatus" 43 | } 44 | 45 | verbose=1 46 | 47 | # Parse arguments 48 | OPTIND=1 49 | while getopts "h:t:s" opt; do 50 | case "$opt" in 51 | h) 52 | STATUS_URL=$OPTARG 53 | ;; 54 | t) 55 | STATUS_TIMEOUT=$OPTARG 56 | ;; 57 | s) 58 | verbose=0 59 | ;; 60 | esac 61 | done 62 | shift "$((OPTIND-1))" 63 | 64 | 65 | # Prints info messages 66 | function printInfo { 67 | if [ "$verbose" == "1" ] 68 | then 69 | echo "$@" 70 | fi 71 | } 72 | 73 | # Prints errors 74 | function printError { 75 | echo "$@" 1>&2 76 | } 77 | 78 | function gracefulShutdownJibri() { 79 | /opt/jitsi/jibri/graceful_shutdown.sh 80 | } 81 | 82 | pid=$(/bin/systemctl show -p MainPID jibri 2>/dev/null | cut -d= -f2) 83 | 84 | echo -n "Graceful shutdown signal..." 85 | gracefulShutdownJibri 86 | shutdownStatus=$? 87 | echo "sent" 88 | if [ $shutdownStatus == 0 ]; 89 | then 90 | printInfo "Graceful shutdown started" 91 | recordingStatus=`isRecording` 92 | SLEEP_COUNT=0 93 | while [[ $recordingStatus == "BUSY" ]]; do 94 | if [[ $SLEEP_COUNT -ge $TERMINATION_DELAY_TIMEOUT ]]; then 95 | printInfo "WAITED $TERMINATION_DELAY_TIMEOUT seconds, stopping jibri." 96 | # send signal to stop streaming/recording and upload any recordings pending 97 | stopJibri 98 | # mark jibri service as intentionally stopped to avoid restarts 99 | /usr/sbin/service jibri stop 100 | # stop looping 101 | break; 102 | fi 103 | printInfo "A recording is in progress" 104 | sleep $SLEEP_TIME 105 | SLEEP_COUNT=$(( $SLEEP_COUNT + $SLEEP_TIME )) 106 | recordingStatus=`isRecording` 107 | done 108 | 109 | sleep 5 110 | 111 | # mark jibri service as intentionally stopped to avoid restarts 112 | /usr/sbin/service jibri stop 113 | 114 | if ps -p $pid > /dev/null 2>&1 115 | then 116 | printInfo "It is still running, lets give it $STATUS_TIMEOUT seconds" 117 | sleep $STATUS_TIMEOUT 118 | if ps -p $pid > /dev/null 2>&1 119 | then 120 | printError "Jibri did not exit after $STATUS_TIMEOUT sec - killing $pid" 121 | kill $pid 122 | else 123 | printInfo "Jibri shutdown OK" 124 | exit 0 125 | fi 126 | else 127 | printInfo "Jibri shutdown OK" 128 | exit 0 129 | fi 130 | # check for 3 seconds if we managed to kill 131 | for I in 1 2 3 132 | do 133 | if ps -p $pid > /dev/null 2>&1 134 | then 135 | sleep 1 136 | fi 137 | done 138 | if ps -p $pid > /dev/null 2>&1 139 | then 140 | printError "Failed to kill $pid" 141 | printError "Sending force kill to $pid" 142 | kill -9 $pid 143 | if ps -p $pid > /dev/null 2>&1 144 | then 145 | printError "Failed to force kill $pid" 146 | exit 1 147 | fi 148 | fi 149 | printInfo "Jibri shutdown OK" 150 | exit 0 151 | else 152 | printError "Failed to signal shutdown of Jibri: $shutdownStatus" 153 | exit 1 154 | fi -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/selenium/status_checks/EmptyCallStatusCheckTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.selenium.status_checks 18 | 19 | import io.kotest.core.spec.IsolationMode 20 | import io.kotest.core.spec.style.ShouldSpec 21 | import io.kotest.matchers.shouldBe 22 | import io.mockk.every 23 | import io.mockk.mockk 24 | import io.mockk.spyk 25 | import org.jitsi.jibri.helpers.minutes 26 | import org.jitsi.jibri.helpers.seconds 27 | import org.jitsi.jibri.selenium.SeleniumEvent 28 | import org.jitsi.jibri.selenium.pageobjects.CallPage 29 | import org.jitsi.utils.logging2.Logger 30 | import org.jitsi.utils.time.FakeClock 31 | import java.time.Duration 32 | 33 | internal class EmptyCallStatusCheckTest : ShouldSpec() { 34 | override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf 35 | 36 | private val clock: FakeClock = spyk() 37 | private val callPage: CallPage = mockk() 38 | private val logger: Logger = mockk(relaxed = true) 39 | 40 | private val check = EmptyCallStatusCheck(logger, clock = clock) 41 | 42 | init { 43 | context("when the call was always empty") { 44 | every { callPage.isCallEmpty() } returns true 45 | context("the check") { 46 | should("return empty after the timeout") { 47 | check.run(callPage) shouldBe null 48 | clock.elapse(15.seconds) 49 | check.run(callPage) shouldBe null 50 | clock.elapse(20.seconds) 51 | check.run(callPage) shouldBe SeleniumEvent.CallEmpty 52 | } 53 | } 54 | } 55 | context("when the call has participants") { 56 | every { callPage.isCallEmpty() } returns false 57 | clock.elapse(5.minutes) 58 | context("the check") { 59 | should("never return empty") { 60 | check.run(callPage) shouldBe null 61 | clock.elapse(10.minutes) 62 | check.run(callPage) shouldBe null 63 | } 64 | } 65 | context("and then goes empty") { 66 | every { callPage.isCallEmpty() } returns true 67 | clock.elapse(20.seconds) 68 | context("the check") { 69 | should("return empty after the timeout") { 70 | check.run(callPage) shouldBe null 71 | clock.elapse(31.seconds) 72 | check.run(callPage) shouldBe SeleniumEvent.CallEmpty 73 | } 74 | } 75 | context("and then has participants again") { 76 | every { callPage.isCallEmpty() } returns false 77 | // Some time passed and the check ran once with no participants 78 | clock.elapse(30.seconds) 79 | context("the check") { 80 | should("never return empty") { 81 | check.run(callPage) shouldBe null 82 | clock.elapse(10.minutes) 83 | check.run(callPage) shouldBe null 84 | } 85 | } 86 | } 87 | } 88 | } 89 | context("when a custom timeout is passed") { 90 | every { callPage.isCallEmpty() } returns true 91 | val customTimeoutCheck = EmptyCallStatusCheck(logger, Duration.ofMinutes(10), clock) 92 | context("the check") { 93 | should("return empty after the timeout") { 94 | customTimeoutCheck.run(callPage) shouldBe null 95 | clock.elapse(15.seconds) 96 | customTimeoutCheck.run(callPage) shouldBe null 97 | clock.elapse(45.seconds) 98 | customTimeoutCheck.run(callPage) shouldBe null 99 | clock.elapse(8.minutes) 100 | customTimeoutCheck.run(callPage) shouldBe null 101 | clock.elapse(61.seconds) 102 | customTimeoutCheck.run(callPage) shouldBe SeleniumEvent.CallEmpty 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/util/JibriSubprocessTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.util 18 | 19 | import io.kotest.core.spec.IsolationMode 20 | import io.kotest.core.spec.style.ShouldSpec 21 | import io.kotest.matchers.collections.shouldBeEmpty 22 | import io.kotest.matchers.collections.shouldNotBeEmpty 23 | import io.kotest.matchers.shouldBe 24 | import io.kotest.matchers.types.shouldBeInstanceOf 25 | import io.mockk.Runs 26 | import io.mockk.every 27 | import io.mockk.just 28 | import io.mockk.mockk 29 | import io.mockk.slot 30 | import io.mockk.verify 31 | import org.jitsi.jibri.helpers.resetOutputLogger 32 | import org.jitsi.jibri.helpers.setTestOutputLogger 33 | import org.jitsi.utils.logging2.Logger 34 | 35 | internal class JibriSubprocessTest : ShouldSpec() { 36 | override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf 37 | 38 | private val processFactory: ProcessFactory = mockk() 39 | private val processWrapper: ProcessWrapper = mockk(relaxed = true) 40 | private val processStatePublisher: ProcessStatePublisher = mockk(relaxed = true) 41 | private val parentLogger: Logger = mockk(relaxed = true) 42 | 43 | @Suppress("MoveLambdaOutsideParentheses") 44 | private val subprocess = JibriSubprocess(parentLogger, "name", mockk(), processFactory, { processStatePublisher }) 45 | private val processStateHandler = slot<(ProcessState) -> Unit>() 46 | private val executorStateUpdates = mutableListOf() 47 | 48 | init { 49 | beforeSpec { 50 | LoggingUtils.setTestOutputLogger { _, _ -> 51 | mockk { 52 | every { get() } returns true 53 | every { get(any(), any()) } returns true 54 | } 55 | } 56 | 57 | every { processFactory.createProcess(any(), any(), any(), any()) } returns processWrapper 58 | every { processStatePublisher.addStatusHandler(capture(processStateHandler)) } just Runs 59 | 60 | subprocess.addStatusHandler { status -> 61 | executorStateUpdates.add(status) 62 | } 63 | } 64 | 65 | afterSpec { 66 | LoggingUtils.resetOutputLogger() 67 | } 68 | context("launching the subprocess") { 69 | context("without any error launching the process") { 70 | subprocess.launch(listOf()) 71 | should("not publish any state until the proc does") { 72 | executorStateUpdates.shouldBeEmpty() 73 | } 74 | context("when the process publishes a state") { 75 | val procState = ProcessState(ProcessRunning(), "most recent output") 76 | processStateHandler.captured(procState) 77 | should("bubble up the state update") { 78 | executorStateUpdates.shouldNotBeEmpty() 79 | executorStateUpdates[0] shouldBe procState 80 | } 81 | } 82 | } 83 | context("and the start process throwing") { 84 | every { processWrapper.start() } throws Exception() 85 | subprocess.launch(listOf()) 86 | should("publish a state update with the error") { 87 | executorStateUpdates.shouldNotBeEmpty() 88 | executorStateUpdates[0].runningState.shouldBeInstanceOf() 89 | } 90 | } 91 | } 92 | context("stopping the subprocess") { 93 | context("before launch") { 94 | should("not cause any errors") { 95 | subprocess.stop() 96 | } 97 | } 98 | context("after it launches") { 99 | subprocess.launch(emptyList()) 100 | context("when it refuses to stop gracefully") { 101 | every { processWrapper.stopAndWaitFor(any()) } returns false 102 | should("try and destroy it forcibly") { 103 | subprocess.stop() 104 | verify { processWrapper.destroyForciblyAndWaitFor(any()) } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/status/JibriStatusManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.jitsi.jibri.status 18 | 19 | import org.jitsi.jibri.error.JibriError 20 | import org.jitsi.jibri.util.StatusPublisher 21 | import org.jitsi.utils.logging.Logger 22 | import org.jitsi.xmpp.extensions.jibri.JibriIq 23 | import java.util.concurrent.ConcurrentHashMap 24 | import kotlin.properties.Delegates 25 | 26 | /** 27 | * The combination of a [ComponentHealthStatus] and an optional detail string to elaborate on the status 28 | */ 29 | data class ComponentHealthDetails( 30 | val healthStatus: ComponentHealthStatus, 31 | val detail: String = "" 32 | ) 33 | 34 | /** 35 | * The [ComponentHealthStatus] representing the overall health of the entire Jibri, as well as the 36 | * [ComponentHealthDetails] for each sub component 37 | */ 38 | data class OverallHealth( 39 | val healthStatus: ComponentHealthStatus, 40 | val details: Map 41 | ) 42 | 43 | /** 44 | * The overall status of this Jibri. This includes both its [ComponentBusyStatus] and its [OverallHealth] 45 | */ 46 | data class JibriStatus( 47 | val busyStatus: ComponentBusyStatus, 48 | val health: OverallHealth 49 | ) 50 | 51 | data class JibriFailure( 52 | val reason: JibriIq.FailureReason? = null, 53 | val error: JibriError? = null 54 | ) 55 | 56 | data class JibriSessionStatus( 57 | val sessionId: String, 58 | val status: JibriIq.Status, 59 | val sipAddress: String? = null, 60 | val failure: JibriFailure? = null, 61 | val shouldRetry: Boolean? = null 62 | ) 63 | 64 | /** 65 | * Models Jibri's overall status which currently consists of 2 things: 66 | * 1) Its 'busy' status: whether or not this Jibri is currently 'busy' 67 | * 2) Its health: whether or not this Jibri is capable of handling requests (regardless of 68 | * its busy status) 69 | */ 70 | class JibriStatusManager : StatusPublisher() { 71 | private val logger = Logger.getLogger(this::class.qualifiedName) 72 | private val subComponentHealth: MutableMap = ConcurrentHashMap() 73 | 74 | /** 75 | * The overall [ComponentHealthStatus] for the entire Jibri, calculated by aggregating all sub-component 76 | * health status 77 | */ 78 | private val overallHealthStatus: ComponentHealthStatus 79 | get() { 80 | return subComponentHealth.values.fold(ComponentHealthStatus.HEALTHY) { overallStatus, currDetails -> 81 | overallStatus.and(currDetails.healthStatus) 82 | } 83 | } 84 | 85 | /** 86 | * The overall health status plus any health details for each component 87 | */ 88 | private val overallHealth: OverallHealth 89 | get() { 90 | return OverallHealth( 91 | overallHealthStatus, 92 | subComponentHealth.toMap() 93 | ) 94 | } 95 | 96 | /** 97 | * The overall status contains both the health and busy statuses 98 | */ 99 | val overallStatus: JibriStatus 100 | get() = JibriStatus(busyStatus, overallHealth) 101 | 102 | /** 103 | * The busy status for this Jibri 104 | */ 105 | var busyStatus: ComponentBusyStatus by Delegates.observable(ComponentBusyStatus.IDLE) { _, old, new -> 106 | if (old != new) { 107 | logger.info("Busy status has changed: $old -> $new") 108 | publishStatus(overallStatus) 109 | } 110 | } 111 | 112 | /** 113 | * API for sub-components to update their health status 114 | */ 115 | @Synchronized 116 | fun updateHealth(componentName: String, healthStatus: ComponentHealthStatus, detail: String = "") { 117 | logger.info( 118 | "Received component health update: $componentName has status $healthStatus " + 119 | "(detail: $detail)" 120 | ) 121 | val oldHealthStatus = overallHealthStatus 122 | subComponentHealth[componentName] = ComponentHealthDetails(healthStatus, detail) 123 | val newHealthStatus = overallHealthStatus 124 | if (oldHealthStatus != newHealthStatus) { 125 | logger.info("Health status has changed: $oldHealthStatus -> $newHealthStatus") 126 | publishStatus(overallStatus) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/selenium/status_checks/MediaReceivedStatusCheck.kt: -------------------------------------------------------------------------------- 1 | package org.jitsi.jibri.selenium.status_checks 2 | 3 | import org.jitsi.jibri.config.Config 4 | import org.jitsi.jibri.selenium.SeleniumEvent 5 | import org.jitsi.jibri.selenium.pageobjects.CallPage 6 | import org.jitsi.metaconfig.config 7 | import org.jitsi.utils.logging2.Logger 8 | import org.jitsi.utils.logging2.createChildLogger 9 | import java.time.Clock 10 | import java.time.Duration 11 | 12 | /** 13 | * Verify that the Jibri web client is receiving media from the other participants. There's two different cases: 14 | * 1. If some participants have audio and/or video enabled (as advertised in signaling). In this case, if jibri 15 | * receives no audio or video for [noMediaTimeout] it will fire a [SeleniumEvent.NoMediaReceived]. This should not 16 | * happen in normal circumstances because if anyone is sending media, it should be forwarded to and received by jibri, 17 | * and we have a separate [IceConnectionStatusCheck] for the connection to the bridge. The check is here as a safety 18 | * net. 19 | * 2. No participant has audio or video enabled (as advertised in signaling). In this case we timeout after 20 | * [allMutedTimeout] and fire a [SeleniumEvent.CallEmpty] to avoid wasting resources. 21 | */ 22 | class MediaReceivedStatusCheck( 23 | parentLogger: Logger, 24 | private val clock: Clock = Clock.systemDefaultZone() 25 | ) : CallStatusCheck { 26 | private val logger = createChildLogger(parentLogger) 27 | 28 | // The last timestamp where we saw non-zero media. We default with the 29 | // assumption we're receiving media. 30 | private var timeOfLastMedia = clock.instant() 31 | 32 | // The timestamp at which we last saw that all clients transitioned to muted 33 | private val clientsAllMutedTransitionTime = StateTransitionTimeTracker(clock) 34 | 35 | override fun run(callPage: CallPage): SeleniumEvent? { 36 | val now = clock.instant() 37 | val bitrates = callPage.getBitrates() 38 | // getNumParticipants includes Jibri, so subtract 1 39 | val numParticipants = callPage.getNumParticipants() - 1 40 | val numMutedParticipants = callPage.numRemoteParticipantsMuted() 41 | val numJigasiParticipants = callPage.numRemoteParticipantsJigasi() 42 | val numHiddenParticipants = callPage.numHiddenParticipants() 43 | // We don't get any mute state for Jigasi participants, so to prevent timing out when only Jigasi participants 44 | // may be speaking, always count them as "muted" 45 | val allClientsMuted = (numMutedParticipants + numJigasiParticipants) == numParticipants 46 | logger.info( 47 | "Jibri client receive bitrates: $bitrates, num participants: $numParticipants, " + 48 | "numMutedParticipants: $numMutedParticipants, numJigasis: $numJigasiParticipants, " + 49 | "numHiddenParticipants: $numHiddenParticipants, all clients muted? $allClientsMuted" 50 | ) 51 | clientsAllMutedTransitionTime.maybeUpdate(allClientsMuted) 52 | val downloadBitrate = bitrates.getOrDefault("download", 0L) as Long 53 | // If all clients are muted, register it as 'receiving media': that way when clients unmute 54 | // we'll get the full noMediaTimeout duration before timing out due to lack of media. 55 | if (downloadBitrate != 0L || allClientsMuted) { 56 | timeOfLastMedia = now 57 | } 58 | val timeSinceLastMedia = Duration.between(timeOfLastMedia, now) 59 | 60 | // There are a couple possible outcomes here: 61 | // 1) All clients are muted, but have been muted for longer than allMutedTimeout so 62 | // we'll exit the call gracefully (CallEmpty) 63 | // 2) No media has flowed for longer than noMediaTimeout and all clients are not 64 | // muted so we'll exit with an error (NoMediaReceived) 65 | // 3) If neither of the above are true, we're fine and no event has occurred 66 | return when { 67 | clientsAllMutedTransitionTime.exceededTimeout(allMutedTimeout) -> SeleniumEvent.CallEmpty 68 | timeSinceLastMedia > noMediaTimeout && !allClientsMuted -> SeleniumEvent.NoMediaReceived 69 | else -> null 70 | } 71 | } 72 | 73 | companion object { 74 | /** 75 | * How long we'll stay in the call if we're not receiving any incoming media (assuming not all participants 76 | * not muted). This should be long enough to allow for a participant losing their connection and re-connecting 77 | * without restarting the recording. 78 | */ 79 | val noMediaTimeout: Duration by config { 80 | "jibri.call-status-checks.no-media-timeout".from(Config.configSource) 81 | } 82 | 83 | /** 84 | * How long we'll stay in the call if all participants are muted 85 | */ 86 | val allMutedTimeout: Duration by config { 87 | "jibri.call-status-checks.all-muted-timeout".from(Config.configSource) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jitsi/jibri/capture/ffmpeg/executor/OutputParserTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 Atlassian Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.jitsi.jibri.capture.ffmpeg.executor 19 | 20 | import io.kotest.core.spec.IsolationMode 21 | import io.kotest.core.spec.style.ShouldSpec 22 | import io.kotest.matchers.should 23 | import io.kotest.matchers.shouldBe 24 | import io.kotest.matchers.types.beInstanceOf 25 | import org.jitsi.jibri.capture.ffmpeg.FfmpegErrorStatus 26 | import org.jitsi.jibri.capture.ffmpeg.OutputLineClassification 27 | import org.jitsi.jibri.capture.ffmpeg.OutputParser 28 | import org.jitsi.jibri.status.ErrorScope 29 | 30 | internal class OutputParserTest : ShouldSpec() { 31 | override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf 32 | 33 | init { 34 | context("An encoding output line") { 35 | val outputLine = 36 | "frame= 95 fps= 31 q=27.0 size= 584kB time=00:00:03.60 bitrate=1329.4kbits/s speed=1.19x" 37 | // val expectedValues = mapOf( 38 | // "frame" to "95", 39 | // "fps" to "31", 40 | // "q" to "27.0", 41 | // "size" to "584kB", 42 | // "time" to "00:00:03.60", 43 | // "bitrate" to "1329.4kbits/s", 44 | // "speed" to "1.19x" 45 | // ) 46 | 47 | should("be parsed correctly") { 48 | val status = OutputParser.parse(outputLine) 49 | status.lineType shouldBe OutputLineClassification.ENCODING 50 | status.detail shouldBe outputLine 51 | } 52 | } 53 | context("A warning output line") { 54 | val outputLine = "Past duration 0.622368 too large" 55 | should("be parsed correctly") { 56 | val status = OutputParser.parse(outputLine) 57 | status.lineType shouldBe OutputLineClassification.UNKNOWN 58 | status.detail.shouldBe(outputLine) 59 | } 60 | } 61 | context("An error output line") { 62 | context("with an error on the session scope") { 63 | val outputLine = "rtmp://a.rtmp.youtube.com/live2/dkafkjlafkjhsadf: Input/output error" 64 | should("be parsed correctly") { 65 | val status = OutputParser.parse(outputLine) 66 | status should beInstanceOf() 67 | status as FfmpegErrorStatus 68 | status.detail.shouldBe(outputLine) 69 | status.error.scope shouldBe ErrorScope.SESSION 70 | } 71 | } 72 | } 73 | context("A broken pipe output line") { 74 | val outputLine = "av_interleaved_write_frame(): Broken pipe" 75 | should("be parsed correctly") { 76 | val status = OutputParser.parse(outputLine) 77 | status should beInstanceOf() 78 | status as FfmpegErrorStatus 79 | status.detail.shouldBe(outputLine) 80 | status.error.scope shouldBe ErrorScope.SESSION 81 | } 82 | } 83 | context("An unexpected exit output line") { 84 | val outputLine = "Exiting normally, received signal 15." 85 | should("be parsed correctly") { 86 | val status = OutputParser.parse(outputLine) 87 | status should beInstanceOf() 88 | status as FfmpegErrorStatus 89 | status.detail.shouldBe(outputLine) 90 | status.error.scope shouldBe ErrorScope.SESSION 91 | } 92 | } 93 | context("An expected exit output line") { 94 | val outputLine = "Exiting normally, received signal 2." 95 | should("be parsed correctly") { 96 | val status = OutputParser.parse(outputLine) 97 | status.lineType shouldBe OutputLineClassification.FINISHED 98 | status.detail.shouldBe(outputLine) 99 | } 100 | } 101 | context("An unknonwn output line") { 102 | val outputLine = "some unknown ffmpeg status" 103 | should("be parsed correctly") { 104 | val status = OutputParser.parse(outputLine) 105 | status.lineType shouldBe OutputLineClassification.UNKNOWN 106 | status.detail.shouldBe(outputLine) 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jitsi/jibri/service/JibriServiceStateMachine.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright @ 2018 - present 8x8, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.jitsi.jibri.service 18 | 19 | import com.tinder.StateMachine 20 | import org.jitsi.jibri.status.ComponentState 21 | import org.jitsi.jibri.util.NotifyingStateMachine 22 | 23 | sealed class JibriServiceEvent { 24 | class SubComponentStartingUp(val componentId: String, val subState: ComponentState.StartingUp) : JibriServiceEvent() 25 | class SubComponentRunning(val componentId: String, val subState: ComponentState.Running) : JibriServiceEvent() 26 | class SubComponentError(val componentId: String, val subState: ComponentState.Error) : JibriServiceEvent() 27 | class SubComponentFinished(val componentId: String, val subState: ComponentState.Finished) : JibriServiceEvent() 28 | } 29 | 30 | fun ComponentState.toJibriServiceEvent(componentId: String): JibriServiceEvent { 31 | return when (this) { 32 | is ComponentState.StartingUp -> JibriServiceEvent.SubComponentStartingUp(componentId, this) 33 | is ComponentState.Running -> JibriServiceEvent.SubComponentRunning(componentId, this) 34 | is ComponentState.Error -> JibriServiceEvent.SubComponentError(componentId, this) 35 | is ComponentState.Finished -> JibriServiceEvent.SubComponentFinished(componentId, this) 36 | } 37 | } 38 | 39 | sealed class JibriServiceSideEffect 40 | 41 | /** 42 | * A state machine for the services to use to handle status updates from their subcomponents. Subcomponents should 43 | * be registered via [JibriServiceStateMachine.registerSubComponent] such that the [JibriService]'s overall state 44 | * can be computed as a function of the subcomponents' states. 45 | */ 46 | class JibriServiceStateMachine : NotifyingStateMachine() { 47 | private val stateMachine = StateMachine.create { 48 | initialState(ComponentState.StartingUp) 49 | 50 | state { 51 | on { 52 | subComponentStates[it.componentId] = it.subState 53 | transitionTo(ComponentState.Error(it.subState.error)) 54 | } 55 | on { 56 | subComponentStates[it.componentId] = it.subState 57 | transitionTo(ComponentState.Finished) 58 | } 59 | on { 60 | subComponentStates[it.componentId] = it.subState 61 | if (subComponentStates.values.all { it is ComponentState.Running }) { 62 | transitionTo(ComponentState.Running) 63 | } else { 64 | dontTransition() 65 | } 66 | } 67 | on { 68 | dontTransition() 69 | } 70 | } 71 | 72 | state { 73 | on { 74 | subComponentStates[it.componentId] = it.subState 75 | transitionTo(ComponentState.Error(it.subState.error)) 76 | } 77 | on { 78 | subComponentStates[it.componentId] = it.subState 79 | transitionTo(ComponentState.Finished) 80 | } 81 | } 82 | 83 | state { 84 | on(any()) { 85 | dontTransition() 86 | } 87 | } 88 | 89 | state { 90 | on(any()) { 91 | dontTransition() 92 | } 93 | } 94 | 95 | onTransition { 96 | val validTransition = it as? StateMachine.Transition.Valid ?: run { 97 | throw Exception("Invalid state transition: $it") 98 | } 99 | if (validTransition.fromState::class != validTransition.toState::class) { 100 | notify(validTransition.fromState, validTransition.toState) 101 | } 102 | } 103 | } 104 | 105 | private val subComponentStates = mutableMapOf() 106 | 107 | fun registerSubComponent(componentKey: String) { 108 | // TODO: we'll assume everything starts in 'starting up' ? 109 | subComponentStates[componentKey] = ComponentState.StartingUp 110 | } 111 | 112 | fun transition(event: JibriServiceEvent): StateMachine.Transition<*, *, *> = stateMachine.transition(event) 113 | } 114 | --------------------------------------------------------------------------------