refactor:

Safe file size lookup using OpenableColumns.SIZE
if (continuation.isActive) before resuming.
Better logging levels (Log.e for errors, Log.w for warnings).
Configurable compression timeout
This commit is contained in:
davotoula
2025-09-22 21:26:07 +02:00
parent ed758ef13e
commit 9af1e10ec2

View File

@@ -125,89 +125,96 @@ class VideoCompressionHelper {
contentType: String?, contentType: String?,
applicationContext: Context, applicationContext: Context,
mediaQuality: CompressorQuality, mediaQuality: CompressorQuality,
timeoutMs: Long = 60_000L, // configurable, default 60s
): MediaCompressorResult { ): MediaCompressorResult {
val videoInfo = getVideoInfo(uri, applicationContext) val videoInfo = getVideoInfo(uri, applicationContext)
val videoBitrateInMbps = val videoBitrateInMbps =
if (videoInfo != null) { if (videoInfo != null) {
val baseBitrate = compressionRules.getValue(mediaQuality).getValue(videoInfo.resolution.getStandardName()).getBitrateMbpsInt() val baseBitrate =
// Apply 1.5x multiplier for 60fps or higher videos compressionRules
val adjustedBitrate = .getValue(mediaQuality)
.getValue(videoInfo.resolution.getStandardName())
.getBitrateMbpsInt()
// Apply 1.5x multiplier for 60fps+
val adjusted =
if (videoInfo.framerate >= 60f) { if (videoInfo.framerate >= 60f) {
(baseBitrate * 1.5f).roundToInt() (baseBitrate * 1.5f).roundToInt()
} else { } else {
baseBitrate baseBitrate
} }
Log.d("VideoCompressionHelper", "Video bitrate calculated: ${adjustedBitrate}Mbps for ${videoInfo.resolution.getStandardName()} quality=$mediaQuality framerate=${videoInfo.framerate}fps")
adjustedBitrate Log.d(
"VideoCompressionHelper",
"Bitrate: ${adjusted}Mbps for ${videoInfo.resolution.getStandardName()} " +
"quality=$mediaQuality framerate=${videoInfo.framerate}fps",
)
adjusted
} else { } else {
// Default/fallback logic when videoInfo is null Log.w("VideoCompressionHelper", "Video bitrate fallback: 2Mbps (videoInfo unavailable)")
Log.d("VideoCompressionHelper", "Video bitrate fallback: 2Mbps (videoInfo unavailable)")
2 2
} }
val resizer = val resizer =
if (videoInfo != null) { if (videoInfo != null) {
val rules = compressionRules.getValue(mediaQuality).getValue(videoInfo.resolution.getStandardName()) val rules =
Log.d("VideoCompressionHelper", "Video resizer: ${videoInfo.resolution.width}x${videoInfo.resolution.height} -> ${rules.width}x${rules.height} (${rules.description})") compressionRules
.getValue(mediaQuality)
.getValue(videoInfo.resolution.getStandardName())
Log.d(
"VideoCompressionHelper",
"Resizer: ${videoInfo.resolution.width}x${videoInfo.resolution.height} -> " +
"${rules.width}x${rules.height} (${rules.description})",
)
VideoResizer.limitSize(rules.width.toDouble(), rules.height.toDouble()) VideoResizer.limitSize(rules.width.toDouble(), rules.height.toDouble())
} else { } else {
// null VideoResizer should result in unchanged resolution Log.d("VideoCompressionHelper", "Resizer: null (original resolution preserved)")
Log.d("VideoCompressionHelper", "Video resizer: null (original resolution preserved)")
null null
} }
// Get original file size for compression reporting // Get original file size safely
val originalSize = val originalSize = applicationContext.getFileSize(uri)
try {
applicationContext.contentResolver.openInputStream(uri)?.use { inputStream ->
inputStream.available().toLong()
} ?: 0L
} catch (e: Exception) {
Log.w("VideoCompressionHelper", "Failed to get original file size: ${e.message}")
0L
}
val result = val result =
withTimeoutOrNull(30000) { withTimeoutOrNull(timeoutMs) {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
VideoCompressor.start( VideoCompressor.start(
// => This is required
context = applicationContext, context = applicationContext,
// => Source can be provided as content uris
uris = listOf(uri), uris = listOf(uri),
isStreamable = true, isStreamable = true,
// THIS STORAGE
// sharedStorageConfiguration = SharedStorageConfiguration(
// saveAt = SaveLocation.movies, // => default is movies
// videoName = "compressed_video" // => required name
// ),
// OR AND NOT BOTH
storageConfiguration = AppSpecificStorageConfiguration(), storageConfiguration = AppSpecificStorageConfiguration(),
configureWith = configureWith =
Configuration( Configuration(
videoBitrateInMbps = videoBitrateInMbps, videoBitrateInMbps = videoBitrateInMbps,
resizer = resizer, resizer = resizer,
// => required name
videoNames = listOf(UUID.randomUUID().toString()), videoNames = listOf(UUID.randomUUID().toString()),
isMinBitrateCheckEnabled = false, isMinBitrateCheckEnabled = false,
), ),
listener = listener =
object : CompressionListener { object : CompressionListener {
override fun onStart(index: Int) {}
override fun onProgress( override fun onProgress(
index: Int, index: Int,
percent: Float, percent: Float,
) { ) {}
}
override fun onStart(index: Int) {}
override fun onSuccess( override fun onSuccess(
index: Int, index: Int,
size: Long, size: Long,
path: String?, path: String?,
) { ) {
if (path != null) { if (path == null) {
applicationContext.notifyUser(
"Video compression succeeded, but path was null",
"VideoCompressionHelper",
Log.WARN,
)
if (continuation.isActive) continuation.resume(null)
return
}
val reductionPercent = val reductionPercent =
if (originalSize > 0) { if (originalSize > 0) {
((originalSize - size) * 100.0 / originalSize).toInt() ((originalSize - size) * 100.0 / originalSize).toInt()
@@ -215,25 +222,42 @@ class VideoCompressionHelper {
0 0
} }
// Sanity check: if compressed file is larger than original, return original // Sanity check: compression not smaller than original
if (originalSize > 0 && size >= originalSize) { if (originalSize > 0 && size >= originalSize) {
Log.d("VideoCompressionHelper", "Compressed file ($size bytes) is larger than original ($originalSize bytes). Using original file.") applicationContext.notifyUser(
applicationContext.showToast("Video compression didn't reduce size. Using original file.") "Compressed file larger than original. Using original.",
continuation.resume(MediaCompressorResult(uri, contentType, null)) "VideoCompressionHelper",
Log.WARN,
)
if (continuation.isActive) {
continuation.resume(
MediaCompressorResult(uri, contentType, null),
)
}
return return
} }
// Show compression result
if (originalSize > 0 && size > 0) { if (originalSize > 0 && size > 0) {
val sizeLabel = formatFileSize(applicationContext, size) val sizeLabel = formatFileSize(applicationContext, size)
val percentLabel = if (reductionPercent >= 0) "-$reductionPercent%" else "+${-reductionPercent}%" val percentLabel =
if (reductionPercent >= 0) "-$reductionPercent%" else "+${-reductionPercent}%"
applicationContext.showToast("Video compressed: $sizeLabel ($percentLabel)") applicationContext.notifyUser(
"Video compressed: $sizeLabel ($percentLabel)",
"VideoCompressionHelper",
)
} }
Log.d("VideoCompressionHelper", "Video compression success. Original size [$originalSize] -> Compressed size [$size] ($reductionPercent% reduction)")
continuation.resume(MediaCompressorResult(Uri.fromFile(File(path)), contentType, size)) Log.d(
} else { "VideoCompressionHelper",
Log.d("VideoCompressionHelper", "Video compression successful, but returned null path") "Compression success: Original [$originalSize] -> " +
continuation.resume(null) "Compressed [$size] ($reductionPercent% reduction)",
)
if (continuation.isActive) {
continuation.resume(
MediaCompressorResult(Uri.fromFile(File(path)), contentType, size),
)
} }
} }
@@ -241,13 +265,17 @@ class VideoCompressionHelper {
index: Int, index: Int,
failureMessage: String, failureMessage: String,
) { ) {
Log.d("VideoCompressionHelper", "Video compression failed: $failureMessage") applicationContext.notifyUser(
// keeps going with original video "Video compression failed: $failureMessage",
continuation.resume(null) "VideoCompressionHelper",
Log.ERROR,
)
if (continuation.isActive) continuation.resume(null)
} }
override fun onCancelled(index: Int) { override fun onCancelled(index: Int) {
continuation.resume(null) Log.w("VideoCompressionHelper", "Video compression cancelled")
if (continuation.isActive) continuation.resume(null)
} }
}, },
) )
@@ -257,13 +285,31 @@ class VideoCompressionHelper {
return result ?: MediaCompressorResult(uri, contentType, null) return result ?: MediaCompressorResult(uri, contentType, null)
} }
private fun Context.showToast( 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("VideoCompressionHelper", "Failed to get file size: ${e.message}")
0L
}
private fun Context.notifyUser(
message: String, message: String,
logTag: String,
logLevel: Int = Log.DEBUG,
duration: Int = Toast.LENGTH_LONG, duration: Int = Toast.LENGTH_LONG,
) { ) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
Toast.makeText(this, message, duration).show() Toast.makeText(this, message, duration).show()
} }
when (logLevel) {
Log.ERROR -> Log.e(logTag, message)
Log.WARN -> Log.w(logTag, message)
else -> Log.d(logTag, message)
}
} }
private fun getVideoInfo( private fun getVideoInfo(