mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-11-10 15:56:50 +01:00
Merge pull request #1489 from davotoula/smarter-video-compression
Smarter video compression
This commit is contained in:
@@ -25,23 +25,12 @@ import android.graphics.Bitmap
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
import com.abedelazizshe.lightcompressorlibrary.CompressionListener
|
|
||||||
import com.abedelazizshe.lightcompressorlibrary.VideoCompressor
|
|
||||||
import com.abedelazizshe.lightcompressorlibrary.VideoQuality
|
|
||||||
import com.abedelazizshe.lightcompressorlibrary.config.AppSpecificStorageConfiguration
|
|
||||||
import com.abedelazizshe.lightcompressorlibrary.config.Configuration
|
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils
|
import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import id.zelory.compressor.Compressor
|
import id.zelory.compressor.Compressor
|
||||||
import id.zelory.compressor.constraint.default
|
import id.zelory.compressor.constraint.default
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
class MediaCompressorResult(
|
class MediaCompressorResult(
|
||||||
val uri: Uri,
|
val uri: Uri,
|
||||||
@@ -67,7 +56,9 @@ class MediaCompressor {
|
|||||||
|
|
||||||
// branch into compression based on content type
|
// branch into compression based on content type
|
||||||
return when {
|
return when {
|
||||||
contentType?.startsWith("video", ignoreCase = true) == true -> compressVideo(uri, contentType, applicationContext, mediaQuality)
|
contentType?.startsWith("video", ignoreCase = true) == true -> {
|
||||||
|
VideoCompressionHelper.compressVideo(uri, contentType, applicationContext, mediaQuality)
|
||||||
|
}
|
||||||
contentType?.startsWith("image", ignoreCase = true) == true &&
|
contentType?.startsWith("image", ignoreCase = true) == true &&
|
||||||
!contentType.contains("gif") &&
|
!contentType.contains("gif") &&
|
||||||
!contentType.contains("svg") ->
|
!contentType.contains("svg") ->
|
||||||
@@ -76,91 +67,6 @@ class MediaCompressor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
|
||||||
private suspend fun compressVideo(
|
|
||||||
uri: Uri,
|
|
||||||
contentType: String?,
|
|
||||||
applicationContext: Context,
|
|
||||||
mediaQuality: CompressorQuality,
|
|
||||||
): MediaCompressorResult {
|
|
||||||
val videoQuality =
|
|
||||||
when (mediaQuality) {
|
|
||||||
CompressorQuality.VERY_LOW -> VideoQuality.VERY_LOW
|
|
||||||
// Override user selection LOW to use VERY_LOW for better video streaming experience
|
|
||||||
CompressorQuality.LOW -> VideoQuality.VERY_LOW
|
|
||||||
CompressorQuality.MEDIUM -> VideoQuality.MEDIUM
|
|
||||||
CompressorQuality.HIGH -> VideoQuality.HIGH
|
|
||||||
CompressorQuality.VERY_HIGH -> VideoQuality.VERY_HIGH
|
|
||||||
else -> VideoQuality.MEDIUM
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d("MediaCompressor", "Using video compression $videoQuality")
|
|
||||||
|
|
||||||
val result =
|
|
||||||
withTimeoutOrNull(30000) {
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
|
||||||
VideoCompressor.start(
|
|
||||||
// => This is required
|
|
||||||
context = applicationContext,
|
|
||||||
// => Source can be provided as content uris
|
|
||||||
uris = listOf(uri),
|
|
||||||
isStreamable = false,
|
|
||||||
// THIS STORAGE
|
|
||||||
// sharedStorageConfiguration = SharedStorageConfiguration(
|
|
||||||
// saveAt = SaveLocation.movies, // => default is movies
|
|
||||||
// videoName = "compressed_video" // => required name
|
|
||||||
// ),
|
|
||||||
// OR AND NOT BOTH
|
|
||||||
storageConfiguration = AppSpecificStorageConfiguration(),
|
|
||||||
configureWith =
|
|
||||||
Configuration(
|
|
||||||
quality = videoQuality,
|
|
||||||
// => required name
|
|
||||||
videoNames = listOf(Uuid.random().toString()),
|
|
||||||
),
|
|
||||||
listener =
|
|
||||||
object : CompressionListener {
|
|
||||||
override fun onProgress(
|
|
||||||
index: Int,
|
|
||||||
percent: Float,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
override fun onStart(index: Int) {}
|
|
||||||
|
|
||||||
override fun onSuccess(
|
|
||||||
index: Int,
|
|
||||||
size: Long,
|
|
||||||
path: String?,
|
|
||||||
) {
|
|
||||||
if (path != null) {
|
|
||||||
Log.d("MediaCompressor", "Video compression success. Compressed size [$size]")
|
|
||||||
continuation.resume(MediaCompressorResult(Uri.fromFile(File(path)), contentType, size))
|
|
||||||
} else {
|
|
||||||
Log.d("MediaCompressor", "Video compression successful, but returned null path")
|
|
||||||
continuation.resume(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(
|
|
||||||
index: Int,
|
|
||||||
failureMessage: String,
|
|
||||||
) {
|
|
||||||
Log.d("MediaCompressor", "Video compression failed: $failureMessage")
|
|
||||||
// keeps going with original video
|
|
||||||
continuation.resume(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancelled(index: Int) {
|
|
||||||
continuation.resume(null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result ?: MediaCompressorResult(uri, contentType, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun compressImage(
|
private suspend fun compressImage(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
contentType: String?,
|
contentType: String?,
|
||||||
@@ -203,15 +109,6 @@ class MediaCompressor {
|
|||||||
3 -> CompressorQuality.UNCOMPRESSED
|
3 -> CompressorQuality.UNCOMPRESSED
|
||||||
else -> CompressorQuality.MEDIUM
|
else -> CompressorQuality.MEDIUM
|
||||||
}
|
}
|
||||||
|
|
||||||
fun compressorQualityToInt(compressorQuality: CompressorQuality): Int =
|
|
||||||
when (compressorQuality) {
|
|
||||||
CompressorQuality.LOW -> 0
|
|
||||||
CompressorQuality.MEDIUM -> 1
|
|
||||||
CompressorQuality.HIGH -> 2
|
|
||||||
CompressorQuality.UNCOMPRESSED -> 3
|
|
||||||
else -> 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.service.uploads
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.text.format.Formatter.formatFileSize
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.abedelazizshe.lightcompressorlibrary.CompressionListener
|
||||||
|
import com.abedelazizshe.lightcompressorlibrary.VideoCompressor
|
||||||
|
import com.abedelazizshe.lightcompressorlibrary.config.AppSpecificStorageConfiguration
|
||||||
|
import com.abedelazizshe.lightcompressorlibrary.config.Configuration
|
||||||
|
import com.abedelazizshe.lightcompressorlibrary.config.VideoResizer
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
data class VideoInfo(
|
||||||
|
val resolution: VideoResolution,
|
||||||
|
val framerate: Float,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VideoResolution(
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
) {
|
||||||
|
val pixels: Int get() = width * height
|
||||||
|
|
||||||
|
fun getStandard(): VideoStandard =
|
||||||
|
when {
|
||||||
|
pixels >= 3840 * 2160 -> VideoStandard.UHD_4K
|
||||||
|
pixels >= 2560 * 1440 -> VideoStandard.QHD_1440P
|
||||||
|
pixels >= 1920 * 1080 -> VideoStandard.FHD_1080P
|
||||||
|
pixels >= 1280 * 720 -> VideoStandard.HD_720P
|
||||||
|
pixels >= 854 * 480 -> VideoStandard.SD_480P
|
||||||
|
pixels >= 640 * 360 -> VideoStandard.NHD_360P
|
||||||
|
pixels >= 426 * 240 -> VideoStandard.QVGA_240P
|
||||||
|
else -> VideoStandard.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class VideoStandard(
|
||||||
|
val label: String,
|
||||||
|
) {
|
||||||
|
UHD_4K("4K"),
|
||||||
|
QHD_1440P("1440p"),
|
||||||
|
FHD_1080P("1080p"),
|
||||||
|
HD_720P("720p"),
|
||||||
|
SD_480P("480p"),
|
||||||
|
NHD_360P("360p"),
|
||||||
|
QVGA_240P("240p"),
|
||||||
|
UNKNOWN("unknown"),
|
||||||
|
;
|
||||||
|
|
||||||
|
override fun toString(): String = label
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CompressionRule(
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val bitrateMbps: Float,
|
||||||
|
val description: String,
|
||||||
|
) {
|
||||||
|
fun getBitrateMbpsInt(framerate: Float): Int {
|
||||||
|
// Apply 1.5x multiplier for 60fps+ videos
|
||||||
|
val multiplier = if (framerate >= 60f) 1.5f else 1.0f
|
||||||
|
|
||||||
|
// Library doesn't support float so we have to convert it to int and use 1 as minimum
|
||||||
|
return (bitrateMbps * multiplier).roundToInt().coerceAtLeast(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object VideoCompressionHelper {
|
||||||
|
private const val LOG_TAG = "VideoCompressionHelper"
|
||||||
|
|
||||||
|
private val compressionRules =
|
||||||
|
mapOf(
|
||||||
|
CompressorQuality.LOW to
|
||||||
|
mapOf(
|
||||||
|
VideoStandard.UHD_4K to CompressionRule(1280, 720, 2f, "4K→720p, 2Mbps"),
|
||||||
|
VideoStandard.QHD_1440P to CompressionRule(1280, 720, 2f, "1440p→720p, 2Mbps"),
|
||||||
|
VideoStandard.FHD_1080P to CompressionRule(854, 480, 1f, "1080p→480p, 1Mbps"),
|
||||||
|
VideoStandard.HD_720P to CompressionRule(640, 360, 1f, "720p→360p, 1Mbps"),
|
||||||
|
VideoStandard.SD_480P to CompressionRule(426, 240, 1f, "480p→240p, 1Mbps"),
|
||||||
|
VideoStandard.NHD_360P to CompressionRule(426, 240, 0.3f, "360p→240p, 0.3Mbps"),
|
||||||
|
VideoStandard.QVGA_240P to CompressionRule(320, 180, 0.2f, "240p→180p, 0.2Mbps"),
|
||||||
|
VideoStandard.UNKNOWN to CompressionRule(854, 480, 1f, "Low quality fallback, 1Mbps"),
|
||||||
|
),
|
||||||
|
CompressorQuality.MEDIUM to
|
||||||
|
mapOf(
|
||||||
|
VideoStandard.UHD_4K to CompressionRule(1920, 1080, 6f, "4K→1080p, 6Mbps"),
|
||||||
|
VideoStandard.QHD_1440P to CompressionRule(1920, 1080, 6f, "1440p→1080p, 6Mbps"),
|
||||||
|
VideoStandard.FHD_1080P to CompressionRule(1280, 720, 3f, "1080p→720p, 3Mbps"),
|
||||||
|
VideoStandard.HD_720P to CompressionRule(854, 480, 2f, "720p→480p, 2Mbps"),
|
||||||
|
VideoStandard.SD_480P to CompressionRule(640, 360, 1f, "480p→360p, 1Mbps"),
|
||||||
|
VideoStandard.NHD_360P to CompressionRule(426, 240, 0.5f, "360p→240p, 0.5Mbps"),
|
||||||
|
VideoStandard.QVGA_240P to CompressionRule(320, 180, 0.3f, "240p→180p, 0.3Mbps"),
|
||||||
|
VideoStandard.UNKNOWN to CompressionRule(1280, 720, 2f, "Medium quality fallback, 2Mbps"),
|
||||||
|
),
|
||||||
|
CompressorQuality.HIGH to
|
||||||
|
mapOf(
|
||||||
|
VideoStandard.UHD_4K to CompressionRule(3840, 2160, 16f, "4K→4K, 16Mbps"),
|
||||||
|
VideoStandard.QHD_1440P to CompressionRule(1920, 1080, 8f, "1440p→1080p, 8Mbps"),
|
||||||
|
VideoStandard.FHD_1080P to CompressionRule(1920, 1080, 6f, "1080p→1080p, 6Mbps"),
|
||||||
|
VideoStandard.HD_720P to CompressionRule(1280, 720, 3f, "720p→720p, 3Mbps"),
|
||||||
|
VideoStandard.SD_480P to CompressionRule(854, 480, 2f, "480p→480p, 2Mbps"),
|
||||||
|
VideoStandard.NHD_360P to CompressionRule(640, 360, 1f, "360p→360p, 1Mbps"),
|
||||||
|
VideoStandard.QVGA_240P to CompressionRule(426, 240, 0.5f, "240p→240p, 0.5Mbps"),
|
||||||
|
VideoStandard.UNKNOWN to CompressionRule(1920, 1080, 3f, "High quality fallback, 3Mbps"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun compressVideo(
|
||||||
|
uri: Uri,
|
||||||
|
contentType: String?,
|
||||||
|
applicationContext: Context,
|
||||||
|
mediaQuality: CompressorQuality,
|
||||||
|
timeoutMs: Long = 60_000L, // configurable, default 60s
|
||||||
|
): MediaCompressorResult {
|
||||||
|
val videoInfo = getVideoInfo(uri, applicationContext)
|
||||||
|
|
||||||
|
val videoBitrateInMbps =
|
||||||
|
if (videoInfo != null) {
|
||||||
|
val bitrateMbpsInt =
|
||||||
|
compressionRules
|
||||||
|
.getValue(mediaQuality)
|
||||||
|
.getValue(videoInfo.resolution.getStandard())
|
||||||
|
.getBitrateMbpsInt(videoInfo.framerate)
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
LOG_TAG,
|
||||||
|
"Bitrate: ${bitrateMbpsInt}Mbps for ${videoInfo.resolution.getStandard()} " +
|
||||||
|
"quality=$mediaQuality framerate=${videoInfo.framerate}fps.",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Log.w(LOG_TAG, "Video bitrate fallback: 2Mbps (videoInfo unavailable)")
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
val resizer =
|
||||||
|
if (videoInfo != null) {
|
||||||
|
val rules =
|
||||||
|
compressionRules
|
||||||
|
.getValue(mediaQuality)
|
||||||
|
.getValue(videoInfo.resolution.getStandard())
|
||||||
|
Log.d(
|
||||||
|
LOG_TAG,
|
||||||
|
"Resizer: ${videoInfo.resolution.width}x${videoInfo.resolution.height} -> " +
|
||||||
|
"${rules.width}x${rules.height} (${rules.description})",
|
||||||
|
)
|
||||||
|
VideoResizer.limitSize(rules.width.toDouble(), rules.height.toDouble())
|
||||||
|
} else {
|
||||||
|
Log.d(LOG_TAG, "Resizer: null (original resolution preserved)")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get original file size safely
|
||||||
|
val originalSize = applicationContext.getFileSize(uri)
|
||||||
|
|
||||||
|
val result =
|
||||||
|
withTimeoutOrNull(timeoutMs) {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
VideoCompressor.start(
|
||||||
|
context = applicationContext,
|
||||||
|
uris = listOf(uri),
|
||||||
|
isStreamable = true,
|
||||||
|
storageConfiguration = AppSpecificStorageConfiguration(),
|
||||||
|
configureWith =
|
||||||
|
Configuration(
|
||||||
|
videoBitrateInMbps = videoBitrateInMbps,
|
||||||
|
resizer = resizer,
|
||||||
|
videoNames = listOf(UUID.randomUUID().toString()),
|
||||||
|
isMinBitrateCheckEnabled = false,
|
||||||
|
),
|
||||||
|
listener =
|
||||||
|
object : CompressionListener {
|
||||||
|
override fun onStart(index: Int) {}
|
||||||
|
|
||||||
|
override fun onProgress(
|
||||||
|
index: Int,
|
||||||
|
percent: Float,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
override fun onSuccess(
|
||||||
|
index: Int,
|
||||||
|
size: Long,
|
||||||
|
path: String?,
|
||||||
|
) {
|
||||||
|
if (path == null) {
|
||||||
|
applicationContext.notifyUser(
|
||||||
|
"Video compression succeeded, but path was null",
|
||||||
|
Log.WARN,
|
||||||
|
)
|
||||||
|
if (continuation.isActive) continuation.resume(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val reductionPercent =
|
||||||
|
if (originalSize > 0) {
|
||||||
|
((originalSize - size) * 100.0 / originalSize).toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check: compression not smaller than original
|
||||||
|
if (originalSize > 0 && size >= originalSize) {
|
||||||
|
applicationContext.notifyUser(
|
||||||
|
"Compressed file larger than original. Using original.",
|
||||||
|
Log.WARN,
|
||||||
|
)
|
||||||
|
if (continuation.isActive) {
|
||||||
|
continuation.resume(
|
||||||
|
MediaCompressorResult(uri, contentType, null),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show compression result
|
||||||
|
if (originalSize > 0 && size > 0) {
|
||||||
|
val sizeLabel = formatFileSize(applicationContext, size)
|
||||||
|
val percentLabel =
|
||||||
|
if (reductionPercent >= 0) "-$reductionPercent%" else "+${-reductionPercent}%"
|
||||||
|
applicationContext.notifyUser(
|
||||||
|
"Video compressed: $sizeLabel ($percentLabel)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
LOG_TAG,
|
||||||
|
"Compression success: Original [$originalSize] -> " +
|
||||||
|
"Compressed [$size] ($reductionPercent% reduction)",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Attempt to correct the path: if it contains "_temp" then remove it
|
||||||
|
val correctedPath =
|
||||||
|
if (path.contains("_temp")) {
|
||||||
|
path.replace("_temp", "")
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
if (continuation.isActive) {
|
||||||
|
continuation.resume(
|
||||||
|
MediaCompressorResult(Uri.fromFile(File(correctedPath)), contentType, size),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
index: Int,
|
||||||
|
failureMessage: String,
|
||||||
|
) {
|
||||||
|
applicationContext.notifyUser(
|
||||||
|
"Video compression failed: $failureMessage",
|
||||||
|
Log.ERROR,
|
||||||
|
)
|
||||||
|
if (continuation.isActive) continuation.resume(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancelled(index: Int) {
|
||||||
|
Log.w(LOG_TAG, "Video compression cancelled")
|
||||||
|
if (continuation.isActive) continuation.resume(null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result ?: MediaCompressorResult(uri, contentType, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.getFileSize(uri: Uri): Long =
|
||||||
|
try {
|
||||||
|
contentResolver.query(uri, arrayOf(android.provider.OpenableColumns.SIZE), null, null, null)?.use { cursor ->
|
||||||
|
val sizeIndex = cursor.getColumnIndex(android.provider.OpenableColumns.SIZE)
|
||||||
|
if (cursor.moveToFirst()) cursor.getLong(sizeIndex) else 0L
|
||||||
|
} ?: 0L
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "Failed to get file size: ${e.message}")
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.notifyUser(
|
||||||
|
message: String,
|
||||||
|
logLevel: Int = Log.DEBUG,
|
||||||
|
duration: Int = Toast.LENGTH_LONG,
|
||||||
|
) {
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
Toast.makeText(this, message, duration).show()
|
||||||
|
}
|
||||||
|
when (logLevel) {
|
||||||
|
Log.ERROR -> Log.e(LOG_TAG, message)
|
||||||
|
Log.WARN -> Log.w(LOG_TAG, message)
|
||||||
|
else -> Log.d(LOG_TAG, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVideoInfo(
|
||||||
|
uri: Uri,
|
||||||
|
context: Context,
|
||||||
|
): VideoInfo? {
|
||||||
|
var retriever: MediaMetadataRetriever? = null
|
||||||
|
return try {
|
||||||
|
retriever = MediaMetadataRetriever()
|
||||||
|
retriever.setDataSource(context, uri)
|
||||||
|
val width = retriever.prepareVideoWidth()
|
||||||
|
val height = retriever.prepareVideoHeight()
|
||||||
|
val rotation = retriever.prepareRotation() ?: 0
|
||||||
|
|
||||||
|
// Get framerate
|
||||||
|
val framerateString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)
|
||||||
|
val framerate = framerateString?.toFloatOrNull() ?: 30.0f
|
||||||
|
|
||||||
|
if (width != null && height != null && width > 0 && height > 0) {
|
||||||
|
// Account for rotation
|
||||||
|
val resolution =
|
||||||
|
if (rotation == 90 || rotation == 270) {
|
||||||
|
VideoResolution(height, width)
|
||||||
|
} else {
|
||||||
|
VideoResolution(width, height)
|
||||||
|
}
|
||||||
|
VideoInfo(resolution, framerate)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "Failed to get video resolution: ${e.message}")
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
retriever?.release()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "Failed to release MediaMetadataRetriever: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user