diff --git a/amethyst/build.gradle b/amethyst/build.gradle index e700680f7..2845df088 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -151,6 +151,12 @@ android { signingConfig = signingConfigs.debug } } + // TODO: remove this when lightcompressor uses one MP4 parser only + packaging { + resources { + resources.pickFirsts.add('builddef.lst') + } + } flavorDimensions = ["channel"] diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt index 0b506c4dc..ee7f51d97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt @@ -45,6 +45,7 @@ class MediaCompressor { contentType: String?, mediaQuality: CompressorQuality, applicationContext: Context, + useH265: Boolean = false, ): MediaCompressorResult { // Skip compression if user selected uncompressed if (mediaQuality == CompressorQuality.UNCOMPRESSED) { @@ -57,7 +58,7 @@ class MediaCompressor { // branch into compression based on content type return when { contentType?.startsWith("video", ignoreCase = true) == true -> { - VideoCompressionHelper.compressVideo(uri, contentType, applicationContext, mediaQuality) + VideoCompressionHelper.compressVideo(uri, contentType, applicationContext, mediaQuality, useH265) } contentType?.startsWith("image", ignoreCase = true) == true && !contentType.contains("gif") && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt index 45d8269e2..207810b1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt @@ -46,6 +46,8 @@ class MultiOrchestrator( fun first() = list.first() + fun hasVideo() = list.any { it.media.mimeType?.startsWith("video", ignoreCase = true) == true } + suspend fun upload( alt: String?, contentWarningReason: String?, @@ -53,6 +55,7 @@ class MultiOrchestrator( server: ServerName, account: Account, context: Context, + useH265: Boolean = false, ): Result { coroutineScope { val jobs = @@ -67,6 +70,7 @@ class MultiOrchestrator( server, account, context, + useH265, ) } } @@ -85,6 +89,7 @@ class MultiOrchestrator( server: ServerName, account: Account, context: Context, + useH265: Boolean = false, ): Result { coroutineScope { val jobs = @@ -100,6 +105,7 @@ class MultiOrchestrator( server, account, context, + useH265, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index dd011f74f..42a763dee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -288,9 +288,10 @@ class UploadOrchestrator { mimeType: String?, compressionQuality: CompressorQuality, context: Context, + useH265: Boolean = false, ) = if (compressionQuality != CompressorQuality.UNCOMPRESSED) { updateState(0.02, UploadingState.Compressing) - MediaCompressor().compress(uri, mimeType, compressionQuality, context.applicationContext) + MediaCompressor().compress(uri, mimeType, compressionQuality, context.applicationContext, useH265) } else { MediaCompressorResult(uri, mimeType, null) } @@ -304,8 +305,9 @@ class UploadOrchestrator { server: ServerName, account: Account, context: Context, + useH265: Boolean = false, ): UploadingFinalState { - val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context) + val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265) return when (server.type) { ServerType.NIP95 -> uploadNIP95(compressed.uri, compressed.contentType, null, null, context) @@ -324,8 +326,9 @@ class UploadOrchestrator { server: ServerName, account: Account, context: Context, + useH265: Boolean = false, ): UploadingFinalState { - val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context) + val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265) val encrypted = EncryptFiles().encryptFile(context, compressed.uri, encrypt) return when (server.type) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt index 6f6b929df..adab194f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt @@ -29,6 +29,7 @@ import android.text.format.Formatter.formatFileSize import android.util.Log import android.widget.Toast import com.abedelazizshe.lightcompressorlibrary.CompressionListener +import com.abedelazizshe.lightcompressorlibrary.VideoCodec import com.abedelazizshe.lightcompressorlibrary.VideoCompressor import com.abedelazizshe.lightcompressorlibrary.config.AppSpecificStorageConfiguration import com.abedelazizshe.lightcompressorlibrary.config.Configuration @@ -141,6 +142,7 @@ object VideoCompressionHelper { contentType: String?, applicationContext: Context, mediaQuality: CompressorQuality, + useH265: Boolean = false, timeoutMs: Long = 60_000L, // configurable, default 60s ): MediaCompressorResult { val videoInfo = getVideoInfo(uri, applicationContext) @@ -186,6 +188,7 @@ object VideoCompressionHelper { resizer = resizer, videoNames = listOf(UUID.randomUUID().toString()), isMinBitrateCheckEnabled = false, + videoCodec = if (useH265) VideoCodec.H265 else VideoCodec.H264, ), listener = object : CompressionListener { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 66f922c90..ef1f45c27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -261,7 +261,7 @@ fun EditPostView( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 630f52573..b384c5503 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -62,6 +62,9 @@ open class NewMediaModel : ViewModel() { // 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED var mediaQualitySlider by mutableIntStateOf(1) + // Codec selection: false = H264, true = H265 + var useH265Codec by mutableStateOf(false) + open fun load( account: Account, uris: ImmutableList, @@ -111,6 +114,7 @@ open class NewMediaModel : ViewModel() { serverToUse, myAccount, context, + useH265Codec, ) if (results.allGood) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 899c3c59c..1af6109fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -261,4 +261,18 @@ fun ImageVideoPost( steps = 2, ) } + + // Only show H.265 codec option if there are videos in the upload + if (postViewModel.multiOrchestrator?.hasVideo() == true) { + SettingSwitchItem( + title = R.string.video_codec_h265_label, + description = R.string.video_codec_h265_description, + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + checked = postViewModel.useH265Codec, + onCheckedChange = { postViewModel.useH265Codec = it }, + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt index 0d3b1052a..84e445182 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt @@ -78,7 +78,7 @@ import kotlinx.collections.immutable.toImmutableList fun ImageVideoDescription( uris: MultiOrchestrator, defaultServer: ServerName, - onAdd: (String, ServerName, Boolean, Int) -> Unit, + onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit, onDelete: (SelectedMediaProcessing) -> Unit, onCancel: () -> Unit, accountViewModel: AccountViewModel, @@ -91,7 +91,7 @@ fun ImageVideoDescription( uris: MultiOrchestrator, defaultServer: ServerName, includeNIP95: Boolean, - onAdd: (String, ServerName, Boolean, Int) -> Unit, + onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit, onDelete: (SelectedMediaProcessing) -> Unit, onCancel: () -> Unit, accountViewModel: AccountViewModel, @@ -128,6 +128,9 @@ fun ImageVideoDescription( // 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED var mediaQualitySlider by remember { mutableIntStateOf(1) } + // Codec selection: false = H264, true = H265 + var useH265Codec by remember { mutableStateOf(false) } + Column( modifier = Modifier @@ -294,32 +297,40 @@ fun ImageVideoDescription( } } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Box(modifier = Modifier.fillMaxWidth()) { - Text( - text = - when (mediaQualitySlider) { - 0 -> stringRes(R.string.media_compression_quality_low) - 1 -> stringRes(R.string.media_compression_quality_medium) - 2 -> stringRes(R.string.media_compression_quality_high) - 3 -> stringRes(R.string.media_compression_quality_uncompressed) - else -> stringRes(R.string.media_compression_quality_medium) - }, - modifier = Modifier.align(Alignment.Center), - ) - } - - Slider( - value = mediaQualitySlider.toFloat(), - onValueChange = { mediaQualitySlider = it.toInt() }, - valueRange = 0f..3f, - steps = 2, + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = + when (mediaQualitySlider) { + 0 -> stringRes(R.string.media_compression_quality_low) + 1 -> stringRes(R.string.media_compression_quality_medium) + 2 -> stringRes(R.string.media_compression_quality_high) + 3 -> stringRes(R.string.media_compression_quality_uncompressed) + else -> stringRes(R.string.media_compression_quality_medium) + }, + modifier = Modifier.align(Alignment.Center), ) } + + Slider( + value = mediaQualitySlider.toFloat(), + onValueChange = { mediaQualitySlider = it.toInt() }, + valueRange = 0f..3f, + steps = 2, + ) + } + + if (uris.first().media.isVideo() == true) { + SettingSwitchItem( + title = R.string.video_codec_h265_label, + description = R.string.video_codec_h265_description, + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + checked = useH265Codec, + onCheckedChange = { useH265Codec = it }, + ) } Button( @@ -327,7 +338,7 @@ fun ImageVideoDescription( Modifier .fillMaxWidth() .padding(vertical = 10.dp), - onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider) }, + onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider, useH265Codec) }, shape = QuoteBorder, colors = ButtonDefaults.buttonColors( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index c692fc600..e53d66b46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -285,7 +285,7 @@ private fun GenericCommentPostBody( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index f339704eb..cb2c7965e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -278,7 +278,7 @@ fun GroupDMScreenContent( ImageVideoDescription( selectedFiles, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.uploadAndHold( accountViewModel.toastManager::toast, context, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index f12efbad8..b0c6b8823 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -266,7 +266,7 @@ private fun NewProductBody( uris = it, defaultServer = accountViewModel.account.settings.defaultFileServer, includeNIP95 = false, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 1929fc017..4a205f158 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -326,7 +326,7 @@ private fun NewPostScreenBody( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 09d47edbb..a4d3100cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -231,7 +231,7 @@ fun PublicMessageScreenContent( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, - onAdd = { alt, server, sensitiveContent, mediaQuality -> + onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 9c6f4283c..438dab8e3 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1039,6 +1039,8 @@ Medium High Uncompressed + Use H.265/HEVC Codec + Better quality at smaller file sizes but not all devices support H.265 playback. Edit draft diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 691f31f5f..ca144d44a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ languageId = "17.0.6" lazysodiumAndroid = "5.2.0" lazysodiumJava = "5.2.0" lifecycleRuntimeKtx = "2.9.4" -lightcompressor = "1.4.0" +lightcompressor = "51defaaa8d" markdown = "e1151c8" media3 = "1.8.0" mockk = "1.14.5"