PixelDroid-App-Android/mediaEditor/src/main/java/org/pixeldroid/media_editor/photoEdit/VideoEditActivity.kt

647 lines
28 KiB
Kotlin
Raw Normal View History

2022-10-28 20:49:25 +02:00
package org.pixeldroid.media_editor.photoEdit
2022-06-10 23:41:29 +02:00
2022-06-18 22:21:19 +02:00
import android.app.Activity
import android.app.AlertDialog
2022-10-28 20:49:25 +02:00
import android.content.ContentResolver
import android.content.Context
2022-06-18 22:21:19 +02:00
import android.content.Intent
2022-10-21 00:03:08 +02:00
import android.graphics.Color
2022-10-19 00:19:42 +02:00
import android.graphics.Rect
2022-06-18 22:21:19 +02:00
import android.media.AudioManager
2022-06-10 23:41:29 +02:00
import android.net.Uri
import android.os.Bundle
2022-06-18 22:21:19 +02:00
import android.os.Handler
import android.os.Looper
import android.text.format.DateUtils
2022-06-10 23:41:29 +02:00
import android.util.Log
2022-10-21 00:03:08 +02:00
import android.util.TypedValue
2022-06-18 22:21:19 +02:00
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
2022-10-28 20:49:25 +02:00
import androidx.appcompat.app.AppCompatActivity
2022-06-10 23:41:29 +02:00
import androidx.core.net.toUri
2022-06-18 22:21:19 +02:00
import androidx.core.os.HandlerCompat
2022-10-21 00:03:08 +02:00
import androidx.core.view.isVisible
2022-06-18 22:21:19 +02:00
import androidx.media.AudioAttributesCompat
import androidx.media2.common.MediaMetadata
import androidx.media2.common.UriMediaItem
import androidx.media2.player.MediaPlayer
2022-10-19 00:19:42 +02:00
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.FFmpegKitConfig
2022-10-28 20:49:25 +02:00
import com.arthenica.ffmpegkit.FFmpegSession
2022-10-19 00:19:42 +02:00
import com.arthenica.ffmpegkit.FFprobeKit
import com.arthenica.ffmpegkit.MediaInformation
import com.arthenica.ffmpegkit.ReturnCode
2022-10-28 20:49:25 +02:00
import com.arthenica.ffmpegkit.Statistics
2022-06-10 23:41:29 +02:00
import com.bumptech.glide.Glide
2022-06-18 22:21:19 +02:00
import com.google.android.material.slider.RangeSlider
2022-10-22 22:07:03 +02:00
import com.google.android.material.slider.Slider
2022-10-28 20:49:25 +02:00
import org.pixeldroid.media_editor.R
import org.pixeldroid.media_editor.databinding.ActivityVideoEditBinding
2022-06-10 23:41:29 +02:00
import java.io.File
2022-10-19 00:19:42 +02:00
import java.io.Serializable
import kotlin.math.absoluteValue
2022-10-28 20:49:25 +02:00
import kotlin.math.roundToInt
2022-06-10 23:41:29 +02:00
2022-10-28 20:49:25 +02:00
const val TAG = "VideoEditActivity"
class VideoEditActivity : AppCompatActivity() {
2022-06-18 22:21:19 +02:00
2022-10-19 00:19:42 +02:00
data class RelativeCropPosition(
2022-10-21 00:03:08 +02:00
// Width of the selected part of the video, relative to the width of the video
val relativeWidth: Float = 1f,
// Height of the selected part of the video, relative to the height of the video
val relativeHeight: Float = 1f,
// Distance of left corner of selected part, relative to the width of the video
val relativeX: Float = 0f,
// Distance of top of selected part, relative to the height of the video
val relativeY: Float = 0f,
2022-10-19 00:19:42 +02:00
): Serializable {
fun notCropped(): Boolean =
2022-10-21 00:03:08 +02:00
(relativeWidth - 1f).absoluteValue < 0.001f
&& (relativeHeight - 1f).absoluteValue < 0.001f
2022-10-19 00:19:42 +02:00
&& relativeX.absoluteValue < 0.001f
2022-10-21 00:03:08 +02:00
&& relativeY.absoluteValue < 0.001f
2022-10-19 00:19:42 +02:00
}
2022-10-28 20:49:25 +02:00
data class VideoEditArguments(
val muted: Boolean,
val videoStart: Float?,
val videoEnd: Float? ,
val speedIndex: Int,
val videoCrop: RelativeCropPosition,
val videoStabilize: Float
): Serializable
private lateinit var videoUri: Uri
2022-06-18 22:21:19 +02:00
private lateinit var mediaPlayer: MediaPlayer
private var videoPosition: Int = -1
2022-10-15 20:13:29 +02:00
2022-10-21 00:03:08 +02:00
private var cropRelativeDimensions: RelativeCropPosition = RelativeCropPosition()
2022-10-19 00:19:42 +02:00
2022-10-22 22:07:03 +02:00
private var stabilization: Float = 0f
set(value){
field = value
if(value > 0.01f && value <= 100f){
// Stabilization requested, show UI
binding.stabilisationSaved.isVisible = true
val typedValue = TypedValue()
val color: Int = if (binding.stabilizer.context.theme
2022-10-23 16:05:02 +02:00
.resolveAttribute(R.attr.colorSecondary, typedValue, true)
2022-10-22 22:07:03 +02:00
) typedValue.data else Color.TRANSPARENT
binding.stabilizer.drawable.setTint(color)
}
else {
binding.stabilisationSaved.isVisible = false
binding.stabilizer.drawable.setTintList(null)
}
}
2022-10-15 20:13:29 +02:00
private var speed: Int = 1
set(value) {
field = value
mediaPlayer.playbackSpeed = speedChoices[value].toFloat()
if(speed != 1) binding.muter.callOnClick()
}
2022-06-18 22:21:19 +02:00
private lateinit var binding: ActivityVideoEditBinding
// Map photoData indexes to FFmpeg Session IDs
private val sessionList: ArrayList<Long> = arrayListOf()
private val tempFiles: ArrayList<File> = ArrayList()
2022-06-10 23:41:29 +02:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
2022-06-18 22:21:19 +02:00
binding = ActivityVideoEditBinding.inflate(layoutInflater)
2022-06-10 23:41:29 +02:00
setContentView(binding.root)
2022-06-18 22:21:19 +02:00
supportActionBar?.setTitle(R.string.toolbar_title_edit)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeButtonEnabled(true)
binding.videoRangeSeekBar.setCustomThumbDrawablesForValues(R.drawable.thumb_left,R.drawable.double_circle,R.drawable.thumb_right)
binding.videoRangeSeekBar.thumbRadius = 20.dpToPx(this)
val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
2022-10-28 20:49:25 +02:00
videoUri = intent.getParcelableExtra(PhotoEditActivity.PICTURE_URI)!!
2022-10-17 13:23:02 +02:00
2022-06-18 22:21:19 +02:00
videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
2022-10-28 20:49:25 +02:00
val inputVideoPath = ffmpegCompliantUri(videoUri)
2022-06-10 23:41:29 +02:00
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation
2022-06-18 22:21:19 +02:00
//Duration in seconds, or null
val duration: Float? = mediaInformation?.duration?.toFloatOrNull()
binding.videoRangeSeekBar.valueFrom = 0f
binding.videoRangeSeekBar.valueTo = duration ?: 100f
binding.videoRangeSeekBar.values = listOf(0f,(duration?: 100f) / 2, duration ?: 100f)
2022-06-10 23:41:29 +02:00
2022-10-28 20:49:25 +02:00
val mediaItem: UriMediaItem = UriMediaItem.Builder(videoUri).build()
2022-06-18 22:21:19 +02:00
mediaItem.metadata = MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_TITLE, "")
.build()
2022-06-10 23:41:29 +02:00
2022-06-18 22:21:19 +02:00
mediaPlayer = MediaPlayer(this)
mediaPlayer.setMediaItem(mediaItem)
//binding.videoView.mediaControlView?.setMediaController()
// Configure audio
mediaPlayer.setAudioAttributes(AudioAttributesCompat.Builder()
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.setUsage(AudioAttributesCompat.USAGE_MEDIA)
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MOVIE)
.build()
2022-06-10 23:41:29 +02:00
)
2022-06-18 22:21:19 +02:00
findViewById<FrameLayout?>(R.id.progress_bar)?.visibility = View.GONE
mediaPlayer.prepare()
2022-06-10 23:41:29 +02:00
2022-10-15 20:13:29 +02:00
2022-06-18 22:21:19 +02:00
binding.muter.setOnClickListener {
if(!binding.muter.isSelected) mediaPlayer.playerVolume = 0f
2022-10-15 20:13:29 +02:00
else {
mediaPlayer.playerVolume = 1f
speed = 1
}
2022-06-18 22:21:19 +02:00
binding.muter.isSelected = !binding.muter.isSelected
2022-06-10 23:41:29 +02:00
}
2022-10-19 00:19:42 +02:00
binding.cropper.setOnClickListener {
2022-10-28 20:49:25 +02:00
showCropInterface(show = true, uri = videoUri)
2022-10-19 00:19:42 +02:00
}
binding.saveCropButton.setOnClickListener {
// This is the rectangle selected by the crop
2022-10-21 17:11:37 +02:00
val cropRect = binding.cropImageView.cropWindowRect
2022-10-19 00:19:42 +02:00
// This is the rectangle of the whole image
val fullImageRect: Rect = binding.cropImageView.getInitialCropWindowRect()
// x, y are coordinates of top left, in the ImageView
val x = cropRect.left - fullImageRect.left
val y = cropRect.top - fullImageRect.top
// width and height selected by the crop
val width = cropRect.width()
val height = cropRect.height()
// To avoid having to calculate the dimensions of the video here, we pass
// relative width, height and x, y back to be treated in FFmpeg
cropRelativeDimensions = RelativeCropPosition(
relativeWidth = width/fullImageRect.width(),
relativeHeight = height/fullImageRect.height(),
relativeX = x/fullImageRect.width(),
relativeY = y/fullImageRect.height()
)
2022-10-21 00:03:08 +02:00
// If a crop was saved, change the color of the crop button to give a visual indication
if(!cropRelativeDimensions.notCropped()){
val typedValue = TypedValue()
val color: Int = if (binding.checkMarkCropped.context.theme
2022-10-23 16:05:02 +02:00
.resolveAttribute(R.attr.colorSecondary, typedValue, true)
2022-10-21 00:03:08 +02:00
) typedValue.data else Color.TRANSPARENT
binding.cropper.drawable.setTint(color)
} else {
// Else reset the tint
binding.cropper.drawable.setTintList(null)
}
2022-10-19 00:19:42 +02:00
showCropInterface(show = false)
}
2022-06-18 22:21:19 +02:00
binding.videoView.setPlayer(mediaPlayer)
mediaPlayer.seekTo((binding.videoRangeSeekBar.values[1]*1000).toLong())
object : Runnable {
override fun run() {
val getCurrent = mediaPlayer.currentPosition / 1000f
if(getCurrent >= binding.videoRangeSeekBar.values[0] && getCurrent <= binding.videoRangeSeekBar.values[2] ) {
binding.videoRangeSeekBar.values = listOf(binding.videoRangeSeekBar.values[0],getCurrent, binding.videoRangeSeekBar.values[2])
}
Handler(Looper.getMainLooper()).postDelayed(this, 1000)
}
}.run()
binding.videoRangeSeekBar.addOnChangeListener { rangeSlider: RangeSlider, value, fromUser ->
// Responds to when the middle slider's value is changed
if(fromUser && value != rangeSlider.values[0] && value != rangeSlider.values[2]) {
mediaPlayer.seekTo((rangeSlider.values[1]*1000).toLong())
}
}
binding.videoRangeSeekBar.setLabelFormatter { value: Float ->
DateUtils.formatElapsedTime(value.toLong())
}
2022-10-15 20:13:29 +02:00
binding.speeder.setOnClickListener {
AlertDialog.Builder(this).apply {
setIcon(R.drawable.speed)
setTitle(R.string.video_speed)
setSingleChoiceItems(speedChoices.map { it.toString() + "x" }.toTypedArray(), speed) { dialog, which ->
// update the selected item which is selected by the user so that it should be selected
// when user opens the dialog next time and pass the instance to setSingleChoiceItems method
speed = which
// when selected an item the dialog should be closed with the dismiss method
dialog.dismiss()
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
}.show()
}
2022-10-22 22:07:03 +02:00
binding.stabilizer.setOnClickListener {
AlertDialog.Builder(this).apply {
setIcon(R.drawable.video_stable)
setTitle(R.string.stabilize_video_intensity)
val slider = Slider(context).apply {
valueFrom = 0f
valueTo = 100f
value = stabilization
}
setView(slider)
setNegativeButton(android.R.string.cancel) { _, _ -> }
setPositiveButton(android.R.string.ok) { _, _ -> stabilization = slider.value}
}.show()
}
2022-10-15 20:13:29 +02:00
2022-06-18 22:21:19 +02:00
val thumbInterval: Float? = duration?.div(7)
thumbInterval?.let {
2022-10-28 20:49:25 +02:00
thumbnail(videoUri, resultHandler, binding.thumbnail1, it)
thumbnail(videoUri, resultHandler, binding.thumbnail2, it.times(2))
thumbnail(videoUri, resultHandler, binding.thumbnail3, it.times(3))
thumbnail(videoUri, resultHandler, binding.thumbnail4, it.times(4))
thumbnail(videoUri, resultHandler, binding.thumbnail5, it.times(5))
thumbnail(videoUri, resultHandler, binding.thumbnail6, it.times(6))
thumbnail(videoUri, resultHandler, binding.thumbnail7, it.times(7))
2022-06-18 22:21:19 +02:00
}
2022-10-23 16:05:02 +02:00
resetControls()
2022-06-18 22:21:19 +02:00
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.edit_menu, menu)
return true
2022-06-10 23:41:29 +02:00
}
2022-06-18 22:21:19 +02:00
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
R.id.action_save -> {
returnWithValues()
}
R.id.action_reset -> {
resetControls()
}
}
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
if(binding.cropImageView.isVisible) {
showCropInterface(false)
} else if (noEdits()) super.onBackPressed()
2022-06-18 22:21:19 +02:00
else {
val builder = AlertDialog.Builder(this)
builder.apply {
setMessage(R.string.save_before_returning)
setPositiveButton(android.R.string.ok) { _, _ ->
returnWithValues()
}
setNegativeButton(R.string.no_cancel_edit) { _, _ ->
super.onBackPressed()
}
}
// Create the AlertDialog
builder.show()
}
}
private fun noEdits(): Boolean {
val videoPositions = binding.videoRangeSeekBar.values.let {
it[0] == 0f && it[2] == binding.videoRangeSeekBar.valueTo
}
val muted = binding.muter.isSelected
2022-10-15 20:13:29 +02:00
val speedUnchanged = speed == 1
2022-10-23 16:05:02 +02:00
val stabilizationUnchanged = stabilization <= 0.01f || stabilization > 100.5f
return !muted && videoPositions && speedUnchanged && cropRelativeDimensions.notCropped() && stabilizationUnchanged
2022-06-18 22:21:19 +02:00
}
2022-10-19 00:19:42 +02:00
private fun showCropInterface(show: Boolean, uri: Uri? = null){
val visibilityOfOthers = if(show) View.GONE else View.VISIBLE
val visibilityOfCrop = if(show) View.VISIBLE else View.GONE
if(show) mediaPlayer.pause()
2022-10-21 00:03:08 +02:00
if(show) binding.cropSavedCard.visibility = View.GONE
else if(!cropRelativeDimensions.notCropped()) binding.cropSavedCard.visibility = View.VISIBLE
2022-10-21 00:03:08 +02:00
2022-10-22 22:07:03 +02:00
binding.stabilisationSaved.visibility =
if(!show && stabilization > 0.01f && stabilization <= 100f) View.VISIBLE
else View.GONE
2022-10-19 00:19:42 +02:00
binding.muter.visibility = visibilityOfOthers
binding.speeder.visibility = visibilityOfOthers
binding.cropper.visibility = visibilityOfOthers
2022-10-22 22:07:03 +02:00
binding.stabilizer.visibility = visibilityOfOthers
2022-10-19 00:19:42 +02:00
binding.videoRangeSeekBar.visibility = visibilityOfOthers
binding.videoView.visibility = visibilityOfOthers
binding.thumbnail1.visibility = visibilityOfOthers
binding.thumbnail2.visibility = visibilityOfOthers
binding.thumbnail3.visibility = visibilityOfOthers
binding.thumbnail4.visibility = visibilityOfOthers
binding.thumbnail5.visibility = visibilityOfOthers
binding.thumbnail6.visibility = visibilityOfOthers
binding.thumbnail7.visibility = visibilityOfOthers
2022-10-21 00:03:08 +02:00
2022-10-19 00:19:42 +02:00
binding.cropImageView.visibility = visibilityOfCrop
binding.saveCropButton.visibility = visibilityOfCrop
if(show && uri != null) binding.cropImageView.setImageUriAsync(uri, cropRelativeDimensions)
}
2022-06-18 22:21:19 +02:00
private fun returnWithValues() {
2022-10-22 22:07:03 +02:00
//TODO Check if some of these should be null to indicate no changes in that category? Ex start/end
2022-10-28 20:49:25 +02:00
val intent = Intent()
2022-06-18 22:21:19 +02:00
.apply {
putExtra(PhotoEditActivity.PICTURE_POSITION, videoPosition)
2022-10-28 20:49:25 +02:00
putExtra(VIDEO_ARGUMENTS_TAG, VideoEditArguments(
binding.muter.isSelected, binding.videoRangeSeekBar.values.first(),
binding.videoRangeSeekBar.values[2],
speed,
cropRelativeDimensions,
stabilization
)
)
2022-06-18 22:21:19 +02:00
putExtra(MODIFIED, !noEdits())
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
}
setResult(Activity.RESULT_OK, intent)
finish()
}
private fun resetControls() {
binding.videoRangeSeekBar.values = listOf(0f, binding.videoRangeSeekBar.valueTo/2, binding.videoRangeSeekBar.valueTo)
binding.muter.isSelected = false
2022-10-21 00:03:08 +02:00
2022-10-19 00:19:42 +02:00
binding.cropImageView.resetCropRect()
2022-10-21 00:03:08 +02:00
cropRelativeDimensions = RelativeCropPosition()
binding.cropper.drawable.setTintList(null)
2022-10-22 22:07:03 +02:00
binding.stabilizer.drawable.setTintList(null)
2022-10-21 00:03:08 +02:00
binding.cropSavedCard.visibility = View.GONE
2022-10-22 22:07:03 +02:00
stabilization = 0f
2022-06-18 22:21:19 +02:00
}
override fun onDestroy() {
super.onDestroy()
sessionList.forEach {
FFmpegKit.cancel(it)
}
tempFiles.forEach{
it.delete()
}
mediaPlayer.close()
}
private fun thumbnail(
inputUri: Uri?,
resultHandler: Handler,
thumbnail: ImageView,
thumbTime: Float,
) {
2022-08-21 00:32:14 +02:00
val file = File.createTempFile("temp_img", ".bmp", cacheDir)
2022-06-18 22:21:19 +02:00
tempFiles.add(file)
val fileUri = file.toUri()
2022-08-21 00:31:36 +02:00
val ffmpegCompliantUri = ffmpegCompliantUri(inputUri)
2022-06-18 22:21:19 +02:00
val outputImagePath =
if(fileUri.toString().startsWith("content://"))
FFmpegKitConfig.getSafParameterForWrite(this, fileUri)
else fileUri.toString()
2022-08-21 00:31:36 +02:00
val session = FFmpegKit.executeWithArgumentsAsync(arrayOf(
"-noaccurate_seek", "-ss", "$thumbTime", "-i", ffmpegCompliantUri, "-vf",
"scale=${thumbnail.width}:${thumbnail.height}",
"-frames:v", "1", "-f", "image2", "-y", outputImagePath), { session ->
2022-06-18 22:21:19 +02:00
val state = session.state
val returnCode = session.returnCode
if (ReturnCode.isSuccess(returnCode)) {
// SUCCESS
resultHandler.post {
if(!this.isFinishing)
Glide.with(this).load(outputImagePath).centerCrop().into(thumbnail)
}
}
// CALLED WHEN SESSION IS EXECUTED
Log.d("VideoEditActivity", "FFmpeg process exited with state $state and rc $returnCode.${session.failStackTrace}")
},
2022-08-21 00:31:36 +02:00
{/* CALLED WHEN SESSION PRINTS LOGS */ }, { /*CALLED WHEN SESSION GENERATES STATISTICS*/ })
2022-06-18 22:21:19 +02:00
sessionList.add(session.sessionId)
}
override fun onPause() {
super.onPause()
mediaPlayer.pause()
}
2022-06-10 23:41:29 +02:00
companion object {
2022-10-28 20:49:25 +02:00
const val VIDEO_ARGUMENTS_TAG = "org.pixeldroid.media_editor.VideoEditTag"
2022-10-15 20:13:29 +02:00
// List of choices of speeds
val speedChoices: List<Number> = listOf(0.5, 1, 2, 4, 8)
2022-06-18 22:21:19 +02:00
const val MODIFIED = "VideoEditModifiedTag"
2022-10-28 20:49:25 +02:00
/**
* @param muted should audio tracks be removed in the output
* @param videoStart when we want to start the video, in seconds, or null if we
* don't want to remove the start
* @param videoEnd when we want to end the video, in seconds, or null if we
* don't want to remove the end
*/
fun startEncoding(
originalUri: Uri,
arguments: VideoEditArguments,
context: Context,
//TODO make interfaces for these callbacks, or something more explicit
registerNewFFmpegSession: (Uri, Long) -> Unit,
trackTempFile: (File) -> Unit,
videoEncodeProgress: (Uri, Int, Boolean, Uri?, Boolean) -> Unit,
) {
// Having a meaningful suffix is necessary so that ffmpeg knows what to put in output
val suffix = originalUri.fileExtension(context.contentResolver)
val file = File.createTempFile("temp_video", ".$suffix", context.cacheDir)
//val file = File.createTempFile("temp_video", ".webm", cacheDir)
trackTempFile(file)
val fileUri = file.toUri()
val outputVideoPath = context.ffmpegCompliantUri(fileUri)
val ffmpegCompliantUri: String = context.ffmpegCompliantUri(originalUri)
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(context.ffmpegCompliantUri(originalUri)).mediaInformation
val totalVideoDuration = mediaInformation?.duration?.toFloatOrNull()
fun secondPass(stabilizeString: String = ""){
val speed = speedChoices[arguments.speedIndex]
val mutedString = if(arguments.muted || arguments.speedIndex != 1) "-an" else null
val startString: List<String?> = if(arguments.videoStart != null) listOf("-ss", "${arguments.videoStart/speed.toFloat()}") else listOf(null, null)
val endString: List<String?> = if(arguments.videoEnd != null) listOf("-to", "${arguments.videoEnd/speed.toFloat() - (arguments.videoStart ?: 0f)/speed.toFloat()}") else listOf(null, null)
// iw and ih are variables for the original width and height values, FFmpeg will know them
val cropString = if(arguments.videoCrop.notCropped()) "" else "crop=${arguments.videoCrop.relativeWidth}*iw:${arguments.videoCrop.relativeHeight}*ih:${arguments.videoCrop.relativeX}*iw:${arguments.videoCrop.relativeY}*ih"
val separator = if(arguments.speedIndex != 1 && !arguments.videoCrop.notCropped()) "," else ""
val speedString = if(arguments.speedIndex != 1) "setpts=PTS/${speed}" else ""
val separatorStabilize = if(stabilizeString == "" || (speedString == "" && cropString == "")) "" else ","
val speedAndCropString: List<String?> = if(arguments.speedIndex!= 1 || !arguments.videoCrop.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")
// This should be set when re-encoding is required (otherwise it defaults to mpeg which then doesn't play)
val encodePreset: List<String?> = if(arguments.speedIndex != 1 && !arguments.videoCrop.notCropped()) listOf("-c:v", "libx264", "-preset", "ultrafast") else listOf(null, null, null, null)
val session: FFmpegSession =
FFmpegKit.executeWithArgumentsAsync(listOfNotNull(
startString[0], startString[1],
"-i", ffmpegCompliantUri,
speedAndCropString[0], speedAndCropString[1],
endString[0], endString[1],
mutedString, "-y",
encodePreset[0], encodePreset[1], encodePreset[2], encodePreset[3],
outputVideoPath,
).toTypedArray(),
//val session: FFmpegSession = FFmpegKit.executeAsync("$startString -i $inputSafePath $endString -c:v libvpx-vp9 -c:a copy -an -y $outputVideoPath",
{ session ->
val returnCode = session.returnCode
if (ReturnCode.isSuccess(returnCode)) {
videoEncodeProgress(originalUri, 100, false, outputVideoPath.toUri(), false)
Log.d(TAG, "Encode completed successfully in ${session.duration} milliseconds")
} else {
videoEncodeProgress(originalUri, 0, false, outputVideoPath.toUri(), true)
Log.e(TAG, "Encode failed with state ${session.state} and rc $returnCode.${session.failStackTrace}")
}
},
{ log -> Log.d("PostCreationActivityEncoding", log.message) }
) { statistics: Statistics? ->
val timeInMilliseconds: Int? = statistics?.time
timeInMilliseconds?.let {
if (timeInMilliseconds > 0) {
val completePercentage = totalVideoDuration?.let {
val speedupDurationModifier = speedChoices[arguments.speedIndex].toFloat()
val newTotalDuration = (it - (arguments.videoStart ?: 0f) - (it - (arguments.videoEnd ?: it)))/speedupDurationModifier
timeInMilliseconds / (10*newTotalDuration)
}
completePercentage?.let {
val rounded: Int = it.roundToInt()
videoEncodeProgress(originalUri, rounded, false, null, false)
}
Log.d(TAG, "Encoding video: %$completePercentage.")
}
}
}
registerNewFFmpegSession(originalUri, session.sessionId)
}
fun stabilizationFirstPass(){
val shakeResultsFile = File.createTempFile("temp_shake_results", ".trf", context.cacheDir)
trackTempFile(shakeResultsFile)
val shakeResultsFileUri = shakeResultsFile.toUri()
val shakeResultsFileSafeUri = context.ffmpegCompliantUri(shakeResultsFileUri).removePrefix("file://")
val inputSafeUri: String = context.ffmpegCompliantUri(originalUri)
// Map chosen "stabilization force" to shakiness, from 3 to 10
val shakiness = (0f..100f).convert(arguments.videoStabilize, 3f..10f).roundToInt()
val analyzeVideoCommandList = listOf(
"-y", "-i", inputSafeUri,
"-vf", "vidstabdetect=shakiness=$shakiness:accuracy=15:result=$shakeResultsFileSafeUri",
"-f", "null", "-"
).toTypedArray()
val session: FFmpegSession =
FFmpegKit.executeWithArgumentsAsync(analyzeVideoCommandList,
{ firstPass ->
if (ReturnCode.isSuccess(firstPass.returnCode)) {
// Map chosen "stabilization force" to shakiness, from 8 to 40
val smoothing = (0f..100f).convert(arguments.videoStabilize, 8f..40f).roundToInt()
val stabilizeVideoCommand =
"vidstabtransform=smoothing=$smoothing:input=${context.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 {
// At this stage, we didn't change speed or start/end of the video
timeInMilliseconds / (10 * it)
}
completePercentage?.let {
val rounded: Int = it.roundToInt()
videoEncodeProgress(originalUri, rounded, true, null, false)
}
Log.d(TAG, "Stabilization pass: %$completePercentage.")
}
}
})
registerNewFFmpegSession(originalUri, session.sessionId)
}
if(arguments.videoStabilize > 0.01f) {
// Stabilization was requested: we need an additional first pass to get stabilization data
stabilizationFirstPass()
} else {
// Immediately call the second pass, no stabilization needed
secondPass()
}
}
fun cancelEncoding(){
FFmpegKit.cancel()
}
fun cancelEncoding(sessionId: Long){
FFmpegKit.cancel(sessionId)
}
2022-06-10 23:41:29 +02:00
}
}