Replace video transcoder with Media3 Transformer (#5018)
This commit is contained in:
parent
aaa0407bff
commit
e8154ec19d
7 changed files with 334 additions and 258 deletions
|
|
@ -100,6 +100,9 @@ androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process",
|
|||
androidx_splash = "androidx.core:core-splashscreen:1.0.1"
|
||||
androidx_media3_exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
||||
androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
|
||||
androidx_media3_transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
|
||||
androidx_media3_effect = { module = "androidx.media3:media3-effect", version.ref = "media3" }
|
||||
androidx_media3_common = { module = "androidx.media3:media3-common", version.ref = "media3" }
|
||||
androidx_biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
|
||||
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
|
||||
|
|
@ -182,7 +185,6 @@ sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions",
|
|||
sqlcipher = "net.zetetic:sqlcipher-android:4.9.0"
|
||||
sqlite = "androidx.sqlite:sqlite-ktx:2.5.2"
|
||||
unifiedpush = "org.unifiedpush.android:connector:3.0.10"
|
||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.2"
|
||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@ dependencies {
|
|||
implementation(projects.services.toolbox.api)
|
||||
implementation(libs.inject)
|
||||
implementation(libs.androidx.exifinterface)
|
||||
implementation(libs.androidx.media3.transformer)
|
||||
implementation(libs.androidx.media3.effect)
|
||||
implementation(libs.androidx.media3.common)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(libs.otaliastudios.transcoder)
|
||||
implementation(libs.vanniktech.blurhash)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
|
|
|
|||
|
|
@ -192,12 +192,19 @@ class AndroidMediaPreProcessor @Inject constructor(
|
|||
val resultFile = runCatchingExceptions {
|
||||
videoCompressor.compress(uri, shouldBeCompressed)
|
||||
.onEach {
|
||||
// TODO handle progress
|
||||
if (it is VideoTranscodingEvent.Progress) {
|
||||
Timber.d("Video compression progress: ${it.value}%")
|
||||
} else if (it is VideoTranscodingEvent.Completed) {
|
||||
Timber.d("Video compression completed: ${it.file.path}")
|
||||
}
|
||||
}
|
||||
.filterIsInstance<VideoTranscodingEvent.Completed>()
|
||||
.first()
|
||||
.file
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to compress video: $uri")
|
||||
}
|
||||
.getOrNull()
|
||||
|
||||
if (resultFile != null) {
|
||||
|
|
@ -283,10 +290,17 @@ class AndroidMediaPreProcessor @Inject constructor(
|
|||
private fun extractVideoMetadata(file: File, mimeType: String?, thumbnailResult: ThumbnailResult?): VideoInfo =
|
||||
MediaMetadataRetriever().runAndRelease {
|
||||
setDataSource(context, Uri.fromFile(file))
|
||||
|
||||
val rotation = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0
|
||||
val rawWidth = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toLong() ?: 0L
|
||||
val rawHeight = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toLong() ?: 0L
|
||||
|
||||
val (width, height) = if (rotation == 90 || rotation == 270) rawHeight to rawWidth else rawWidth to rawHeight
|
||||
|
||||
VideoInfo(
|
||||
duration = extractDuration(),
|
||||
width = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toLong() ?: 0L,
|
||||
height = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toLong() ?: 0L,
|
||||
width = width,
|
||||
height = height,
|
||||
mimetype = mimeType,
|
||||
size = file.length(),
|
||||
thumbnailInfo = thumbnailResult?.info,
|
||||
|
|
|
|||
|
|
@ -8,76 +8,146 @@
|
|||
package io.element.android.libraries.mediaupload.impl
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaCodecInfo
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.otaliastudios.transcoder.Transcoder
|
||||
import com.otaliastudios.transcoder.TranscoderListener
|
||||
import com.otaliastudios.transcoder.internal.media.MediaFormatConstants
|
||||
import com.otaliastudios.transcoder.resize.AtMostResizer
|
||||
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||
import com.otaliastudios.transcoder.strategy.PassThroughTrackStrategy
|
||||
import com.otaliastudios.transcoder.strategy.TrackStrategy
|
||||
import com.otaliastudios.transcoder.validator.WriteAlwaysValidator
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.Size
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.effect.Presentation
|
||||
import androidx.media3.transformer.Composition
|
||||
import androidx.media3.transformer.DefaultEncoderFactory
|
||||
import androidx.media3.transformer.EditedMediaItem
|
||||
import androidx.media3.transformer.Effects
|
||||
import androidx.media3.transformer.ExportException
|
||||
import androidx.media3.transformer.ExportResult
|
||||
import androidx.media3.transformer.ProgressHolder
|
||||
import androidx.media3.transformer.TransformationRequest
|
||||
import androidx.media3.transformer.Transformer
|
||||
import androidx.media3.transformer.VideoEncoderSettings
|
||||
import io.element.android.libraries.androidutils.file.createTmpFile
|
||||
import io.element.android.libraries.androidutils.file.getMimeType
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val MP4_EXTENSION = "mp4"
|
||||
|
||||
class VideoCompressor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
fun compress(uri: Uri, shouldBeCompressed: Boolean) = callbackFlow {
|
||||
@OptIn(UnstableApi::class)
|
||||
fun compress(uri: Uri, shouldBeCompressed: Boolean): Flow<VideoTranscodingEvent> = callbackFlow {
|
||||
val metadata = getVideoMetadata(uri)
|
||||
|
||||
val expectedExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(context.getMimeType(uri))
|
||||
|
||||
val videoStrategy = VideoStrategyFactory.create(
|
||||
expectedExtension = expectedExtension,
|
||||
val videoCompressorConfig = VideoCompressorConfigFactory.create(
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
val tmpFile = context.createTmpFile(extension = MP4_EXTENSION)
|
||||
val future = Transcoder.into(tmpFile.path)
|
||||
.setVideoTrackStrategy(videoStrategy)
|
||||
.addDataSource(context, uri)
|
||||
// Force the output to be written, even if no transcoding was actually needed
|
||||
.setValidator(WriteAlwaysValidator())
|
||||
.setListener(object : TranscoderListener {
|
||||
override fun onTranscodeProgress(progress: Double) {
|
||||
trySend(VideoTranscodingEvent.Progress(progress.toFloat()))
|
||||
}
|
||||
val tmpFile = context.createTmpFile(extension = "mp4")
|
||||
|
||||
override fun onTranscodeCompleted(successCode: Int) {
|
||||
val width = metadata?.width ?: Int.MAX_VALUE
|
||||
val height = metadata?.height ?: Int.MAX_VALUE
|
||||
|
||||
val videoResizeEffect = videoCompressorConfig.resizer?.let {
|
||||
val outputSize = it.getOutputSize(Size(width, height))
|
||||
if (metadata?.rotation == 90 || metadata?.rotation == 270) {
|
||||
// If the video is rotated, we need to swap width and height
|
||||
Presentation.createForWidthAndHeight(
|
||||
outputSize.height,
|
||||
outputSize.width,
|
||||
Presentation.LAYOUT_SCALE_TO_FIT,
|
||||
)
|
||||
} else {
|
||||
// Otherwise, we can use the original width and height
|
||||
Presentation.createForWidthAndHeight(
|
||||
outputSize.width,
|
||||
outputSize.height,
|
||||
Presentation.LAYOUT_SCALE_TO_FIT,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// If we are resizing, we also want to reduce set frame rate to the default value (30fps)
|
||||
val newFrameRate = videoCompressorConfig.newFrameRate
|
||||
|
||||
// If we need to resize the video, we also want to recalculate the bitrate
|
||||
val newBitrate = videoCompressorConfig.newBitRate
|
||||
|
||||
val inputMediaItem = MediaItem.fromUri(uri)
|
||||
val outputMediaItem = EditedMediaItem.Builder(inputMediaItem)
|
||||
.setFrameRate(newFrameRate)
|
||||
.run {
|
||||
if (videoResizeEffect != null) {
|
||||
setEffects(Effects(emptyList(), listOf(videoResizeEffect)))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
val encoderFactory = DefaultEncoderFactory.Builder(context)
|
||||
.setRequestedVideoEncoderSettings(
|
||||
VideoEncoderSettings.Builder()
|
||||
.setBitrateMode(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)
|
||||
.setBitrate(newBitrate)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
val videoTransformer = Transformer.Builder(context)
|
||||
.setVideoMimeType(MimeTypes.VIDEO_H264)
|
||||
.setAudioMimeType(MimeTypes.AUDIO_AAC)
|
||||
.setPortraitEncodingEnabled(false)
|
||||
.setEncoderFactory(encoderFactory)
|
||||
.addListener(object : Transformer.Listener {
|
||||
override fun onCompleted(composition: Composition, exportResult: ExportResult) {
|
||||
trySend(VideoTranscodingEvent.Completed(tmpFile))
|
||||
close()
|
||||
}
|
||||
|
||||
override fun onTranscodeCanceled() {
|
||||
override fun onError(composition: Composition, exportResult: ExportResult, exportException: ExportException) {
|
||||
Timber.e(exportException, "Video transcoding failed")
|
||||
tmpFile.safeDelete()
|
||||
close()
|
||||
close(exportException)
|
||||
}
|
||||
|
||||
override fun onTranscodeFailed(exception: Throwable) {
|
||||
tmpFile.safeDelete()
|
||||
close(exception)
|
||||
}
|
||||
override fun onFallbackApplied(
|
||||
composition: Composition,
|
||||
originalTransformationRequest: TransformationRequest,
|
||||
fallbackTransformationRequest: TransformationRequest
|
||||
) = Unit
|
||||
})
|
||||
.transcode()
|
||||
.build()
|
||||
|
||||
val progressJob = launch(Dispatchers.Main) {
|
||||
val progressHolder = ProgressHolder()
|
||||
while (isActive) {
|
||||
val state = videoTransformer.getProgress(progressHolder)
|
||||
if (state != Transformer.PROGRESS_STATE_NOT_STARTED) {
|
||||
channel.send(VideoTranscodingEvent.Progress(progressHolder.progress.toFloat()))
|
||||
}
|
||||
delay(500)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
videoTransformer.start(outputMediaItem, tmpFile.path)
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
if (!future.isDone) {
|
||||
future.cancel(true)
|
||||
}
|
||||
progressJob.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +159,7 @@ class VideoCompressor @Inject constructor(
|
|||
val width = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: -1
|
||||
val height = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: -1
|
||||
val bitrate = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toLongOrNull() ?: -1
|
||||
val framerate = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)?.toIntOrNull() ?: -1
|
||||
val frameRate = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)?.toIntOrNull() ?: -1
|
||||
val rotation = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0
|
||||
|
||||
val (actualWidth, actualHeight) = if (width == -1 || height == -1) {
|
||||
|
|
@ -104,7 +174,7 @@ class VideoCompressor @Inject constructor(
|
|||
width = actualWidth,
|
||||
height = actualHeight,
|
||||
bitrate = bitrate,
|
||||
frameRate = framerate,
|
||||
frameRate = frameRate,
|
||||
rotation = rotation,
|
||||
)
|
||||
}
|
||||
|
|
@ -126,45 +196,3 @@ sealed interface VideoTranscodingEvent {
|
|||
data class Progress(val value: Float) : VideoTranscodingEvent
|
||||
data class Completed(val file: File) : VideoTranscodingEvent
|
||||
}
|
||||
|
||||
internal object VideoStrategyFactory {
|
||||
// 720p
|
||||
private const val MAX_COMPRESSED_PIXEL_SIZE = 1280
|
||||
|
||||
// 1080p
|
||||
private const val MAX_PIXEL_SIZE = 1920
|
||||
|
||||
fun create(
|
||||
expectedExtension: String?,
|
||||
metadata: VideoFileMetadata?,
|
||||
shouldBeCompressed: Boolean,
|
||||
): TrackStrategy {
|
||||
val width = metadata?.width?.takeIf { it >= 0 } ?: Int.MAX_VALUE
|
||||
val height = metadata?.height?.takeIf { it >= 0 } ?: Int.MAX_VALUE
|
||||
val bitrate = metadata?.bitrate?.takeIf { it >= 0 }
|
||||
val frameRate = metadata?.frameRate?.takeIf { it >= 0 }
|
||||
val rotation = metadata?.rotation?.takeIf { it >= 0 }
|
||||
|
||||
// We only create a resizer if needed
|
||||
val resizer = when {
|
||||
shouldBeCompressed && (width > MAX_COMPRESSED_PIXEL_SIZE || height > MAX_COMPRESSED_PIXEL_SIZE) -> AtMostResizer(MAX_COMPRESSED_PIXEL_SIZE)
|
||||
width > MAX_PIXEL_SIZE || height > MAX_PIXEL_SIZE -> AtMostResizer(MAX_PIXEL_SIZE)
|
||||
else -> null
|
||||
}
|
||||
|
||||
return if (resizer == null && rotation == 0 && expectedExtension == MP4_EXTENSION) {
|
||||
// If there's no transcoding or resizing needed for the video file, just create a new file with the same contents but no metadata
|
||||
// Rotation is not kept by the PassThroughTrackStrategy, so we need to ensure the video is not rotated
|
||||
PassThroughTrackStrategy()
|
||||
} else {
|
||||
DefaultVideoStrategy.Builder()
|
||||
.apply {
|
||||
resizer?.let { addResizer(it) }
|
||||
bitrate?.let { bitRate(it) }
|
||||
frameRate?.let { frameRate(it) }
|
||||
}
|
||||
.mimeType(MediaFormatConstants.MIMETYPE_VIDEO_AVC)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaupload.impl
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.Size
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.transformer.VideoEncoderSettings
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
internal object VideoCompressorConfigFactory {
|
||||
// Major dimension of 720p
|
||||
private const val MAX_COMPRESSED_PIXEL_SIZE = 1280
|
||||
|
||||
// Major dimension of 1080p
|
||||
private const val MAX_PIXEL_SIZE = 1920
|
||||
|
||||
private const val DEFAULT_FRAME_RATE = 30
|
||||
|
||||
fun create(
|
||||
metadata: VideoFileMetadata?,
|
||||
shouldBeCompressed: Boolean,
|
||||
): VideoCompressorConfig {
|
||||
val width = metadata?.width?.takeIf { it >= 0 } ?: Int.MAX_VALUE
|
||||
val height = metadata?.height?.takeIf { it >= 0 } ?: Int.MAX_VALUE
|
||||
val originalBitrate = metadata?.bitrate?.takeIf { it >= 0 }
|
||||
val originalFrameRate = metadata?.frameRate?.takeIf { it >= 0 } ?: DEFAULT_FRAME_RATE
|
||||
|
||||
// We only create a resizer if needed
|
||||
val resizer = when {
|
||||
shouldBeCompressed && (width > MAX_COMPRESSED_PIXEL_SIZE || height > MAX_COMPRESSED_PIXEL_SIZE) -> VideoResizer(MAX_COMPRESSED_PIXEL_SIZE)
|
||||
width > MAX_PIXEL_SIZE || height > MAX_PIXEL_SIZE -> VideoResizer(MAX_PIXEL_SIZE)
|
||||
else -> null
|
||||
}
|
||||
|
||||
// If we are resizing, we also want to reduce the frame rate to the default value (30fps)
|
||||
val newFrameRate = if (resizer is VideoResizer) {
|
||||
min(originalFrameRate, DEFAULT_FRAME_RATE)
|
||||
} else {
|
||||
originalFrameRate
|
||||
}
|
||||
|
||||
// If we need to resize the video, we also want to recalculate the bitrate
|
||||
val newBitrate = if (resizer is VideoResizer) {
|
||||
val maxSize = resizer.getOutputSize(Size(width, height))
|
||||
val pixelsPerFrame = maxSize.width * maxSize.height
|
||||
val frameRate = newFrameRate
|
||||
// Apparently, 0.1 bits per pixel is a sweet spot for video compression
|
||||
val bitsPerPixel = 0.1f
|
||||
|
||||
(pixelsPerFrame * bitsPerPixel * frameRate).toLong()
|
||||
} else {
|
||||
originalBitrate
|
||||
}
|
||||
|
||||
return VideoCompressorConfig(
|
||||
resizer = resizer,
|
||||
newBitRate = newBitrate?.toInt() ?: VideoEncoderSettings.NO_VALUE,
|
||||
newFrameRate = newFrameRate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
internal data class VideoCompressorConfig(
|
||||
val resizer: VideoResizer?,
|
||||
val newBitRate: Int,
|
||||
val newFrameRate: Int,
|
||||
)
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
internal class VideoResizer(
|
||||
val maxSize: Int,
|
||||
) {
|
||||
fun getOutputSize(inputSize: Size): Size {
|
||||
val resultMajor = min(inputSize.major(), maxSize)
|
||||
val aspectRatio = inputSize.major().toFloat() / inputSize.minor().toFloat()
|
||||
return Size(resultMajor, (resultMajor / aspectRatio).roundToInt())
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
internal fun Size.major(): Int = if (width > height) width else height
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
internal fun Size.minor(): Int = if (width < height) width else height
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaupload.impl
|
||||
|
||||
import androidx.media3.transformer.VideoEncoderSettings
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class VideoCompressorConfigFactoryTest {
|
||||
@Test
|
||||
fun `if we don't have metadata the video will be resized`() {
|
||||
// Given
|
||||
val metadata = null
|
||||
val shouldBeCompressed = false
|
||||
|
||||
// When
|
||||
val videoCompressorConfig = VideoCompressorConfigFactory.create(
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertThat(videoCompressorConfig.resizer).isNotNull()
|
||||
assertThat(videoCompressorConfig.newFrameRate).isEqualTo(30)
|
||||
assertThat(videoCompressorConfig.newBitRate).isNotEqualTo(VideoEncoderSettings.NO_VALUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should be compressed and is larger than 720p it will be resized`() {
|
||||
// Given
|
||||
val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50, rotation = 0)
|
||||
val shouldBeCompressed = true
|
||||
|
||||
// When
|
||||
val videoCompressorConfig = VideoCompressorConfigFactory.create(
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsResized(videoCompressorConfig)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should be compressed and is smaller or equal to 720p it will not be resized`() {
|
||||
// Given
|
||||
val metadata = VideoFileMetadata(width = 1280, height = 720, bitrate = 1_000_000, frameRate = 50, rotation = 0)
|
||||
val shouldBeCompressed = true
|
||||
|
||||
// When
|
||||
val videoCompressorConfig = VideoCompressorConfigFactory.create(
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsNotResized(videoCompressorConfig)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should not be compressed and is larger than 1080p it will be resized`() {
|
||||
// Given
|
||||
val metadata = VideoFileMetadata(width = 2560, height = 1440, bitrate = 1_000_000, frameRate = 50, rotation = 0)
|
||||
val shouldBeCompressed = false
|
||||
|
||||
// When
|
||||
val videoCompressorConfig = VideoCompressorConfigFactory.create(
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsResized(videoCompressorConfig)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should not be compressed and is smaller or equal than 1080p it will not be resized`() {
|
||||
// Given
|
||||
val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50, rotation = 0)
|
||||
val shouldBeCompressed = false
|
||||
|
||||
// When
|
||||
val videoCompressorConfig = VideoCompressorConfigFactory.create(
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsNotResized(videoCompressorConfig)
|
||||
}
|
||||
|
||||
private inline fun assertIsResized(videoCompressorConfig: VideoCompressorConfig) {
|
||||
assertThat(videoCompressorConfig.resizer).isNotNull()
|
||||
}
|
||||
|
||||
private inline fun assertIsNotResized(videoCompressorConfig: VideoCompressorConfig) {
|
||||
assertThat(videoCompressorConfig.resizer).isNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaupload.impl
|
||||
|
||||
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||
import com.otaliastudios.transcoder.strategy.PassThroughTrackStrategy
|
||||
import com.otaliastudios.transcoder.strategy.TrackStrategy
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class VideoStrategyFactoryTest {
|
||||
@Test
|
||||
fun `if we don't have metadata the video will be transcoded just in case`() {
|
||||
// Given
|
||||
val expectedExtension = "mp4"
|
||||
val metadata = null
|
||||
val shouldBeCompressed = true
|
||||
|
||||
// When
|
||||
val videoStrategy = VideoStrategyFactory.create(
|
||||
expectedExtension = expectedExtension,
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsTranscoded(videoStrategy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should be compressed and is larger than 720p it will be transcoded`() {
|
||||
// Given
|
||||
val expectedExtension = "mp4"
|
||||
val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50, rotation = 0)
|
||||
val shouldBeCompressed = true
|
||||
|
||||
// When
|
||||
val videoStrategy = VideoStrategyFactory.create(
|
||||
expectedExtension = expectedExtension,
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsTranscoded(videoStrategy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should be compressed, has the right format and is smaller or equal to 720p it will not be transcoded`() {
|
||||
// Given
|
||||
val expectedExtension = "mp4"
|
||||
val metadata = VideoFileMetadata(width = 1280, height = 720, bitrate = 1_000_000, frameRate = 50, rotation = 0)
|
||||
val shouldBeCompressed = true
|
||||
|
||||
// When
|
||||
val videoStrategy = VideoStrategyFactory.create(
|
||||
expectedExtension = expectedExtension,
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsNotTranscoded(videoStrategy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should not be compressed and is larger than 1080p it will be transcoded`() {
|
||||
// Given
|
||||
val expectedExtension = "mp4"
|
||||
val metadata = VideoFileMetadata(width = 2560, height = 1440, bitrate = 1_000_000, frameRate = 50, rotation = 0)
|
||||
val shouldBeCompressed = false
|
||||
|
||||
// When
|
||||
val videoStrategy = VideoStrategyFactory.create(
|
||||
expectedExtension = expectedExtension,
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsTranscoded(videoStrategy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should not be compressed, has the right format and is smaller or equal than 1080p it will not be transcoded`() {
|
||||
// Given
|
||||
val expectedExtension = "mp4"
|
||||
val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50, rotation = 0)
|
||||
val shouldBeCompressed = false
|
||||
|
||||
// When
|
||||
val videoStrategy = VideoStrategyFactory.create(
|
||||
expectedExtension = expectedExtension,
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsNotTranscoded(videoStrategy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should not be compressed but has a wrong format it will be transcoded`() {
|
||||
// Given
|
||||
val expectedExtension = "mkv"
|
||||
val metadata = VideoFileMetadata(width = 320, height = 240, bitrate = 1_000_000, frameRate = 50, rotation = 0)
|
||||
val shouldBeCompressed = false
|
||||
|
||||
// When
|
||||
val videoStrategy = VideoStrategyFactory.create(
|
||||
expectedExtension = expectedExtension,
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsTranscoded(videoStrategy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should be compressed and has a wrong format it will be transcoded`() {
|
||||
// Given
|
||||
val expectedExtension = "mkv"
|
||||
val metadata = VideoFileMetadata(width = 320, height = 240, bitrate = 1_000_000, frameRate = 50, rotation = 0)
|
||||
val shouldBeCompressed = true
|
||||
|
||||
// When
|
||||
val videoStrategy = VideoStrategyFactory.create(
|
||||
expectedExtension = expectedExtension,
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsTranscoded(videoStrategy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if the video should not be compressed but has a rotation not zero it will be transcoded`() {
|
||||
// Given
|
||||
val expectedExtension = "mp4"
|
||||
val metadata = VideoFileMetadata(width = 320, height = 240, bitrate = 1_000_000, frameRate = 50, rotation = 90)
|
||||
val shouldBeCompressed = false
|
||||
|
||||
// When
|
||||
val videoStrategy = VideoStrategyFactory.create(
|
||||
expectedExtension = expectedExtension,
|
||||
metadata = metadata,
|
||||
shouldBeCompressed = shouldBeCompressed
|
||||
)
|
||||
|
||||
// Then
|
||||
assertIsTranscoded(videoStrategy)
|
||||
}
|
||||
|
||||
private inline fun assertIsTranscoded(videoStrategy: TrackStrategy) {
|
||||
assert(videoStrategy is DefaultVideoStrategy)
|
||||
}
|
||||
|
||||
private inline fun assertIsNotTranscoded(videoStrategy: TrackStrategy) {
|
||||
assert(videoStrategy is PassThroughTrackStrategy)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue