Merge pull request #3779 from element-hq/feature/bma/mediaUpload
Optimize media upload
This commit is contained in:
commit
a9618e8193
25 changed files with 370 additions and 187 deletions
|
|
@ -37,7 +37,7 @@ class AdvancedSettingsPresenter @Inject constructor(
|
|||
.collectAsState(initial = true)
|
||||
val doesCompressMedia by sessionPreferencesStore
|
||||
.doesCompressMedia()
|
||||
.collectAsState(initial = false)
|
||||
.collectAsState(initial = true)
|
||||
val theme by remember {
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
<string name="screen_advanced_settings_element_call_base_url">"Custom Element Call base URL"</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_description">"Set a custom base URL for Element Call."</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Invalid URL, please make sure you include the protocol (http/https) and the correct address."</string>
|
||||
<string name="screen_advanced_settings_media_compression_description">"Optimize for upload"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Media"</string>
|
||||
<string name="screen_advanced_settings_media_compression_description">"Upload photos and videos faster and reduce data usage"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Optimise media quality"</string>
|
||||
<string name="screen_advanced_settings_push_provider_android">"Push notification provider"</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Disable the rich text editor to type Markdown manually."</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Read receipts"</string>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class AdvancedSettingsPresenterTest {
|
|||
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
||||
assertThat(initialState.showChangeThemeDialog).isFalse()
|
||||
assertThat(initialState.isSharePresenceEnabled).isTrue()
|
||||
assertThat(initialState.doesCompressMedia).isFalse()
|
||||
assertThat(initialState.doesCompressMedia).isTrue()
|
||||
assertThat(initialState.theme).isEqualTo(Theme.System)
|
||||
}
|
||||
}
|
||||
|
|
@ -76,11 +76,11 @@ class AdvancedSettingsPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
assertThat(initialState.doesCompressMedia).isFalse()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(true))
|
||||
assertThat(awaitItem().doesCompressMedia).isTrue()
|
||||
assertThat(initialState.doesCompressMedia).isTrue()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(false))
|
||||
assertThat(awaitItem().doesCompressMedia).isFalse()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(true))
|
||||
assertThat(awaitItem().doesCompressMedia).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import io.element.android.features.roomdetails.aMatrixRoom
|
|||
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditEvents
|
||||
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditPresenter
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
|
|
@ -513,7 +514,7 @@ class RoomDetailsEditPresenterTest {
|
|||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||
skipItems(4)
|
||||
updateAvatarResult.assertions().isCalledOnce().with(value("image/jpeg"), value(fakeFileContents))
|
||||
updateAvatarResult.assertions().isCalledOnce().with(value(MimeTypes.Jpeg), value(fakeFileContents))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import app.cash.molecule.RecompositionMode
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
|
|
@ -580,7 +581,7 @@ fun anImageInfo(): ImageInfo {
|
|||
return ImageInfo(
|
||||
height = 100,
|
||||
width = 100,
|
||||
mimetype = "image/jpeg",
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 1000,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = aMediaSource(),
|
||||
|
|
|
|||
|
|
@ -125,9 +125,13 @@ class AndroidMediaPreProcessor @Inject constructor(
|
|||
val compressionResult = imageCompressor.compressToTmpFile(
|
||||
inputStreamProvider = { contentResolver.openInputStream(uri)!! },
|
||||
resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE),
|
||||
mimeType = mimeType,
|
||||
orientation = orientation,
|
||||
).getOrThrow()
|
||||
val thumbnailResult = thumbnailFactory.createImageThumbnail(compressionResult.file)
|
||||
val thumbnailResult = thumbnailFactory.createImageThumbnail(
|
||||
file = compressionResult.file,
|
||||
mimeType = mimeType,
|
||||
)
|
||||
val imageInfo = compressionResult.toImageInfo(
|
||||
mimeType = mimeType,
|
||||
thumbnailResult = thumbnailResult
|
||||
|
|
@ -142,7 +146,10 @@ class AndroidMediaPreProcessor @Inject constructor(
|
|||
|
||||
suspend fun processImageWithoutCompression(): MediaUploadInfo {
|
||||
val file = copyToTmpFile(uri)
|
||||
val thumbnailResult = thumbnailFactory.createImageThumbnail(file)
|
||||
val thumbnailResult = thumbnailFactory.createImageThumbnail(
|
||||
file = file,
|
||||
mimeType = mimeType,
|
||||
)
|
||||
val imageInfo = contentResolver.openInputStream(uri).use { input ->
|
||||
val bitmap = BitmapFactory.decodeStream(input, null, null)!!
|
||||
ImageInfo(
|
||||
|
|
@ -171,17 +178,13 @@ class AndroidMediaPreProcessor @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun processVideo(uri: Uri, mimeType: String?, shouldBeCompressed: Boolean): MediaUploadInfo {
|
||||
val resultFile = if (shouldBeCompressed) {
|
||||
videoCompressor.compress(uri)
|
||||
.onEach {
|
||||
// TODO handle progress
|
||||
}
|
||||
.filterIsInstance<VideoTranscodingEvent.Completed>()
|
||||
.first()
|
||||
.file
|
||||
} else {
|
||||
copyToTmpFile(uri)
|
||||
}
|
||||
val resultFile = videoCompressor.compress(uri, shouldBeCompressed)
|
||||
.onEach {
|
||||
// TODO handle progress
|
||||
}
|
||||
.filterIsInstance<VideoTranscodingEvent.Completed>()
|
||||
.first()
|
||||
.file
|
||||
val thumbnailInfo = thumbnailFactory.createVideoThumbnail(resultFile)
|
||||
val videoInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo)
|
||||
return MediaUploadInfo.Video(
|
||||
|
|
|
|||
|
|
@ -34,14 +34,16 @@ class ImageCompressor @Inject constructor(
|
|||
suspend fun compressToTmpFile(
|
||||
inputStreamProvider: () -> InputStream,
|
||||
resizeMode: ResizeMode,
|
||||
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
||||
mimeType: String,
|
||||
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||
desiredQuality: Int = 78,
|
||||
): Result<ImageCompressionResult> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
val format = mimeTypeToCompressFormat(mimeType)
|
||||
val extension = mimeTypeToCompressFileExtension(mimeType)
|
||||
val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow()
|
||||
// Encode bitmap to the destination temporary file
|
||||
val tmpFile = context.createTmpFile(extension = "jpeg")
|
||||
val tmpFile = context.createTmpFile(extension = extension)
|
||||
tmpFile.outputStream().use {
|
||||
compressedBitmap.compress(format, desiredQuality, it)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaupload.impl
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
|
||||
fun mimeTypeToCompressFormat(mimeType: String) = when (mimeType) {
|
||||
MimeTypes.Png -> Bitmap.CompressFormat.PNG
|
||||
else -> Bitmap.CompressFormat.JPEG
|
||||
}
|
||||
|
||||
fun mimeTypeToCompressFileExtension(mimeType: String) = when (mimeType) {
|
||||
MimeTypes.Png -> "png"
|
||||
else -> "jpeg"
|
||||
}
|
||||
|
||||
fun mimeTypeToThumbnailMimeType(mimeType: String) = when (mimeType) {
|
||||
MimeTypes.Png -> MimeTypes.Png
|
||||
else -> MimeTypes.Jpeg
|
||||
}
|
||||
|
|
@ -53,8 +53,11 @@ class ThumbnailFactory @Inject constructor(
|
|||
private val sdkIntProvider: BuildVersionSdkIntProvider
|
||||
) {
|
||||
@SuppressLint("NewApi")
|
||||
suspend fun createImageThumbnail(file: File): ThumbnailResult? {
|
||||
return createThumbnail { cancellationSignal ->
|
||||
suspend fun createImageThumbnail(
|
||||
file: File,
|
||||
mimeType: String,
|
||||
): ThumbnailResult? {
|
||||
return createThumbnail(mimeType = mimeType) { cancellationSignal ->
|
||||
try {
|
||||
// This API works correctly with GIF
|
||||
if (sdkIntProvider.isAtLeast(Build.VERSION_CODES.Q)) {
|
||||
|
|
@ -83,7 +86,7 @@ class ThumbnailFactory @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun createVideoThumbnail(file: File): ThumbnailResult? {
|
||||
return createThumbnail {
|
||||
return createThumbnail(mimeType = MimeTypes.Jpeg) {
|
||||
MediaMetadataRetriever().runAndRelease {
|
||||
setDataSource(context, file.toUri())
|
||||
getFrameAtTime(VIDEO_THUMB_FRAME)
|
||||
|
|
@ -91,7 +94,10 @@ class ThumbnailFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun createThumbnail(bitmapFactory: (CancellationSignal) -> Bitmap?): ThumbnailResult? = suspendCancellableCoroutine { continuation ->
|
||||
private suspend fun createThumbnail(
|
||||
mimeType: String,
|
||||
bitmapFactory: (CancellationSignal) -> Bitmap?,
|
||||
): ThumbnailResult? = suspendCancellableCoroutine { continuation ->
|
||||
val cancellationSignal = CancellationSignal()
|
||||
continuation.invokeOnCancellation {
|
||||
cancellationSignal.cancel()
|
||||
|
|
@ -101,9 +107,11 @@ class ThumbnailFactory @Inject constructor(
|
|||
continuation.resume(null)
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
val thumbnailFile = context.createTmpFile(extension = "jpeg")
|
||||
val format = mimeTypeToCompressFormat(mimeType)
|
||||
val extension = mimeTypeToCompressFileExtension(mimeType)
|
||||
val thumbnailFile = context.createTmpFile(extension = extension)
|
||||
thumbnailFile.outputStream().use { outputStream ->
|
||||
bitmapThumbnail.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
|
||||
bitmapThumbnail.compress(format, 78, outputStream)
|
||||
}
|
||||
val blurhash = BlurHash.encode(bitmapThumbnail, 3, 3)
|
||||
val thumbnailResult = ThumbnailResult(
|
||||
|
|
@ -111,7 +119,7 @@ class ThumbnailFactory @Inject constructor(
|
|||
info = ThumbnailInfo(
|
||||
height = bitmapThumbnail.height.toLong(),
|
||||
width = bitmapThumbnail.width.toLong(),
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
mimetype = mimeTypeToThumbnailMimeType(mimeType),
|
||||
size = thumbnailFile.length()
|
||||
),
|
||||
blurhash = blurhash
|
||||
|
|
|
|||
|
|
@ -24,12 +24,20 @@ import javax.inject.Inject
|
|||
class VideoCompressor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
fun compress(uri: Uri) = callbackFlow {
|
||||
fun compress(uri: Uri, shouldBeCompressed: Boolean) = callbackFlow {
|
||||
val tmpFile = context.createTmpFile(extension = "mp4")
|
||||
val future = Transcoder.into(tmpFile.path)
|
||||
.setVideoTrackStrategy(
|
||||
DefaultVideoStrategy.Builder()
|
||||
.addResizer(AtMostResizer(720, 480))
|
||||
.addResizer(
|
||||
AtMostResizer(
|
||||
if (shouldBeCompressed) {
|
||||
720
|
||||
} else {
|
||||
1080
|
||||
}
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.addDataSource(context, uri)
|
||||
|
|
|
|||
3
libraries/mediaupload/impl/src/test/assets/image.jpeg
Normal file
3
libraries/mediaupload/impl/src/test/assets/image.jpeg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:77276f9b174f8823eaf787ab0a659199ef5d30c0361ec8b9b4f0890adb1907a1
|
||||
size 9986336
|
||||
|
|
@ -35,113 +35,194 @@ import kotlin.time.Duration
|
|||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class AndroidMediaPreProcessorTest {
|
||||
@Test
|
||||
fun `test processing image`() = runTest {
|
||||
private suspend fun TestScope.process(
|
||||
asset: Asset,
|
||||
compressIfPossible: Boolean,
|
||||
sdkIntVersion: Int = Build.VERSION_CODES.P,
|
||||
deleteOriginal: Boolean = false,
|
||||
): MediaUploadInfo {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "image.png")
|
||||
val sut = createAndroidMediaPreProcessor(context, sdkIntVersion)
|
||||
val file = getFileFromAssets(context, asset.filename)
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Png,
|
||||
deleteOriginal = false,
|
||||
compressIfPossible = true,
|
||||
mimeType = asset.mimeType,
|
||||
deleteOriginal = deleteOriginal,
|
||||
compressIfPossible = compressIfPossible,
|
||||
)
|
||||
val data = result.getOrThrow()
|
||||
assertThat(data.file.path).endsWith("image.png")
|
||||
val info = data as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 109_908,
|
||||
ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4484),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K13]7q%zWC00R4of%\$baad"
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
assertThat(data.file.path).endsWith(asset.filename)
|
||||
return data
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing image api Q`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context, sdkIntVersion = Build.VERSION_CODES.Q)
|
||||
val file = getFileFromAssets(context, "image.png")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Png,
|
||||
deleteOriginal = false,
|
||||
fun `test processing png`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImagePng,
|
||||
compressIfPossible = true,
|
||||
)
|
||||
val data = result.getOrThrow()
|
||||
assertThat(data.file.path).endsWith("image.png")
|
||||
val info = data as MediaUploadInfo.Image
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = assetImagePng.height,
|
||||
width = assetImagePng.width,
|
||||
mimetype = assetImagePng.mimeType,
|
||||
size = 2_026_433,
|
||||
ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Png, size = 91),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing png api Q`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImagePng,
|
||||
compressIfPossible = true,
|
||||
sdkIntVersion = Build.VERSION_CODES.Q,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 109_908,
|
||||
height = assetImagePng.height,
|
||||
width = assetImagePng.width,
|
||||
mimetype = assetImagePng.mimeType,
|
||||
size = 2_026_433,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing image no compression`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "image.png")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Png,
|
||||
deleteOriginal = false,
|
||||
fun `test processing png no compression`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImagePng,
|
||||
compressIfPossible = false,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("image.png")
|
||||
val info = result as MediaUploadInfo.Image
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 1_856_786,
|
||||
thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Jpeg, size = 643),
|
||||
height = assetImagePng.height,
|
||||
width = assetImagePng.width,
|
||||
mimetype = assetImagePng.mimeType,
|
||||
size = assetImagePng.size,
|
||||
thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Png, size = 91),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ",
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing image and delete`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "image.png")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Png,
|
||||
deleteOriginal = true,
|
||||
fun `test processing png and delete`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImagePng,
|
||||
compressIfPossible = false,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("image.png")
|
||||
val info = result as MediaUploadInfo.Image
|
||||
deleteOriginal = true,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 1_856_786,
|
||||
thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Jpeg, size = 643),
|
||||
height = assetImagePng.height,
|
||||
width = assetImagePng.width,
|
||||
mimetype = assetImagePng.mimeType,
|
||||
size = assetImagePng.size,
|
||||
thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Png, size = 91),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ",
|
||||
)
|
||||
)
|
||||
// Does not work
|
||||
// assertThat(file.exists()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing jpeg`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImageJpeg,
|
||||
compressIfPossible = true,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 979,
|
||||
width = 3006,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 84_845,
|
||||
ThumbnailInfo(height = 244, width = 751, mimetype = MimeTypes.Jpeg, size = 7_178),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K07gBzX=j_D4xZjoaSe,s:"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing jpeg api Q`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImageJpeg,
|
||||
compressIfPossible = true,
|
||||
sdkIntVersion = Build.VERSION_CODES.Q,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 979,
|
||||
width = 3_006,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 84_845,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing jpeg no compression`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImageJpeg,
|
||||
compressIfPossible = false,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = assetImageJpeg.height,
|
||||
width = assetImageJpeg.width,
|
||||
mimetype = assetImageJpeg.mimeType,
|
||||
size = assetImageJpeg.size,
|
||||
thumbnailInfo = ThumbnailInfo(height = 6, width = 6, mimetype = MimeTypes.Jpeg, size = 631),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing jpeg and delete`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImageJpeg,
|
||||
compressIfPossible = false,
|
||||
deleteOriginal = true,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = assetImageJpeg.height,
|
||||
width = assetImageJpeg.width,
|
||||
mimetype = assetImageJpeg.mimeType,
|
||||
size = assetImageJpeg.size,
|
||||
thumbnailInfo = ThumbnailInfo(height = 6, width = 6, mimetype = MimeTypes.Jpeg, size = 631),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ",
|
||||
)
|
||||
|
|
@ -152,70 +233,50 @@ class AndroidMediaPreProcessorTest {
|
|||
|
||||
@Test
|
||||
fun `test processing gif`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "animated_gif.gif")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Gif,
|
||||
deleteOriginal = false,
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetAnimatedGif,
|
||||
compressIfPossible = true,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("animated_gif.gif")
|
||||
val info = result as MediaUploadInfo.Image
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 600,
|
||||
width = 800,
|
||||
mimetype = MimeTypes.Gif,
|
||||
size = 687_979,
|
||||
height = assetAnimatedGif.height,
|
||||
width = assetAnimatedGif.width,
|
||||
mimetype = assetAnimatedGif.mimeType,
|
||||
size = assetAnimatedGif.size,
|
||||
thumbnailInfo = ThumbnailInfo(height = 50, width = 50, mimetype = MimeTypes.Jpeg, size = 691),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ",
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing file`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "text.txt")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.PlainText,
|
||||
deleteOriginal = false,
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetText,
|
||||
compressIfPossible = true,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("text.txt")
|
||||
val info = result as MediaUploadInfo.AnyFile
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.AnyFile
|
||||
assertThat(info.fileInfo).isEqualTo(
|
||||
FileInfo(
|
||||
mimetype = MimeTypes.PlainText,
|
||||
size = 13,
|
||||
mimetype = assetText.mimeType,
|
||||
size = assetText.size,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Ignore("Compressing video is not working with Robolectric")
|
||||
@Test
|
||||
fun `test processing video`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "video.mp4")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Mp4,
|
||||
deleteOriginal = false,
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetVideo,
|
||||
compressIfPossible = true,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("video.mp4")
|
||||
val info = result as MediaUploadInfo.Video
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Video
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.videoInfo).isEqualTo(
|
||||
VideoInfo(
|
||||
|
|
@ -230,22 +291,16 @@ class AndroidMediaPreProcessorTest {
|
|||
blurhash = null,
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Ignore("Compressing video is not working with Robolectric")
|
||||
@Test
|
||||
fun `test processing video no compression`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "video.mp4")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Mp4,
|
||||
deleteOriginal = false,
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetVideo,
|
||||
compressIfPossible = false,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("video.mp4")
|
||||
val info = result as MediaUploadInfo.Video
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Video
|
||||
// Computing thumbnailFile is failing with Robolectric
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
assertThat(info.videoInfo).isEqualTo(
|
||||
|
|
@ -263,22 +318,15 @@ class AndroidMediaPreProcessorTest {
|
|||
blurhash = null,
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing audio`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "sample3s.mp3")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Mp3,
|
||||
deleteOriginal = false,
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetAudio,
|
||||
compressIfPossible = true,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("sample3s.mp3")
|
||||
val info = result as MediaUploadInfo.Audio
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Audio
|
||||
assertThat(info.audioInfo).isEqualTo(
|
||||
AudioInfo(
|
||||
// Not available with Robolectric?
|
||||
|
|
@ -287,7 +335,6 @@ class AndroidMediaPreProcessorTest {
|
|||
mimetype = MimeTypes.Mp3,
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaupload.impl
|
||||
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
|
||||
data class Asset(
|
||||
val filename: String,
|
||||
val mimeType: String,
|
||||
val size: Long,
|
||||
val width: Long?,
|
||||
val height: Long?,
|
||||
)
|
||||
|
||||
/**
|
||||
* "image.png" is a 1_818 x 1_178 PNG image with a size of 1_856_786 bytes.
|
||||
*/
|
||||
val assetImagePng = Asset(
|
||||
filename = "image.png",
|
||||
mimeType = MimeTypes.Png,
|
||||
size = 1_856_786,
|
||||
width = 1_818,
|
||||
height = 1_178,
|
||||
)
|
||||
|
||||
/**
|
||||
* "image.jpeg" is a 12_024 x 3_916, JPEG image with a size of 9_986_336 bytes.
|
||||
*/
|
||||
val assetImageJpeg = Asset(
|
||||
filename = "image.jpeg",
|
||||
mimeType = MimeTypes.Jpeg,
|
||||
size = 9_986_336,
|
||||
width = 12_024,
|
||||
height = 3_916,
|
||||
)
|
||||
|
||||
/**
|
||||
* "video.mp4" is a 1_280 x 720, MP4 video with a size of 1_673_712 bytes.
|
||||
*/
|
||||
val assetVideo = Asset(
|
||||
filename = "video.mp4",
|
||||
mimeType = MimeTypes.Mp4,
|
||||
size = 1_673_712,
|
||||
width = 1_280,
|
||||
height = 720,
|
||||
)
|
||||
|
||||
/**
|
||||
* "sample3s.mp3" is a 3 seconds MP3 audio file with a size of 52_079 bytes.
|
||||
*/
|
||||
val assetAudio = Asset(
|
||||
filename = "sample3s.mp3",
|
||||
mimeType = MimeTypes.Mp3,
|
||||
size = 52_079,
|
||||
width = null,
|
||||
height = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* "text.txt" is a 13 bytes text file.
|
||||
*/
|
||||
val assetText = Asset(
|
||||
filename = "text.txt",
|
||||
mimeType = MimeTypes.PlainText,
|
||||
size = 13,
|
||||
width = null,
|
||||
height = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* "animated_gif.gif" is a 800 x 600, GIF image with a size of 687_979 bytes.
|
||||
*/
|
||||
val assetAnimatedGif = Asset(
|
||||
filename = "animated_gif.gif",
|
||||
mimeType = MimeTypes.Gif,
|
||||
size = 687_979,
|
||||
width = 800,
|
||||
height = 600,
|
||||
)
|
||||
|
|
@ -83,7 +83,7 @@ class DefaultSessionPreferencesStore(
|
|||
override fun isSessionVerificationSkipped(): Flow<Boolean> = get(skipSessionVerification) { false }
|
||||
|
||||
override suspend fun setCompressMedia(compress: Boolean) = update(compressMedia, compress)
|
||||
override fun doesCompressMedia(): Flow<Boolean> = get(compressMedia) { false }
|
||||
override fun doesCompressMedia(): Flow<Boolean> = get(compressMedia) { true }
|
||||
|
||||
override suspend fun clear() {
|
||||
dataStoreFile.safeDelete()
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class InMemorySessionPreferencesStore(
|
|||
isSendTypingNotificationsEnabled: Boolean = true,
|
||||
isRenderTypingNotificationsEnabled: Boolean = true,
|
||||
isSessionVerificationSkipped: Boolean = false,
|
||||
doesCompressMedia: Boolean = false,
|
||||
doesCompressMedia: Boolean = true,
|
||||
) : SessionPreferencesStore {
|
||||
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
|
||||
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:13e7793d8dd6d08e182b128a9b3dac2877557e8bdd220561d36c2ce1650b94ff
|
||||
size 48107
|
||||
oid sha256:cd172b454fdaf7966e8c85481959000c28310ab2ffccd1b13dd9af3b1e392a3d
|
||||
size 54935
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1bac5247c3a4990eb9155a21f72a49059cbaa93288380ba1ab6be2def8b3a6a9
|
||||
size 47876
|
||||
oid sha256:cff335c3b36ad364bf237b5635577da8f5c82e86bdd35bfb1c138e222e616640
|
||||
size 54698
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:11c969c1d04150cef68da64865bade2ea3bfc1aa5f0d790262315131b57d6233
|
||||
size 31702
|
||||
oid sha256:397049b4a74bd122c09e1e16d4ee72dcf5abc6562bccfe839d394388b1b0a2b9
|
||||
size 31741
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc1aa9348e470e9d7e7e1e838e18a3403159c2effb49fa359b2b2db97dd81961
|
||||
size 47901
|
||||
oid sha256:7b5cee26851a31850647b550225071bd0a5c4d16026b42771f43e395fc1f0dcc
|
||||
size 54766
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90d1184879c5a34dc27cc942ec3110e14ccd9a90c152b10a1844fde0566d54fe
|
||||
size 47841
|
||||
oid sha256:21378e572926adcafa78ecbf16042f1abab81790ead55c2da6134d84d3e88145
|
||||
size 54742
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c2beb4f4f190f6aca2d4da338d980e8611283ffb9bfd2f402482f8ecc629cf22
|
||||
size 46759
|
||||
oid sha256:9dcd26f37cea379f17d4b7d35b66c8dabdef2b83afe168f261c7007149ac7044
|
||||
size 53658
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b65acfd3126efc5cd4fe1a929a786468cc18ee371cb4462ef0cf9f6e8963fcea
|
||||
size 46456
|
||||
oid sha256:d38e06cbbabfd6fdab433008ec2249f2ca3838a9a95a5f6300e79fce5b805733
|
||||
size 53366
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8e90bbde9aac7e710703e836f293a00b2a5f35447d4d63c9732de21a0f291449
|
||||
size 29336
|
||||
oid sha256:2836655a2f3ef9aca1f2ba94222bc92f9bedab82589169e34ea0a325cb1c0e52
|
||||
size 29363
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6da0cf1729162fa1745bcb4dd86be06f90d3bcf6bf034e4a64e1ee9119b6cdd5
|
||||
size 46501
|
||||
oid sha256:6823de7de81b167d27c1c9c8aadab6b027976673552590f1dbea9dad7748919f
|
||||
size 53413
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:66109868b1e893bc569d70110e3e587d7a17d777838cfc3e5c3d3189338d924e
|
||||
size 46423
|
||||
oid sha256:fc4b71bbb0d276540c887a4c6869eca003ff500ac1d8b319d63483be21cc04d0
|
||||
size 53377
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue