Skip to content

Commit

Permalink
[starter] Support recording on Linux via ffmpeg
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 356ee0c087620d10670ff11759e79061ea3fe554
  • Loading branch information
MaXal authored and intellij-monorepo-bot committed Jun 18, 2024
1 parent d1ddcfd commit fd6aba0
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.intellij.ide.starter.process.exec.ExecOutputRedirect
import com.intellij.ide.starter.process.exec.ProcessExecutor
import com.intellij.ide.starter.process.getProcessList
import com.intellij.ide.starter.runner.events.IdeLaunchEvent
import com.intellij.ide.starter.utils.getRunningDisplays
import com.intellij.tools.ide.starter.bus.EventsBus
import com.intellij.tools.ide.util.common.logOutput
import kotlinx.coroutines.async
Expand All @@ -15,10 +16,8 @@ import kotlin.collections.single
import kotlin.collections.singleOrNull
import kotlin.io.path.div
import kotlin.io.path.pathString
import kotlin.text.drop
import kotlin.text.split
import kotlin.text.startsWith
import kotlin.text.toInt
import kotlin.time.Duration.Companion.hours

object XorgWindowManagerHandler {
Expand All @@ -31,21 +30,6 @@ object XorgWindowManagerHandler {
private val xvfbName = "Xvfb"



private fun getRunningDisplays(): List<Int> {
logOutput("Looking for running displays")
val found = getProcessList()
.filter { it.command.contains(xvfbName) }.map {
logOutput(it.command)
it.command.split(" ")
.single { arg -> arg.startsWith(":") }
.drop(1)
.toInt()
}
logOutput("Found $xvfbName displays: $found")
return found
}

fun provideDisplay(): Int {
val displays = getRunningDisplays()
return displays.singleOrNull() ?: runXvfb()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ data class IDERunContext(
val launchName: String = "",
val expectedKill: Boolean = false,
val expectedExitCode: Int = 0,
val collectNativeThreads: Boolean = false
val collectNativeThreads: Boolean = false,
) {
val contextName: String
get() = if (launchName.isNotEmpty()) {
Expand Down Expand Up @@ -327,12 +327,14 @@ data class IDERunContext(
}
}

private suspend fun captureDiagnosticOnKill(logsDir: Path,
jdkHome: Path,
startConfig: IDEStartConfig,
pid: Long,
process: Process,
snapshotsDir: Path) {
private suspend fun captureDiagnosticOnKill(
logsDir: Path,
jdkHome: Path,
startConfig: IDEStartConfig,
pid: Long,
process: Process,
snapshotsDir: Path,
) {

catchAll {
takeScreenshot(logsDir)
Expand Down Expand Up @@ -378,12 +380,14 @@ data class IDERunContext(
}
}

suspend fun startCollectThreadDumpsLoop(logsDir: Path,
process: Process,
jdkHome: Path,
workDir: Path,
collectingProcessId: Long,
processName: String) {
suspend fun startCollectThreadDumpsLoop(
logsDir: Path,
process: Process,
jdkHome: Path,
workDir: Path,
collectingProcessId: Long,
processName: String,
) {
val monitoringThreadDumpDir = logsDir.resolve(processName).resolve("monitoring-thread-dumps").createDirectoriesIfNotExist()

var cnt = 0
Expand Down Expand Up @@ -448,17 +452,14 @@ data class IDERunContext(
addSystemProperty("idea.log.path", logsDir)
}

/**
* Make sure that tests are run with: `-Djava.awt.headless=false` option
*/
fun withScreenRecording() {
val screenRecorder = runCatching { IDEScreenRecorder(this) }.getOrNull()
EventsBus.subscribe(IDERunContext::javaClass) { _: IdeBeforeLaunchEvent ->
screenRecorder?.start()
val screenRecorder = IDEScreenRecorder(this)
EventsBus.subscribe(IDERunContext::javaClass) { _: IdeLaunchEvent ->
screenRecorder.start()
}

EventsBus.subscribe(IDERunContext::javaClass) { _: IdeAfterLaunchEvent ->
screenRecorder?.stop()
screenRecorder.stop()
}
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.intellij.ide.starter.screenRecorder

import com.intellij.ide.starter.runner.IDERunContext
import com.intellij.ide.starter.utils.getRunningDisplays
import com.intellij.openapi.util.SystemInfo
import com.intellij.tools.ide.util.common.logOutput
import org.monte.media.Format
import org.monte.media.FormatKeys.MediaType
import org.monte.media.VideoFormatKeys.*
Expand All @@ -9,18 +12,66 @@ import org.monte.screenrecorder.ScreenRecorder
import java.awt.GraphicsEnvironment
import java.awt.Rectangle
import java.awt.Toolkit
import kotlin.io.path.createFile
import kotlin.io.path.div
import kotlin.io.path.pathString

class IDEScreenRecorder(runContext: IDERunContext) : ScreenRecorder(
GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration,
Rectangle(0, 0, Toolkit.getDefaultToolkit().screenSize.width,
Toolkit.getDefaultToolkit().screenSize.height),
Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI),
Format(MediaTypeKey, MediaType.VIDEO, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE,
CompressorNameKey,
ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, DepthKey, 24, FrameRateKey,
Rational.valueOf(15.0), QualityKey, 1.0f,
KeyFrameIntervalKey, 15 * 60),
Format(MediaTypeKey, MediaType.VIDEO, EncodingKey, "black", FrameRateKey,
Rational.valueOf(30.0)), null,
(runContext.logsDir / "screenRecording").toFile())
class IDEScreenRecorder(private val runContext: IDERunContext) {
var javaScreenRecorder: ScreenRecorder? = null
var ffmpegProcess: Process? = null

init {
//on Linux, we run xvfb and test process is headless, so we need external tool to record screen
if (!SystemInfo.isLinux) {
javaScreenRecorder = runCatching {
ScreenRecorder(
GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration,
Rectangle(0, 0, Toolkit.getDefaultToolkit().screenSize.width,
Toolkit.getDefaultToolkit().screenSize.height),
Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI),
Format(MediaTypeKey, MediaType.VIDEO, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE,
CompressorNameKey,
ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, DepthKey, 24, FrameRateKey,
Rational.valueOf(15.0), QualityKey, 1.0f,
KeyFrameIntervalKey, 15 * 60),
Format(MediaTypeKey, MediaType.VIDEO, EncodingKey, "black", FrameRateKey,
Rational.valueOf(30.0)), null,
(runContext.logsDir / "screenRecording").toFile())
}.getOrNull()
}
}

fun start() {
if (javaScreenRecorder != null) {
javaScreenRecorder?.start()
}
else {
ffmpegProcess = runCatching { startFFMpegRecording(runContext) }.getOrElse {
logOutput("Can't start ffmpeg recording: ${it.message}")
null
}
}
}

fun stop(){
javaScreenRecorder?.stop()
ffmpegProcess?.destroy()
}

private fun startFFMpegRecording(ideRunContext: IDERunContext): Process? {
val resolution = "1920x1080"
val displayWithColumn = ":" + (getRunningDisplays().firstOrNull() ?: "0")
val recordingFile = ideRunContext.logsDir / "screen.mkv"
val ffmpegLogFile = (ideRunContext.logsDir / "ffmpeg.log").also { it.createFile() }
val args = listOf("/usr/bin/ffmpeg", "-f", "x11grab", "-video_size", resolution, "-framerate", "3", "-i",
displayWithColumn,
"-codec:v", "libx264", "-preset", "superfast", recordingFile.pathString)
logOutput("Start screen recording to $recordingFile\nArgs: ${args.joinToString(" ")}")

//we can't use ProcessExecutor since its start method is blocking and we need a handle to process to stop it
val processBuilder = ProcessBuilder(args)
processBuilder.redirectError(ffmpegLogFile.toFile())
processBuilder.redirectOutput(ffmpegLogFile.toFile())
return processBuilder.start()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.intellij.ide.starter.utils

import com.intellij.ide.starter.process.getProcessList
import com.intellij.tools.ide.util.common.logOutput

fun getRunningDisplays(): List<Int> {
logOutput("Looking for running displays")
val found = getProcessList()
.filter { it.command.contains("Xvfb") }.map {
logOutput(it.command)
it.command.split(" ")
.single { arg -> arg.startsWith(":") }
.drop(1)
.toInt()
}
logOutput("Found Xvfb displays: $found")
return found
}

0 comments on commit fd6aba0

Please sign in to comment.