Skip to content

Commit 67839c4

Browse files
committed
Do a best effort to compress the video file:
Allow uploading the original file if it can't be transcoded.
1 parent dbb595c commit 67839c4

File tree

2 files changed

+92
-74
lines changed

2 files changed

+92
-74
lines changed

libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,17 @@ class AndroidMediaPreProcessor @Inject constructor(
184184
}
185185

186186
private suspend fun processVideo(uri: Uri, mimeType: String?, shouldBeCompressed: Boolean): MediaUploadInfo {
187-
val resultFile = videoCompressor.compress(uri, shouldBeCompressed)
188-
.onEach {
189-
// TODO handle progress
190-
}
191-
.filterIsInstance<VideoTranscodingEvent.Completed>()
192-
.first()
193-
.file
187+
val resultFile = runCatching {
188+
videoCompressor.compress(uri, shouldBeCompressed)
189+
.onEach {
190+
// TODO handle progress
191+
}
192+
.filterIsInstance<VideoTranscodingEvent.Completed>()
193+
.first()
194+
.file
195+
}
196+
// If the video could not be compressed, just copy use the original one by copying to a temporary file
197+
.getOrNull() ?: copyToTmpFile(uri)
194198
val thumbnailInfo = thumbnailFactory.createVideoThumbnail(resultFile)
195199
val videoInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo)
196200
return MediaUploadInfo.Video(

libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt

Lines changed: 81 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,104 +8,118 @@
88
package io.element.android.libraries.mediaupload.impl
99

1010
import android.content.Context
11+
import android.media.MediaMetadataRetriever
1112
import android.net.Uri
13+
import android.webkit.MimeTypeMap
1214
import com.otaliastudios.transcoder.Transcoder
1315
import com.otaliastudios.transcoder.TranscoderListener
14-
import com.otaliastudios.transcoder.common.TrackType
1516
import com.otaliastudios.transcoder.internal.media.MediaFormatConstants
16-
import com.otaliastudios.transcoder.internal.utils.mutableTrackMapOf
1717
import com.otaliastudios.transcoder.resize.AtMostResizer
1818
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
19-
import com.otaliastudios.transcoder.time.TimeInterpolator
2019
import io.element.android.libraries.androidutils.file.createTmpFile
20+
import io.element.android.libraries.androidutils.file.getMimeType
2121
import io.element.android.libraries.androidutils.file.safeDelete
2222
import io.element.android.libraries.di.ApplicationContext
2323
import kotlinx.coroutines.channels.awaitClose
2424
import kotlinx.coroutines.flow.callbackFlow
25+
import timber.log.Timber
2526
import java.io.File
2627
import javax.inject.Inject
2728

2829
class VideoCompressor @Inject constructor(
2930
@ApplicationContext private val context: Context,
3031
) {
32+
companion object {
33+
private const val MP4_EXTENSION = "mp4"
34+
}
35+
3136
fun compress(uri: Uri, shouldBeCompressed: Boolean) = callbackFlow {
32-
val tmpFile = context.createTmpFile(extension = "mp4")
33-
val future = Transcoder.into(tmpFile.path)
34-
.setTimeInterpolator(MonotonicTimelineInterpolator())
35-
.setVideoTrackStrategy(
36-
DefaultVideoStrategy.Builder()
37-
.addResizer(
38-
AtMostResizer(
39-
if (shouldBeCompressed) {
40-
720
41-
} else {
42-
1080
43-
}
44-
)
45-
)
46-
.mimeType(MediaFormatConstants.MIMETYPE_VIDEO_AVC)
47-
.build()
48-
)
49-
.addDataSource(context, uri)
50-
.setListener(object : TranscoderListener {
51-
override fun onTranscodeProgress(progress: Double) {
52-
trySend(VideoTranscodingEvent.Progress(progress.toFloat()))
53-
}
37+
val (width, height) = getVideoDimensions(uri) ?: (Int.MAX_VALUE to Int.MAX_VALUE)
5438

55-
override fun onTranscodeCompleted(successCode: Int) {
56-
trySend(VideoTranscodingEvent.Completed(tmpFile))
57-
close()
58-
}
39+
val resizer = when {
40+
shouldBeCompressed && (width > 720 || height > 720) -> AtMostResizer(720)
41+
width > 1080 || height > 1080 -> AtMostResizer(1080)
42+
else -> null
43+
}
5944

60-
override fun onTranscodeCanceled() {
61-
tmpFile.safeDelete()
62-
close()
63-
}
45+
val expectedExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(context.getMimeType(uri))
46+
47+
val tmpFile = context.createTmpFile(extension = MP4_EXTENSION)
6448

65-
override fun onTranscodeFailed(exception: Throwable) {
66-
tmpFile.safeDelete()
67-
close(exception)
49+
// If there's no transcoding needed for the video file, just copy it to the tmp file and return it
50+
val future = if (expectedExtension == MP4_EXTENSION && resizer == null) {
51+
context.contentResolver.openInputStream(uri)?.use { input ->
52+
tmpFile.outputStream().use { output ->
53+
input.copyTo(output)
6854
}
69-
})
70-
.transcode()
55+
}
56+
trySend(VideoTranscodingEvent.Completed(tmpFile))
57+
close()
58+
null
59+
} else {
60+
Transcoder.into(tmpFile.path)
61+
.setVideoTrackStrategy(
62+
DefaultVideoStrategy.Builder()
63+
.apply {
64+
resizer?.let { addResizer(it) }
65+
}
66+
.mimeType(MediaFormatConstants.MIMETYPE_VIDEO_AVC)
67+
.build()
68+
)
69+
.addDataSource(context, uri)
70+
.setListener(object : TranscoderListener {
71+
override fun onTranscodeProgress(progress: Double) {
72+
trySend(VideoTranscodingEvent.Progress(progress.toFloat()))
73+
}
74+
75+
override fun onTranscodeCompleted(successCode: Int) {
76+
trySend(VideoTranscodingEvent.Completed(tmpFile))
77+
close()
78+
}
79+
80+
override fun onTranscodeCanceled() {
81+
tmpFile.safeDelete()
82+
close()
83+
}
84+
85+
override fun onTranscodeFailed(exception: Throwable) {
86+
tmpFile.safeDelete()
87+
close(exception)
88+
}
89+
})
90+
.transcode()
91+
}
7192

7293
awaitClose {
73-
if (!future.isDone) {
94+
if (future?.isDone == false) {
7495
future.cancel(true)
7596
}
7697
}
7798
}
78-
}
7999

80-
sealed interface VideoTranscodingEvent {
81-
data class Progress(val value: Float) : VideoTranscodingEvent
82-
data class Completed(val file: File) : VideoTranscodingEvent
83-
}
100+
private fun getVideoDimensions(uri: Uri): Pair<Int, Int>? {
101+
return runCatching {
102+
MediaMetadataRetriever().use {
103+
it.setDataSource(context, uri)
84104

85-
/**
86-
* A TimeInterpolator that ensures timestamps are monotonically increasing.
87-
* Timestamps can go back and forth for many reasons, like miscalculations in
88-
* MediaCodec output or manually generated timestamps, or at the boundary
89-
* between one data source and another.
90-
*
91-
* Since `MediaMuxer.writeSampleData` can throw in case of invalid timestamps,
92-
* this interpolator ensures that the next timestamp is at least equal to
93-
* the previous timestamp plus 1. It does no effort to preserve the input deltas,
94-
* so the input stream must be as consistent as possible.
95-
*
96-
* For example, `20 30 40 50 10 20 30` would become `20 30 40 50 51 52 53`.
97-
*
98-
* Copied from the private [com.otaliastudios.transcoder.time.MonotonicTimelineInterpolator].
99-
*/
100-
private class MonotonicTimelineInterpolator : TimeInterpolator {
101-
private val last = mutableTrackMapOf(Long.MIN_VALUE, Long.MIN_VALUE)
105+
val width = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull() ?: -1
106+
val height = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: -1
102107

103-
override fun interpolate(type: TrackType, time: Long): Long {
104-
return interpolate(last[type], time).also { last[type] = it }
108+
if (width == -1 || height == -1) {
109+
// Try getting the first frame instead
110+
val bitmap = it.getFrameAtTime(0) ?: return null
111+
bitmap.width to bitmap.height
112+
} else {
113+
width to height
114+
}
115+
}
116+
}.onFailure {
117+
Timber.e(it, "Failed to get video dimensions")
118+
}.getOrNull()
105119
}
120+
}
106121

107-
private fun interpolate(prev: Long, next: Long): Long {
108-
if (prev == Long.MIN_VALUE) return next
109-
return next.coerceAtLeast(prev + 1)
110-
}
122+
sealed interface VideoTranscodingEvent {
123+
data class Progress(val value: Float) : VideoTranscodingEvent
124+
data class Completed(val file: File) : VideoTranscodingEvent
111125
}

0 commit comments

Comments
 (0)