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