From b815fc24241131a7314efe04d14f8cf3b08ac79d Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Sun, 23 Oct 2022 16:05:02 +0200 Subject: [PATCH] Finish stabilization --- .../app/postCreation/PostCreationActivity.kt | 72 +++++++++++++++++-- .../app/postCreation/PostCreationViewModel.kt | 8 ++- .../app/postCreation/carousel/CarouselItem.kt | 3 +- .../postCreation/carousel/ImageCarousel.kt | 9 ++- .../photoEdit/VideoEditActivity.kt | 10 +-- .../java/org/pixeldroid/app/utils/Utils.kt | 7 ++ app/src/main/res/values/strings.xml | 7 +- 7 files changed, 96 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt index 357ae155..772bea34 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt @@ -37,6 +37,7 @@ import org.pixeldroid.app.postCreation.carousel.CarouselItem import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity +import org.pixeldroid.app.utils.convert import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.ffmpegCompliantUri @@ -58,6 +59,7 @@ data class PhotoData( var imageDescription: String? = null, var video: Boolean, var videoEncodeProgress: Int? = null, + var videoEncodeStabilizationFirstPass: Boolean? = null, ) class PostCreationActivity : BaseThemedWithoutBarActivity() { @@ -92,7 +94,7 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { // update UI binding.carousel.addData( newPhotoData.map { - CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) + CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass) } ) } @@ -358,7 +360,7 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(ffmpegCompliantUri(inputUri)).mediaInformation val totalVideoDuration = mediaInformation?.duration?.toFloatOrNull() - fun secondPass(){ + fun secondPass(stabilizeString: String = ""){ val speed = VideoEditActivity.speedChoices[speedIndex] val mutedString = if(muted || speedIndex != 1) "-an" else null @@ -371,8 +373,10 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { val separator = if(speedIndex != 1 && !crop.notCropped()) "," else "" val speedString = if(speedIndex != 1) "setpts=PTS/${speed}" else "" - val speedAndCropString: List = if(speedIndex!= 1 || !crop.notCropped()) - listOf("-filter:v", speedString + separator + cropString) + val separatorStabilize = if(stabilizeString == "" || (speedString == "" && cropString == "")) "" else "," + + val speedAndCropString: List = if(speedIndex!= 1 || !crop.notCropped() || stabilizeString.isNotEmpty()) + listOf("-filter:v", stabilizeString + separatorStabilize + speedString + separator + cropString) // Stream copy is not compatible with filter, but when not filtering we can copy the stream without re-encoding else listOf("-c", "copy") @@ -447,8 +451,64 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { } fun stabilizationFirstPass(){ -//TODO FFmpeg - secondPass() + + val shakeResultsFile = File.createTempFile("temp_shake_results", ".trf", cacheDir) + model.trackTempFile(shakeResultsFile) + val shakeResultsFileUri = shakeResultsFile.toUri() + val shakeResultsFileSafeUri = ffmpegCompliantUri(shakeResultsFileUri).removePrefix("file://") + + val inputSafeUri: String = ffmpegCompliantUri(inputUri) + + // Map chosen "stabilization force" to shakiness, from 3 to 10 + val shakiness = (0f..100f).convert(stabilize, 3f..10f).roundToInt() + + val analyzeVideoCommandList = listOf( + "-y", "-i", inputSafeUri, + "-vf", "vidstabdetect=shakiness=$shakiness:accuracy=15:result=$shakeResultsFileSafeUri", + "-f", "null", "-" + ).toTypedArray() + + FFmpegKit.executeWithArgumentsAsync(analyzeVideoCommandList, + { firstPass -> + if (ReturnCode.isSuccess(firstPass.returnCode)) { + // Map chosen "stabilization force" to shakiness, from 8 to 40 + val smoothing = (0f..100f).convert(stabilize, 8f..40f).roundToInt() + + val stabilizeVideoCommand = + "vidstabtransform=smoothing=$smoothing:input=${ffmpegCompliantUri(shakeResultsFileUri).removePrefix("file://")}" + secondPass(stabilizeVideoCommand) + } else { + Log.e( + "PostCreationActivityEncoding", + "Video stabilization first pass failed!" + ) + } + }, + { log -> Log.d("PostCreationActivityEncoding", log.message) }, + { statistics: Statistics? -> + + val timeInMilliseconds: Int? = statistics?.time + timeInMilliseconds?.let { + if (timeInMilliseconds > 0) { + val completePercentage = totalVideoDuration?.let { + val speedupDurationModifier = + VideoEditActivity.speedChoices[speedIndex].toFloat() + + val newTotalDuration = (it - (videoStart ?: 0f) - (it - (videoEnd + ?: it))) / speedupDurationModifier + timeInMilliseconds / (10 * newTotalDuration) + } + resultHandler.post { + completePercentage?.let { + val rounded: Int = it.roundToInt() + model.setVideoEncodeAtPosition(position, rounded, true) + binding.carousel.updateProgress(rounded, position, false) + } + } + Log.d(TAG, "Stabilization pass: %$completePercentage.") + } + } + }) } if(stabilize > 0.01f) { diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt index ec7064af..a701a717 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -196,8 +196,8 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList() } - fun setVideoEncodeAtPosition(position: Int, progress: Int?) { - photoData.value?.set(position, photoData.value!![position].copy(videoEncodeProgress = progress)) + fun setVideoEncodeAtPosition(position: Int, progress: Int?, stabilizationFirstPass: Boolean = false) { + photoData.value?.set(position, photoData.value!![position].copy(videoEncodeProgress = progress, videoEncodeStabilizationFirstPass = stabilizationFirstPass)) photoData.value = photoData.value } @@ -377,9 +377,11 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null val videoCrop: RelativeCropPosition = data.getSerializableExtra(VideoEditActivity.VIDEO_CROP) as RelativeCropPosition - val videoStabilize: Float = data.getFloatExtra(VideoEditActivity.VIDEO_STABILIZE, 1f) + val videoStabilize: Float = data.getFloatExtra(VideoEditActivity.VIDEO_STABILIZE, 0f) + videoEncodeStabilizationFirstPass = videoStabilize > 0.01f videoEncodeProgress = 0 + sessionMap[position]?.let { FFmpegKit.cancel(it) } _uiState.update { currentUiState -> currentUiState.copy( diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt index bbd4b8c7..6adb2b83 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt @@ -6,5 +6,6 @@ data class CarouselItem constructor( val imageUrl: Uri, val caption: String? = null, val video: Boolean, - var encodeProgress: Int? + var encodeProgress: Int?, + var stabilizationFirstPass: Boolean? ) \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt index d67ef4d1..97b9f89c 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt @@ -95,8 +95,9 @@ class ImageCarousel( if (thisProgress != null) { binding.encodeInfoCard.visibility = VISIBLE binding.encodeProgress.visibility = VISIBLE - binding.encodeInfoText.text = - context.getString(R.string.encode_progress).format(thisProgress) + binding.encodeInfoText.text = (if(data?.getOrNull(position)?.stabilizationFirstPass == true){ + context.getString(R.string.analyzing_stabilization) + } else context.getString(R.string.encode_progress)).format(thisProgress) binding.encodeProgress.progress = thisProgress } else { binding.encodeInfoCard.visibility = GONE @@ -582,7 +583,9 @@ class ImageCarousel( binding.encodeProgress.visibility = VISIBLE binding.encodeInfoCard.visibility = VISIBLE binding.encodeProgress.progress = progress - binding.encodeInfoText.text = context.getString(R.string.encode_progress).format(progress) + binding.encodeInfoText.text = (if(data?.getOrNull(position)?.stabilizationFirstPass == true){ + context.getString(R.string.analyzing_stabilization) + } else context.getString(R.string.encode_progress)).format(progress) } } } diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt index bd82a195..43060fed 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt @@ -76,7 +76,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() { binding.stabilisationSaved.isVisible = true val typedValue = TypedValue() val color: Int = if (binding.stabilizer.context.theme - .resolveAttribute(R.attr.colorOnPrimaryContainer, typedValue, true) + .resolveAttribute(R.attr.colorSecondary, typedValue, true) ) typedValue.data else Color.TRANSPARENT binding.stabilizer.drawable.setTint(color) @@ -196,7 +196,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() { if(!cropRelativeDimensions.notCropped()){ val typedValue = TypedValue() val color: Int = if (binding.checkMarkCropped.context.theme - .resolveAttribute(R.attr.colorOnPrimaryContainer, typedValue, true) + .resolveAttribute(R.attr.colorSecondary, typedValue, true) ) typedValue.data else Color.TRANSPARENT binding.cropper.drawable.setTint(color) @@ -279,7 +279,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() { thumbnail(uri, resultHandler, binding.thumbnail7, it.times(7)) } - + resetControls() } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -328,7 +328,9 @@ class VideoEditActivity : BaseThemedWithBarActivity() { val muted = binding.muter.isSelected val speedUnchanged = speed == 1 - return !muted && videoPositions && speedUnchanged && cropRelativeDimensions.notCropped() + val stabilizationUnchanged = stabilization <= 0.01f || stabilization > 100.5f + + return !muted && videoPositions && speedUnchanged && cropRelativeDimensions.notCropped() && stabilizationUnchanged } private fun showCropInterface(show: Boolean, uri: Uri? = null){ diff --git a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt index 23249264..708c65a1 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt @@ -15,6 +15,7 @@ import android.view.WindowManager import android.webkit.MimeTypeMap import androidx.annotation.AttrRes import androidx.annotation.ColorInt +import androidx.annotation.FloatRange import androidx.annotation.StyleRes import androidx.appcompat.app.AppCompatDelegate import androidx.browser.customtabs.CustomTabsIntent @@ -234,6 +235,12 @@ fun Context.themeActionBar(): Int? { } } +/** Maps a Float from this range to target range */ +fun ClosedRange.convert(number: Float, target: ClosedRange): Float { + val ratio = number / (endInclusive - start) + return (ratio * (target.endInclusive - target.start)) +} + @ColorInt fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5391c024..70e9d054 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -268,6 +268,10 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Error encoding Encode success! Encode %1$d%% + Stabilize video + Change intensity of stabilization + Stabilization saved + Analysis for stabilization %1$d%% Select what to keep of the video Mute video Change video speed @@ -296,9 +300,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Error while adding images "Thumbnail of image in this notification's post" Preview of a post - Stabilize video - Change intensity of stabilization - Stabilization saved %d reply %d replies