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

#796 Improved the screen recorder for creating more than 3 minutes long videos #939

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c0f879a
Improved the screen recorder for creating than 3 minutes videos
SergKhram May 24, 2024
4adcb17
Beautify
SergKhram May 24, 2024
f0474d9
Merge branch 'develop' into feature/ISSUE-796/improve-screen-recorder
Malinskiy May 24, 2024
aa07237
Feature toggle has added
SergKhram May 24, 2024
194fffc
Merge remote-tracking branch 'origin/feature/ISSUE-796/improve-screen…
SergKhram May 24, 2024
a94214d
Fixed the comments
SergKhram May 24, 2024
62186d9
Fixed the comments p2
SergKhram May 24, 2024
f482f12
Fix startTime
SergKhram May 24, 2024
aac6021
Made work with map more safety
SergKhram May 25, 2024
17bc268
Bottleneck fixed
SergKhram May 25, 2024
5b1cbc7
Updated the docs
SergKhram May 25, 2024
18ddb75
Merge branch 'develop' into feature/ISSUE-796/improve-screen-recorder
SergKhram Jun 3, 2024
ba88d7d
Merge branch 'develop' into feature/ISSUE-796/improve-screen-recorder
Malinskiy Jun 13, 2024
2dcdf08
Fixed the comments
SergKhram Jun 13, 2024
ed82b6f
Merge branch 'develop' into feature/ISSUE-796/improve-screen-recorder
Malinskiy Jun 15, 2024
5316ba3
Removed the featureToggle
SergKhram Jun 16, 2024
3afa536
Merge branch 'develop' into feature/ISSUE-796/improve-screen-recorder
SergKhram Jun 18, 2024
51d7cc0
Merge branch 'develop' into feature/ISSUE-796/improve-screen-recorder
SergKhram Jun 22, 2024
4ab20e1
docs(demo): add android + ios demo
Malinskiy Nov 15, 2023
9f22612
Merge branch 'develop' into feature/ISSUE-796/improve-screen-recorder
SergKhram Jun 28, 2024
3431df5
Merge branch 'develop' into feature/ISSUE-796/improve-screen-recorder
SergKhram Jul 10, 2024
aae7032
Merge branch 'develop' into feature/ISSUE-796/improve-screen-recorder
SergKhram Jul 23, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ fun String.escape(): String {
}

val escapeRegex = "[^a-zA-Z0-9\\.\\#]".toRegex()

fun String.addFileNumberForVideo(fileNumber: String) = "${this.split(".mp4")[0]}$fileNumber.mp4"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like very specific thing for stringextensions. I suggest putting this somewhere closer to the video file creation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class RemoteFileManager(private val device: AndroidDevice) {
}

