PixelDroid-App-Android/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt

463 lines
18 KiB
Kotlin
Raw Normal View History

2022-06-10 23:41:29 +02:00
package org.pixeldroid.app.postCreation.photoEdit
2022-06-18 22:21:19 +02:00
import android.app.Activity
import android.app.AlertDialog
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-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
import com.arthenica.ffmpegkit.FFprobeKit
import com.arthenica.ffmpegkit.MediaInformation
import com.arthenica.ffmpegkit.ReturnCode
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-06-18 22:21:19 +02:00
import org.pixeldroid.app.R
2022-06-10 23:41:29 +02:00
import org.pixeldroid.app.databinding.ActivityVideoEditBinding
2022-06-18 22:21:19 +02:00
import org.pixeldroid.app.postCreation.PostCreationActivity
import org.pixeldroid.app.postCreation.carousel.dpToPx
2022-07-10 13:42:19 +02:00
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
2022-08-21 00:31:36 +02:00
import org.pixeldroid.app.utils.ffmpegCompliantUri
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-06-10 23:41:29 +02:00
2022-07-10 13:42:19 +02:00
class VideoEditActivity : BaseThemedWithBarActivity() {
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-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
.resolveAttribute(R.attr.colorOnPrimaryContainer, typedValue, true)
) 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())
val uri = intent.getParcelableExtra<Uri>(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-08-21 00:31:36 +02:00
val inputVideoPath = ffmpegCompliantUri(uri)
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-06-18 22:21:19 +02:00
val mediaItem: UriMediaItem = UriMediaItem.Builder(uri).build()
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 {
showCropInterface(show = true, uri = uri)
}
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
.resolveAttribute(R.attr.colorOnPrimaryContainer, typedValue, true)
) 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 {
thumbnail(uri, resultHandler, binding.thumbnail1, it)
thumbnail(uri, resultHandler, binding.thumbnail2, it.times(2))
thumbnail(uri, resultHandler, binding.thumbnail3, it.times(3))
thumbnail(uri, resultHandler, binding.thumbnail4, it.times(4))
thumbnail(uri, resultHandler, binding.thumbnail5, it.times(5))
thumbnail(uri, resultHandler, binding.thumbnail6, it.times(6))
thumbnail(uri, resultHandler, binding.thumbnail7, it.times(7))
}
}
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-19 00:19:42 +02:00
return !muted && videoPositions && speedUnchanged && cropRelativeDimensions.notCropped()
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-06-18 22:21:19 +02:00
val intent = Intent(this, PostCreationActivity::class.java)
.apply {
putExtra(PhotoEditActivity.PICTURE_POSITION, videoPosition)
putExtra(MUTED, binding.muter.isSelected)
2022-10-15 20:13:29 +02:00
putExtra(SPEED, speed)
2022-06-18 22:21:19 +02:00
putExtra(MODIFIED, !noEdits())
putExtra(VIDEO_START, binding.videoRangeSeekBar.values.first())
putExtra(VIDEO_END, binding.videoRangeSeekBar.values[2])
2022-10-19 00:19:42 +02:00
putExtra(VIDEO_CROP, cropRelativeDimensions)
2022-10-22 22:07:03 +02:00
putExtra(VIDEO_STABILIZE, stabilization)
2022-06-18 22:21:19 +02:00
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 {
const val VIDEO_TAG = "VideoEditTag"
2022-06-18 22:21:19 +02:00
const val MUTED = "VideoEditMutedTag"
2022-10-15 20:13:29 +02:00
const val SPEED = "VideoEditSpeedTag"
// 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 VIDEO_START = "VideoEditVideoStartTag"
const val VIDEO_END = "VideoEditVideoEndTag"
2022-10-19 00:19:42 +02:00
const val VIDEO_CROP = "VideoEditVideoCropTag"
2022-10-22 22:07:03 +02:00
const val VIDEO_STABILIZE = "VideoEditVideoStabilizeTag"
2022-06-18 22:21:19 +02:00
const val MODIFIED = "VideoEditModifiedTag"
2022-06-10 23:41:29 +02:00
}
}