Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative Recording Mode Using Local Recording #532

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/main/kotlin/org/jitsi/jibri/selenium/JibriSelenium.kt
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ val RECORDING_URL_OPTIONS = listOf(
*/
class JibriSelenium(
parentLogger: Logger,
val sessionId: String,
private val jibriSeleniumOptions: JibriSeleniumOptions = JibriSeleniumOptions()
) : StatusPublisher<ComponentState>() {
private val logger = createChildLogger(parentLogger)
Expand All @@ -187,6 +188,10 @@ class JibriSelenium(
init {
System.setProperty("webdriver.chrome.logfile", "/tmp/chromedriver.log")
val chromeOptions = ChromeOptions()
val prefs = HashMap<String, Any>()
prefs["download.default_directory"] = "/recordings/$sessionId"
prefs["download.prompt_for_download"] = false
chromeOptions.setExperimentalOption("prefs", prefs)
chromeOptions.addArguments(chromeOpts)
chromeOptions.setExperimentalOption("w3c", false)
chromeOptions.addArguments(jibriSeleniumOptions.extraChromeCommandLineFlags)
Expand Down Expand Up @@ -365,6 +370,8 @@ class JibriSelenium(
logger.info("Chrome driver quit")
}

fun getChromeDriver(): ChromeDriver = chromeDriver

companion object {
private val browserOutputLogger = getLoggerWithHandler("browser", BrowserFileHandler())
const val COMPONENT_ID = "Selenium"
Expand Down
81 changes: 81 additions & 0 deletions src/main/kotlin/org/jitsi/jibri/selenium/util/SeleniumUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.jitsi.jibri.selenium.util

import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.support.ui.ExpectedCondition
import org.openqa.selenium.support.ui.ExpectedConditions
import org.openqa.selenium.support.ui.WebDriverWait

/**
* Utility class.
* @author Damian Minkov
* @author Pawel Domas
*/
object SeleniumUtils {
var IS_LINUX: Boolean = false

var IS_MAC: Boolean = false

init {
// OS
val osName: String? = System.getProperty("os.name")

when {
osName == null -> {
IS_LINUX = false
IS_MAC = false
}
osName.startsWith(prefix = "Linux") -> {
IS_LINUX = true
IS_MAC = false
}
osName.startsWith(prefix = "Mac") -> {
IS_LINUX = false
IS_MAC = true
}
else -> {
IS_LINUX = false
IS_MAC = false
}
}
}

/**
* Click an element on the page by first checking for visibility and then
* checking for clickability.
*
* @param driver the `WebDriver`.
* @param by the search query for the element
*/
fun click(driver: WebDriver, by: By) {
waitForElementBy(driver = driver, by = by, timeout = 10)
val wait = WebDriverWait(driver, 10)
val element: WebElement = wait.until(ExpectedConditions.elementToBeClickable(by))
element.click()
}

/**
* Waits until an element becomes available and return it.
* @param driver the `WebDriver`.
* @param by the xpath to search for the element
* @param timeout the time to wait for the element in seconds.
* @return WebElement the found element
*/
private fun waitForElementBy(driver: WebDriver, by: By, timeout: Long): WebElement? {
var foundElement: WebElement? = null
WebDriverWait(driver, timeout)
.until<Boolean>(ExpectedCondition<Boolean?> { d: WebDriver? ->
val elements: List<WebElement> = d!!.findElements(by)
when {
elements.isNotEmpty() -> {
foundElement = elements[0]
return@ExpectedCondition true
}
else -> return@ExpectedCondition false
}
})

return foundElement
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ import org.jitsi.xmpp.extensions.jibri.JibriIq
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.concurrent.TimeUnit
import kotlin.io.path.listDirectoryEntries
import org.jitsi.jibri.selenium.util.SeleniumUtils
import org.openqa.selenium.By
import org.openqa.selenium.chrome.ChromeDriver

/**
* Parameters needed for starting a [FileRecordingJibriService]
Expand Down Expand Up @@ -106,7 +112,7 @@ class FileRecordingJibriService(
}

private val capturer = capturer ?: FfmpegCapturer(logger)
private val jibriSelenium = jibriSelenium ?: JibriSelenium(logger)
private val jibriSelenium = jibriSelenium ?: JibriSelenium(logger, fileRecordingParams.sessionId)

/**
* The [Sink] this class will use to model the file on the filesystem
Expand All @@ -123,6 +129,10 @@ class FileRecordingJibriService(
"jibri.recording.finalize-script".from(Config.configSource)
}

private val recordingMode: String by config {
"jibri.recording.mode".from(Config.configSource)
}

/**
* The directory in which we'll store recordings for this particular session. This is a directory that will
* be nested within [recordingsDirectory].
Expand Down Expand Up @@ -170,7 +180,13 @@ class FileRecordingJibriService(
jibriSelenium.addToPresence("session_id", fileRecordingParams.sessionId)
jibriSelenium.addToPresence("mode", JibriIq.RecordingMode.FILE.toString())
jibriSelenium.sendPresence()
capturer.start(sink)
when (recordingMode) {
"ffmpeg" -> capturer.start(sink)
else -> {
startAndStopRecordingWithSelenium(driver = jibriSelenium.getChromeDriver())
publishStatus(status = ComponentState.Running)
}
}
} catch (t: Throwable) {
logger.error("Error while setting fields in presence", t)
publishStatus(ComponentState.Error(ErrorSettingPresenceFields))
Expand All @@ -180,8 +196,39 @@ class FileRecordingJibriService(

override fun stop() {
logger.info("Stopping capturer")
capturer.stop()
when (recordingMode) {
"ffmpeg" -> capturer.stop()
else -> startAndStopRecordingWithSelenium(driver = jibriSelenium.getChromeDriver(), type = "stop")
}
logger.info("Quitting selenium")
when {
recordingMode != "ffmpeg" -> {
var found = false
val startTime: Long = System.currentTimeMillis()
while (!Files.exists(sink.file)) {
sessionRecordingDirectory.listDirectoryEntries()
.forEach { entry: Path ->
run {
val fileName: Path = entry.fileName
when {
fileName.toString().endsWith(suffix = ".webm") ->
logger.info { "webm found: $entry. File is renaming from: ${sink.file}" }
.run {
found = true
sink.file = sessionRecordingDirectory.resolve(entry)
sink.format = "webm"
}

else -> logger.warn { "Unhandled file: $fileName" }
}
}
}
if (found || System.currentTimeMillis() - startTime > 30 * 1_000)
break
TimeUnit.SECONDS.sleep(1).also { logger.info { "Media was not found, sleeping..." } }
}
}
}

// It's possible that the service was stopped before we even wrote anything, so check if we actually wrote
// any data to disk. If not, we'll skip writing the metadata and running the finalize script and instead
Expand All @@ -192,7 +239,7 @@ class FileRecordingJibriService(
try {
Files.delete(sessionRecordingDirectory)
} catch (t: Throwable) {
logger.error("Problem deleting session recording directory", t)
logger.error("Problem deleting session recording directory. ${t.message}")
}
jibriSelenium.leaveCallAndQuitBrowser()
return
Expand All @@ -203,7 +250,7 @@ class FileRecordingJibriService(
} catch (t: Throwable) {
logger.error(
"An error occurred while trying to get the participants list, proceeding with " +
"an empty participants list",
"an empty participants list. ${t.message}",
t
)
listOf<Map<String, Any>>()
Expand Down Expand Up @@ -232,8 +279,22 @@ class FileRecordingJibriService(
logger.info("Finalizing the recording")
jibriServiceFinalizer?.doFinalize()
}

/**
* Starts or stops local recording with Selenium.

* @param driver The ChromeDriver instance.
* @param type The type of action to perform, either "start" or "stop" default is "start"
*/
private fun startAndStopRecordingWithSelenium(driver: ChromeDriver, type: String = "start") {
logger.info { "startAndStopRecordingWithSelenium called. id: ${jibriSelenium.sessionId}, type: $type" }
SeleniumUtils.click(driver, By.id("more-actions-id"))
SeleniumUtils.click(driver, By.id("menu-item-local-recording"))
SeleniumUtils.click(driver, By.id("modal-dialog-ok-button"))
logger.info { "Sleeping 5 seconds..." }.run { TimeUnit.SECONDS.sleep(5) }
}
}

object ErrorCreatingRecordingsDirectory : JibriError(ErrorScope.SYSTEM, "Could not creat recordings director")
object RecordingsDirectoryNotWritable : JibriError(ErrorScope.SYSTEM, "Recordings directory is not writable")
object CouldntWriteMeetingMetadata : JibriError(ErrorScope.SYSTEM, "Could not write meeting metadata")
object CouldntWriteMeetingMetadata : JibriError(ErrorScope.SYSTEM, "Could not write meeting metadata")
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class SipGatewayJibriService(
*/
private val jibriSelenium = jibriSelenium ?: JibriSelenium(
logger,
"",
JibriSeleniumOptions(
displayName = if (sipGatewayServiceParams.callParams.displayName.isNotBlank()) {
sipGatewayServiceParams.callParams.displayName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class StreamingJibriService(
}
private val capturer = FfmpegCapturer(logger)
private val sink: Sink
private val jibriSelenium = JibriSelenium(logger)
private val jibriSelenium = JibriSelenium(logger, streamingParams.sessionId)

private val rtmpAllowList: List<Pattern> by config {
"jibri.streaming.rtmp-allow-list".from(Config.configSource)
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/org/jitsi/jibri/sink/impl/FileSink.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ import java.time.format.DateTimeFormatter
* over the value anyway. Because of that I just made the value hard-coded.
*/
class FileSink(recordingsDirectory: Path, callName: String, extension: String = "mp4") : Sink {
val file: Path
var file: Path
init {
val suffix = "_${LocalDateTime.now().format(TIMESTAMP_FORMATTER)}.$extension"
val filename = "${callName.take(MAX_FILENAME_LENGTH - suffix.length)}$suffix"
file = recordingsDirectory.resolve(filename)
}
override val path: String = file.toString()
override val format: String = extension
override var format: String = extension
override val options: Array<String> = arrayOf(
"-profile:v",
"main",
Expand Down