companion object {
const val MAX_FILENAME = 255
const val MAX_FILENAME = 254 //we need 1 more char for fileNumber in case of using video attachment > 180 && apiLevel < 34
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constant is not specific for video files so it should never change. It's a detail about Android filesystem implementation. I suggest changing the logic of the code, not the constants here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const val TMP_PATH = "/data/local/tmp"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import com.malinskiy.marathon.device.NetworkState
import com.malinskiy.marathon.device.file.measureFileTransfer
import com.malinskiy.marathon.exceptions.DeviceLostException
import com.malinskiy.marathon.execution.TestBatchResults
import com.malinskiy.marathon.extension.addFileNumberForVideo
import com.malinskiy.marathon.extension.escape
import com.malinskiy.marathon.extension.withTimeout
import com.malinskiy.marathon.extension.withTimeoutOrNull
Expand All @@ -74,6 +75,7 @@ import java.awt.image.BufferedImage
import java.io.File
import java.time.Duration
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
import com.malinskiy.marathon.android.model.ShellCommandResult as MarathonShellCommandResult

Expand Down Expand Up @@ -372,7 +374,27 @@ class AdamAndroidDevice(
remoteFilePath: String,
options: VideoConfiguration
) {
val screenRecorderCommand = options.toScreenRecorderCommand(remoteFilePath)
var secondsRemaining = TimeUnit.SECONDS.convert(options.timeLimit, options.timeLimitUnits)
if(secondsRemaining <= 180 || apiLevel >= 34) {
startScreenRecorder(remoteFilePath, options)
} else {
var recordsCount = 0
while(recordsCount == 0 || secondsRemaining>=180) {
SergKhram marked this conversation as resolved.
Show resolved Hide resolved
startScreenRecorder(remoteFilePath, options, recordsCount.toString()) {
secondsRemaining -= 180
recordsCount++
}
}
}
}

private suspend fun startScreenRecorder(
remoteFilePath: String,
options: VideoConfiguration,
fileNumber: String = "",
SergKhram marked this conversation as resolved.
Show resolved Hide resolved
finallyBlock: (() -> Unit)? = null
SergKhram marked this conversation as resolved.
Show resolved Hide resolved
) {
val screenRecorderCommand = options.toScreenRecorderCommand(remoteFilePath.addFileNumberForVideo(fileNumber), this)
try {
withTimeoutOrNull(androidConfiguration.timeoutConfiguration.screenrecorder) {
val result = client.execute(ShellCommandRequest(screenRecorderCommand), serial = adbSerial)
Expand All @@ -388,6 +410,8 @@ class AdamAndroidDevice(
logger.warn(e) { "screenrecord start was interrupted" }
} catch (e: Exception) {
logger.error("Unable to start screenrecord", e)
} finally {
finallyBlock?.invoke()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.malinskiy.marathon.device.DevicePoolId
import com.malinskiy.marathon.device.toDeviceInfo
import com.malinskiy.marathon.execution.Attachment
import com.malinskiy.marathon.execution.AttachmentType
import com.malinskiy.marathon.extension.addFileNumberForVideo
import com.malinskiy.marathon.extension.withTimeoutOrNull
import com.malinskiy.marathon.io.FileManager
import com.malinskiy.marathon.io.FileType
Expand All @@ -22,6 +23,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancelAndJoin
import java.io.File
import java.time.Duration
import kotlin.system.measureTimeMillis

Expand Down Expand Up @@ -122,29 +124,56 @@ class ScreenRecorderTestBatchListener(
}

private suspend fun pullTestVideo(test: Test) {
val localVideoFile = fileManager.createFile(FileType.VIDEO, pool, device.toDeviceInfo(), test, testBatchId)
val localVideoFiles = mutableListOf<File>()
val remoteFilePath = device.fileManager.remoteVideoForTest(test, testBatchId)
val millis = measureTimeMillis {
device.safePullFile(remoteFilePath, localVideoFile.toString())
if(device.apiLevel >= 34 || videoConfiguration.timeLimit <= 180) {
val localVideoFile = fileManager.createFile(FileType.VIDEO, pool, device.toDeviceInfo(), test, testBatchId)
device.safePullFile(remoteFilePath, localVideoFile.toString())
localVideoFiles.add(localVideoFile)
} else {
for (i in 0 .. (videoConfiguration.timeLimit / 180)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect that the actual number of files depends on the time the test took, not the configuration limit, so the loop should take into account only the time passed or/and number of shell invocations with screenrecord binary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

val localVideoFile = fileManager.createFile(FileType.VIDEO, pool, device.toDeviceInfo(), test, testBatchId, i.toString())
device.safePullFile(remoteFilePath.addFileNumberForVideo(i.toString()), localVideoFile.toString())
localVideoFiles.add(localVideoFile)
}
}
}
logger.trace { "Pulling finished in ${millis}ms $remoteFilePath " }
attachmentListeners.forEach { it.onAttachment(test, Attachment(localVideoFile, AttachmentType.VIDEO)) }
attachmentListeners.forEach {
localVideoFiles.forEach { localVideoFile ->
it.onAttachment(test, Attachment(localVideoFile, AttachmentType.VIDEO))
}
}
}

/**
* This can be called both when test times out and device unavailable
*/
private suspend fun pullLastBatchVideo(remoteFilePath: String) {
val localVideoFile = fileManager.createFile(FileType.VIDEO, pool, device.toDeviceInfo(), testBatchId = testBatchId)
val millis = measureTimeMillis {
device.safePullFile(remoteFilePath, localVideoFile.toString())
if(device.apiLevel >= 34 || videoConfiguration.timeLimit <= 180) {
val localVideoFile = fileManager.createFile(FileType.VIDEO, pool, device.toDeviceInfo(), testBatchId = testBatchId)
device.safePullFile(remoteFilePath, localVideoFile.toString())
} else {
for (i in 0 .. (videoConfiguration.timeLimit / 180)) {
val localVideoFile = fileManager.createFile(FileType.VIDEO, pool, device.toDeviceInfo(), testBatchId = testBatchId, id = i.toString())
device.safePullFile(remoteFilePath.addFileNumberForVideo(i.toString()), localVideoFile.toString())
}
}
}
logger.trace { "Pulling finished in ${millis}ms $remoteFilePath " }
}

private suspend fun removeRemoteVideo(remoteFilePath: String) {
val millis = measureTimeMillis {
device.fileManager.removeRemotePath(remoteFilePath)
if(device.apiLevel >= 34 || videoConfiguration.timeLimit <= 180) {
device.fileManager.removeRemotePath(remoteFilePath)
} else {
for (i in 0 .. (videoConfiguration.timeLimit / 180)) {
device.fileManager.removeRemotePath(remoteFilePath.addFileNumberForVideo(i.toString()))
}
}
}
logger.trace { "Removed file in ${millis}ms $remoteFilePath" }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.malinskiy.marathon.android.extension

import com.malinskiy.marathon.android.AndroidDevice
import com.malinskiy.marathon.android.model.AndroidTestBundle
import com.malinskiy.marathon.config.vendor.VendorConfiguration
import com.malinskiy.marathon.config.vendor.android.AndroidTestBundleConfiguration
import com.malinskiy.marathon.config.vendor.android.VideoConfiguration
import java.util.concurrent.TimeUnit

fun VideoConfiguration.toScreenRecorderCommand(remoteFilePath: String): String {
fun VideoConfiguration.toScreenRecorderCommand(remoteFilePath: String, device: AndroidDevice? = null): String {
val sb = StringBuilder()

sb.append("screenrecord")
Expand Down Expand Up @@ -36,7 +37,7 @@ fun VideoConfiguration.toScreenRecorderCommand(remoteFilePath: String): String {
if (timeLimit > 0) {
sb.append("--time-limit ")
var seconds = TimeUnit.SECONDS.convert(timeLimit, timeLimitUnits)
if (seconds > 180) {
if (seconds > 180 && ((device?.apiLevel ?: 0) < 34)) {
seconds = 180
}
sb.append(seconds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class RemoteFileManagerTest {
emptyList()
), "batch-id"
)
assertThat(actual).isEqualTo("/sdcard/pkg.clazz-testWithAVeryLongNameThatExceeds255CharactersqwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnmQWERT-batch-id.mp4")
assertThat(actual).isEqualTo("/sdcard/pkg.clazz-testWithAVeryLongNameThatExceeds255CharactersqwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnmQWER-batch-id.mp4")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,34 @@ package com.malinskiy.marathon.android

import assertk.assertThat
import assertk.assertions.isEqualTo
import com.malinskiy.adam.AndroidDebugBridgeClient
import com.malinskiy.adam.server.junit5.AdbClient
import com.malinskiy.adam.server.junit5.AdbServer
import com.malinskiy.adam.server.junit5.AdbTest
import com.malinskiy.adam.server.stub.AndroidDebugBridgeServer
import com.malinskiy.marathon.android.adam.TestConfigurationFactory
import com.malinskiy.marathon.android.adam.TestDeviceFactory
import com.malinskiy.marathon.android.adam.boot
import com.malinskiy.marathon.android.adam.features
import com.malinskiy.marathon.android.extension.toScreenRecorderCommand
import com.malinskiy.marathon.config.vendor.android.FileSyncConfiguration
import com.malinskiy.marathon.config.vendor.android.FileSyncEntry
import com.malinskiy.marathon.config.vendor.android.VideoConfiguration
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.mockito.kotlin.mock

@AdbTest
class VideoConfigurationTest {
@AdbClient
lateinit var client: AndroidDebugBridgeClient

@AdbServer
lateinit var server: AndroidDebugBridgeServer

@Test
fun testDefaults() {
assertThat(VideoConfiguration().toScreenRecorderCommand("/sdcard/video.mp4"))
Expand All @@ -18,4 +41,39 @@ class VideoConfigurationTest {
assertThat(VideoConfiguration(timeLimit = 200).toScreenRecorderCommand("/sdcard/video.mp4"))
.isEqualTo("screenrecord --size 720x1280 --bit-rate 1000000 --time-limit 180 /sdcard/video.mp4")
}

@ParameterizedTest
@MethodSource("apiLevels")
fun testLongVideoDependsOnApiLevel(sdkLevel: Int, expectedTimeLimit: Int) {
val configuration = TestConfigurationFactory.create(
fileSyncConfiguration = FileSyncConfiguration(
mutableSetOf(
FileSyncEntry(
"screenshots"
)
)
)
)
val device = TestDeviceFactory.create(client, configuration, mock())
runBlocking {
server.multipleSessions {
serial("emulator-5554") {
boot(sdk = sdkLevel)
}
features("emulator-5554")
}

device.setup()
}
assertThat(VideoConfiguration(timeLimit = 200).toScreenRecorderCommand("/sdcard/video.mp4", device))
.isEqualTo("screenrecord --size 720x1280 --bit-rate 1000000 --time-limit $expectedTimeLimit /sdcard/video.mp4")
}

companion object {
@JvmStatic
fun apiLevels() = listOf(
Arguments.of(33, 180),
Arguments.of(34, 200)
)
}
}
Loading