diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 89f1821d..4ba866d4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,8 +28,7 @@
android:theme="@style/AppTheme.ActionBar.Transparent"/>
+ android:exported="false"/>
= mutableMapOf()
+ // Keep track of temporary files to delete them (avoids filling cache super fast with videos)
+ private val tempFiles: ArrayList = ArrayList()
+
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPostCreationBinding.inflate(layoutInflater)
setContentView(binding.root)
+
+
+
user = db.userDao().getActiveUser()
instance = user?.run {
@@ -89,7 +108,7 @@ class PostCreationActivity : BaseActivity() {
intent.clipData?.let { addPossibleImages(it) }
val carousel: ImageCarousel = binding.carousel
- carousel.addData(photoData.map { CarouselItem(it.imageUri, video = it.video) })
+ carousel.addData(photoData.map { CarouselItem(it.imageUri, video = it.video, encodeProgress = null) })
carousel.layoutCarouselCallback = {
if(it){
// Became a carousel
@@ -109,7 +128,7 @@ class PostCreationActivity : BaseActivity() {
// get the description and send the post
binding.postCreationSendButton.setOnClickListener {
- if (validateDescription() && photoData.isNotEmpty()) upload()
+ if (validatePost() && photoData.isNotEmpty()) upload()
}
// Button to retry image upload when it fails
@@ -123,7 +142,7 @@ class PostCreationActivity : BaseActivity() {
}
binding.editPhotoButton.setOnClickListener {
- carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
+ carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
edit(currentPosition)
}
}
@@ -133,27 +152,36 @@ class PostCreationActivity : BaseActivity() {
}
binding.savePhotoButton.setOnClickListener {
- carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
+ carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
savePicture(it, currentPosition)
}
}
binding.removePhotoButton.setOnClickListener {
- carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
+ carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
photoData.removeAt(currentPosition)
- carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) })
+ sessionMap[currentPosition]?.let { FFmpegKit.cancel(it) }
+ carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) })
binding.addPhotoButton.isEnabled = true
}
}
}
+ override fun onDestroy() {
+ super.onDestroy()
+ FFmpegKit.cancel()
+ tempFiles.forEach {
+ it.delete()
+ }
+ }
+
/**
* Will add as many images as possible to [photoData], from the [clipData], and if
- * ([photoData].size + [clipData].itemCount) > [albumLimit] then it will only add as many images
+ * ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
*/
- private fun addPossibleImages(clipData: ClipData){
+ private fun addPossibleImages(clipData: ClipData) {
var count = clipData.itemCount
if(count + photoData.size > instance.albumLimit){
AlertDialog.Builder(this).apply {
@@ -168,7 +196,7 @@ class PostCreationActivity : BaseActivity() {
}
for (i in 0 until count) {
clipData.getItemAt(i).uri.let {
- val sizeAndVideoPair: Pair = it.getSizeAndVideoValidate()
+ val sizeAndVideoPair: Pair = it.getSizeAndVideoValidate(photoData.size + 1)
photoData.add(PhotoData(imageUri = it, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second))
}
}
@@ -178,7 +206,7 @@ class PostCreationActivity : BaseActivity() {
* Returns the size of the file of the Uri, and whether it is a video,
* and opens a dialog in case it is too big or in case the file is unsupported.
*/
- private fun Uri.getSizeAndVideoValidate(): Pair {
+ private fun Uri.getSizeAndVideoValidate(editPosition: Int): Pair {
val size: Long =
if (toString().startsWith("content")) {
contentResolver.query(this, null, null, null, null)
@@ -209,7 +237,7 @@ class PostCreationActivity : BaseActivity() {
if (sizeInkBytes > instance.maxPhotoSize || sizeInkBytes > instance.maxVideoSize) {
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
AlertDialog.Builder(this@PostCreationActivity).apply {
- setMessage(getString(R.string.size_exceeds_instance_limit, photoData.size + 1, sizeInkBytes, maxSize))
+ setMessage(getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize))
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
}
@@ -221,7 +249,7 @@ class PostCreationActivity : BaseActivity() {
result.data?.clipData?.let {
addPossibleImages(it)
}
- binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) })
+ binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) })
} else if (result.resultCode != Activity.RESULT_CANCELED) {
Toast.makeText(applicationContext, "Error while adding images", Toast.LENGTH_SHORT).show()
}
@@ -294,7 +322,7 @@ class PostCreationActivity : BaseActivity() {
}
- private fun validateDescription(): Boolean {
+ private fun validatePost(): Boolean {
binding.postTextInputLayout.run {
val content = editText?.length() ?: 0
if (content > counterMaxLength) {
@@ -303,6 +331,13 @@ class PostCreationActivity : BaseActivity() {
return false
}
}
+ if(!photoData.all { it.videoEncodeProgress == null }){
+ AlertDialog.Builder(this).apply {
+ setMessage(R.string.still_encoding)
+ setNegativeButton(android.R.string.ok) { _, _ -> }
+ }.show()
+ return false
+ }
return true
}
@@ -435,20 +470,132 @@ class PostCreationActivity : BaseActivity() {
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0)
photoData.getOrNull(position)?.apply {
- imageUri = result.data!!.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri()
- val (imageSize, imageVideo) = imageUri.getSizeAndVideoValidate()
- size = imageSize
- video = imageVideo
+ if (video) {
+ val muted: Boolean = result.data!!.getBooleanExtra(VideoEditActivity.MUTED, false)
+ val videoStart: Float? = result.data!!.getFloatExtra(VideoEditActivity.VIDEO_START, -1f).let {
+ if(it == -1f) null else it
+ }
+ val modified: Boolean = result.data!!.getBooleanExtra(VideoEditActivity.MODIFIED, false)
+ val videoEnd: Float? = result.data!!.getFloatExtra(VideoEditActivity.VIDEO_END, -1f).let {
+ if(it == -1f) null else it
+ }
+ if(modified){
+ videoEncodeProgress = 0
+ sessionMap[position]?.let { FFmpegKit.cancel(it) }
+ startEncoding(position, muted, videoStart, videoEnd)
+ }
+ } else {
+ imageUri = result.data!!.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri()
+ val (imageSize, imageVideo) = imageUri.getSizeAndVideoValidate(position)
+ size = imageSize
+ video = imageVideo
+ }
progress = null
uploadId = null
} ?: Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
- binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) })
+ binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) })
} else if(result?.resultCode != Activity.RESULT_CANCELED){
Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
}
}
+ /**
+ * @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
+ */
+ private fun startEncoding(position: Int, muted: Boolean, videoStart: Float?, videoEnd: Float?) {
+ val originalUri = photoData[position].imageUri
+
+ // Having a meaningful suffix is necessary so that ffmpeg knows what to put in output
+ val suffix = if(originalUri.scheme == "content") {
+ contentResolver.getType(photoData[position].imageUri)?.takeLastWhile { it != '/' }
+ } else {
+ originalUri.toString().takeLastWhile { it != '/' }
+ }
+ val file = File.createTempFile("temp_video", ".$suffix")
+ //val file = File.createTempFile("temp_video", ".webm")
+ tempFiles.add(file)
+ val fileUri = file.toUri()
+ val outputVideoPath = ffmpegSafeUri(fileUri)
+
+ val inputUri = photoData[position].imageUri
+
+ val inputSafePath = ffmpegSafeUri(inputUri)
+
+ val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(ffmpegSafeUri(inputUri)).mediaInformation
+ val totalVideoDuration = mediaInformation?.duration?.toFloatOrNull()
+
+ val mutedString = if(muted) "-an" else ""
+ val startString = if(videoStart != null) "-ss $videoStart" else ""
+
+ val endString = if(videoEnd != null) "-to ${videoEnd - (videoStart ?: 0f)}" else ""
+
+ val session: FFmpegSession = FFmpegKit.executeAsync("$startString -i $inputSafePath $endString -c copy $mutedString -y $outputVideoPath",
+ //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)) {
+ fun successResult() {
+ // Hide progress indicator in carousel
+ binding.carousel.updateProgress(null, position, false)
+ val (imageSize, imageVideo) = outputVideoPath.toUri().let {
+ photoData[position].imageUri = it
+ it.getSizeAndVideoValidate(position)
+ }
+ photoData[position].videoEncodeProgress = null
+ photoData[position].size = imageSize
+ binding.carousel.addData(photoData.map {
+ CarouselItem(it.imageUri,
+ it.imageDescription,
+ it.video,
+ it.videoEncodeProgress)
+ })
+ }
+
+ val post = resultHandler.post {
+ successResult()
+ }
+ if(!post) {
+ Log.e(TAG, "Failed to post changes, trying to recover in 100ms")
+ resultHandler.postDelayed({successResult()}, 100)
+ }
+ Log.d(TAG, "Encode completed successfully in ${session.duration} milliseconds")
+ } else {
+ resultHandler.post {
+ binding.carousel.updateProgress(null, position, error = true)
+ photoData[position].videoEncodeProgress = null
+ }
+ 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 newTotalDuration = it - (videoStart ?: 0f) - (it - (videoEnd ?: it))
+ timeInMilliseconds / (10*newTotalDuration)
+ }
+ resultHandler.post {
+ completePercentage?.let {
+ val rounded = it.roundToInt()
+ photoData[position].videoEncodeProgress = rounded
+ binding.carousel.updateProgress(rounded, position, false)
+ }
+ }
+ Log.d(TAG, "Encoding video: %$completePercentage.")
+ }
+ }
+ }
+ sessionMap[position] = session.sessionId
+ }
+
private fun edit(position: Int) {
val intent = Intent(
this,
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
new file mode 100644
index 00000000..8b7f1d1b
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
@@ -0,0 +1,30 @@
+package org.pixeldroid.app.postCreation
+
+import android.content.ClipData
+import android.os.Bundle
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+
+class PostCreationViewModel : ViewModel() {
+ private val photoData: MutableLiveData> by lazy {
+ MutableLiveData>().also {
+ loadUsers()
+ }
+ }
+
+ fun getUsers(): LiveData> {
+ return photoData
+ }
+
+ private fun loadUsers() {
+ // Do an asynchronous operation to fetch users.
+ }
+}
+class PostCreationViewModelFactory(val bundle: ClipData? = null) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return modelClass.getConstructor(ClipData::class.java).newInstance(bundle)
+ }
+
+}
\ No newline at end of file
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 37dfda86..bbd4b8c7 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
@@ -5,5 +5,6 @@ import android.net.Uri
data class CarouselItem constructor(
val imageUrl: Uri,
val caption: String? = null,
- val video: Boolean
+ val video: Boolean,
+ var encodeProgress: Int?
)
\ 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 5b628b3f..bb2473aa 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
@@ -69,7 +69,7 @@ class ImageCarousel(
private var isBuiltInIndicator = false
- private var data: List? = null
+ private var data: MutableList? = null
var onItemClickListener: OnItemClickListener? = this
set(value) {
@@ -88,28 +88,34 @@ class ImageCarousel(
/**
* Get or set current item position
*/
- var currentPosition = -1
+ var currentPosition = RecyclerView.NO_POSITION
get() {
return snapHelper.getSnapPosition(recyclerView.layoutManager)
}
set(value) {
- val position = when {
- value >= data?.size ?: 0 -> {
- -1
- }
- value < 0 -> {
- -1
- }
- else -> {
- value
- }
+ val position = when (value) {
+ !in 0..((data?.size?.minus(1)) ?: 0) -> RecyclerView.NO_POSITION
+ else -> value
}
- field = position
+ if (position != RecyclerView.NO_POSITION && field != position) {
+ val thisProgress = data?.get(position)?.encodeProgress
+ if (thisProgress != null) {
+ binding.encodeProgress.visibility = VISIBLE
+ binding.encodeInfoText.visibility = VISIBLE
+ binding.encodeInfoText.text =
+ context.getString(R.string.encode_progress).format(thisProgress)
+ binding.encodeProgress.progress = thisProgress
+ } else {
+ binding.encodeProgress.visibility = INVISIBLE
+ binding.encodeInfoText.visibility = INVISIBLE
+ }
+ } else binding.encodeProgress.visibility = INVISIBLE
- if (position != -1) {
+ if (position != RecyclerView.NO_POSITION && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
recyclerView.smoothScrollToPosition(position)
}
+ field = position
}
/**
@@ -450,10 +456,9 @@ class ImageCarousel(
private fun initListeners() {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ val position = currentPosition
if (showCaption) {
- val position = snapHelper.getSnapPosition(recyclerView.layoutManager)
-
if (position >= 0) {
val dataItem = adapter?.getItem(position)
@@ -469,6 +474,8 @@ class ImageCarousel(
}
}
+ if(dx !=0 || dy != 0) currentPosition = position
+
onScrollListener?.onScrolled(recyclerView, dx, dy)
}
@@ -561,12 +568,37 @@ class ImageCarousel(
adapter?.apply {
addAll(data)
- this@ImageCarousel.data = data
+ this@ImageCarousel.data = data.toMutableList()
initOnScrollStateChange()
}
}
+ fun updateProgress(progress: Int?, position: Int, error: Boolean){
+ data?.get(position)?.encodeProgress = progress
+ if(currentPosition == position) {
+ if (progress == null) {
+ binding.encodeProgress.visibility = INVISIBLE
+ binding.encodeInfoText.visibility = VISIBLE
+ if(error){
+ binding.encodeInfoText.setText(R.string.encode_error)
+ binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error),
+ null, null, null)
+
+ } else {
+ binding.encodeInfoText.setText(R.string.encode_success)
+ binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.check_circle_24),
+ null, null, null)
+ }
+ } else {
+ binding.encodeProgress.visibility = VISIBLE
+ binding.encodeProgress.progress = progress
+ binding.encodeInfoText.visibility = VISIBLE
+ binding.encodeInfoText.text = context.getString(R.string.encode_progress).format(progress)
+ }
+ }
+ }
+
/**
* Goto previous item.
*/
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt
index d32789f9..04566a8b 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt
@@ -148,7 +148,7 @@ class PhotoEditActivity : BaseActivity() {
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.edit_photo_menu, menu)
+ menuInflater.inflate(R.menu.edit_menu, menu)
return true
}
@@ -191,8 +191,8 @@ class PhotoEditActivity : BaseActivity() {
}
}
- return super.onOptionsItemSelected(item)
-}
+ return super.onOptionsItemSelected(item)
+ }
fun onFilterSelected(filter: Filter) {
filteredImage = compressedOriginalImage!!.copy(BITMAP_CONFIG, true)
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 53c3240d..11d0526a 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
@@ -1,57 +1,279 @@
package org.pixeldroid.app.postCreation.photoEdit
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.Intent
+import android.media.AudioManager
import android.net.Uri
import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.text.format.DateUtils
import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.SeekBar
+import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.core.net.toUri
+import androidx.core.os.HandlerCompat
+import androidx.media.AudioAttributesCompat
+import androidx.media2.common.MediaMetadata
+import androidx.media2.common.UriMediaItem
+import androidx.media2.player.MediaPlayer
import com.arthenica.ffmpegkit.*
-import com.arthenica.ffmpegkit.MediaInformation.KEY_DURATION
import com.bumptech.glide.Glide
+import com.google.android.material.slider.RangeSlider
+import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityVideoEditBinding
+import org.pixeldroid.app.postCreation.PostCreationActivity
+import org.pixeldroid.app.postCreation.carousel.dpToPx
import org.pixeldroid.app.utils.BaseActivity
+import org.pixeldroid.app.utils.ffmpegSafeUri
import java.io.File
+import java.text.NumberFormat
+import java.time.format.DateTimeFormatter
+import java.util.*
+import kotlin.collections.ArrayList
class VideoEditActivity : BaseActivity() {
+
+ private lateinit var mediaPlayer: MediaPlayer
+ private var videoPosition: Int = -1
+ private lateinit var binding: ActivityVideoEditBinding
+ // Map photoData indexes to FFmpeg Session IDs
+ private val sessionList: ArrayList = arrayListOf()
+ private val tempFiles: ArrayList = ArrayList()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val binding = ActivityVideoEditBinding.inflate(layoutInflater)
-
+ binding = ActivityVideoEditBinding.inflate(layoutInflater)
setContentView(binding.root)
- val uri = intent.getParcelableExtra(PhotoEditActivity.PICTURE_URI) as Uri?
- val videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
- val inputVideoPath =if(uri.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForRead(this, uri) else uri.toString()
- val inputVideoPath2 =if(uri.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForRead(this, uri) else uri.toString()
+ 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(PhotoEditActivity.PICTURE_URI)!!
+ videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1)
+
+ val inputVideoPath = ffmpegSafeUri(uri)
val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation
- val duration: Long? = mediaInformation?.getNumberProperty(KEY_DURATION)
-
- val file = File.createTempFile("temp_img", ".png").toUri()
-
- val outputImagePath =if(file.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForWrite(this, file) else file.toString()
-
- val session = FFmpegKit.execute(
- "-i $inputVideoPath2 -filter_complex \"select='not(mod(n,1000))',scale=240:-1,tile=layout=4x1\" -vframes 1 -q:v 2 -y $outputImagePath"
- )
- if (ReturnCode.isSuccess(session.returnCode)) {
- Glide.with(this).load(file).into(binding.thumbnails)
- // SUCCESS
- } else if (ReturnCode.isCancel(session.returnCode)) {
-
- // CANCEL
- } else {
-
- // FAILURE
- Log.d("VideoEditActivity",
- String.format("Command failed with state %s and rc %s.%s",
- session.state,
- session.returnCode,
- session.failStackTrace))
+ binding.muter.setOnClickListener {
+ binding.muter.isSelected = !binding.muter.isSelected
}
+ //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)
+
+
+ val mediaItem: UriMediaItem = UriMediaItem.Builder(uri).build()
+ mediaItem.metadata = MediaMetadata.Builder()
+ .putString(MediaMetadata.METADATA_KEY_TITLE, "")
+ .build()
+
+ 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()
+ )
+
+ findViewById(R.id.progress_bar)?.visibility = View.GONE
+
+ mediaPlayer.prepare()
+
+ binding.muter.setOnClickListener {
+ if(!binding.muter.isSelected) mediaPlayer.playerVolume = 0f
+ else mediaPlayer.playerVolume = 1f
+ binding.muter.isSelected = !binding.muter.isSelected
+ }
+
+ 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())
+ }
+
+
+ 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
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+
+ when(item.itemId) {
+ android.R.id.home -> onBackPressed()
+ R.id.action_save -> {
+ returnWithValues()
+ }
+ R.id.action_reset -> {
+ resetControls()
+ }
+ }
+
+ return super.onOptionsItemSelected(item)
+ }
+
+ override fun onBackPressed() {
+ if (noEdits()) super.onBackPressed()
+ 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
+ return !muted && videoPositions
+ }
+
+
+ private fun returnWithValues() {
+ val intent = Intent(this, PostCreationActivity::class.java)
+ .apply {
+ putExtra(PhotoEditActivity.PICTURE_POSITION, videoPosition)
+ putExtra(MUTED, binding.muter.isSelected)
+ putExtra(MODIFIED, !noEdits())
+ putExtra(VIDEO_START, binding.videoRangeSeekBar.values.first())
+ putExtra(VIDEO_END, binding.videoRangeSeekBar.values[2])
+ 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
+ }
+
+ 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,
+ ) {
+ val file = File.createTempFile("temp_img", ".bmp")
+ tempFiles.add(file)
+ val fileUri = file.toUri()
+ val inputSafePath = ffmpegSafeUri(inputUri)
+
+ val outputImagePath =
+ if(fileUri.toString().startsWith("content://"))
+ FFmpegKitConfig.getSafParameterForWrite(this, fileUri)
+ else fileUri.toString()
+ val session = FFmpegKit.executeAsync(
+ "-noaccurate_seek -ss $thumbTime -i $inputSafePath -vf scale=${thumbnail.width}:${thumbnail.height} -frames:v 1 -f image2 -y $outputImagePath",
+ { session ->
+ 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}")
+ },
+ {/* CALLED WHEN SESSION PRINTS LOGS */ }) { /*CALLED WHEN SESSION GENERATES STATISTICS*/ }
+ sessionList.add(session.sessionId)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mediaPlayer.pause()
+ }
+
companion object {
const val VIDEO_TAG = "VideoEditTag"
+ const val MUTED = "VideoEditMutedTag"
+ const val VIDEO_START = "VideoEditVideoStartTag"
+ const val VIDEO_END = "VideoEditVideoEndTag"
+ const val MODIFIED = "VideoEditModifiedTag"
}
}
\ No newline at end of file
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 c724c15b..b2ef5187 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt
@@ -19,6 +19,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.arthenica.ffmpegkit.FFmpegKitConfig
import okhttp3.HttpUrl
import org.pixeldroid.app.R
import kotlin.properties.ReadWriteProperty
@@ -65,6 +66,12 @@ fun normalizeDomain(domain: String): String {
.trim(Char::isWhitespace)
}
+fun Context.ffmpegSafeUri(inputUri: Uri?): String =
+ if (inputUri?.scheme == "content")
+ FFmpegKitConfig.getSafParameterForRead(this, inputUri)
+ else inputUri.toString()
+
+
fun bitmapFromUri(contentResolver: ContentResolver, uri: Uri?): Bitmap =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder
@@ -107,7 +114,7 @@ fun Bitmap.flip(horizontal: Boolean, vertical: Boolean): Bitmap {
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
}
-fun BaseActivity.openUrl(url: String): Boolean{
+fun BaseActivity.openUrl(url: String): Boolean {
val intent = CustomTabsIntent.Builder().build()
diff --git a/app/src/main/res/drawable/double_circle.xml b/app/src/main/res/drawable/double_circle.xml
new file mode 100644
index 00000000..907d1235
--- /dev/null
+++ b/app/src/main/res/drawable/double_circle.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/error.xml b/app/src/main/res/drawable/error.xml
new file mode 100644
index 00000000..17575711
--- /dev/null
+++ b/app/src/main/res/drawable/error.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/selector_mute.xml b/app/src/main/res/drawable/selector_mute.xml
new file mode 100644
index 00000000..7103cc0c
--- /dev/null
+++ b/app/src/main/res/drawable/selector_mute.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/thumb_left.xml b/app/src/main/res/drawable/thumb_left.xml
new file mode 100644
index 00000000..d6c10419
--- /dev/null
+++ b/app/src/main/res/drawable/thumb_left.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/thumb_right.xml b/app/src/main/res/drawable/thumb_right.xml
new file mode 100644
index 00000000..6b5f6222
--- /dev/null
+++ b/app/src/main/res/drawable/thumb_right.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/volume_off.xml b/app/src/main/res/drawable/volume_off.xml
new file mode 100644
index 00000000..b71ede34
--- /dev/null
+++ b/app/src/main/res/drawable/volume_off.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/volume_up.xml b/app/src/main/res/drawable/volume_up.xml
new file mode 100644
index 00000000..836cad86
--- /dev/null
+++ b/app/src/main/res/drawable/volume_up.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_video_edit.xml b/app/src/main/res/layout/activity_video_edit.xml
index d7c9300f..fe3feb98 100644
--- a/app/src/main/res/layout/activity_video_edit.xml
+++ b/app/src/main/res/layout/activity_video_edit.xml
@@ -1,15 +1,112 @@
+ android:background="@android:color/black"
+ android:scrollbarThumbHorizontal="@drawable/thumb_left">
+
+
+
+
+
+
+
+
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toLeftOf="@+id/thumbnail2" />
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/image_carousel.xml b/app/src/main/res/layout/image_carousel.xml
index 3bbd86fe..dee9a908 100644
--- a/app/src/main/res/layout/image_carousel.xml
+++ b/app/src/main/res/layout/image_carousel.xml
@@ -162,4 +162,30 @@
app:layout_constraintTop_toTopOf="@+id/indicator"
tools:visibility="visible" />
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/edit_photo_menu.xml b/app/src/main/res/menu/edit_menu.xml
similarity index 83%
rename from app/src/main/res/menu/edit_photo_menu.xml
rename to app/src/main/res/menu/edit_menu.xml
index 31194bd7..4730db3b 100644
--- a/app/src/main/res/menu/edit_photo_menu.xml
+++ b/app/src/main/res/menu/edit_menu.xml
@@ -7,13 +7,13 @@
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0e756cb5..7a9f17e5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -263,4 +263,11 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"
Play video
Video editing is not yet supported
Reel showing thumbnails of the video you are editing
+ RESET
+ SAVE
+ Error encoding
+ Encode success!
+ Encode %1$d%%
+ Select what to keep of the video
+ One or more videos are still encoding. Wait for them to finish before uploading
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 02b00d99..8e59e764 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -4,4 +4,4 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
-distributionSha256Sum=f581709a9c35e9cb92e16f585d2c4bc99b2b1a5f85d2badbd3dc6bff59e1e6dd
+distributionSha256Sum=b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302