diff --git a/app/build.gradle b/app/build.gradle index 59a90105..b2715169 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ import com.android.build.api.dsl.ManagedVirtualDevice plugins { - id "com.cookpad.android.plugin.license-tools" version "1.2.2" + id "com.cookpad.android.plugin.license-tools" version "1.2.8" } apply plugin: 'com.android.application' 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 551ee41b..219d4fab 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt @@ -47,7 +47,7 @@ import java.io.OutputStream import java.text.SimpleDateFormat import java.util.* import kotlin.math.roundToInt - +import kotlin.collections.flatten const val TAG = "Post Creation Activity" @@ -130,8 +130,10 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { uiState.newEncodingJobMuted?.let { muted -> uiState.newEncodingJobVideoStart?.let { videoStart -> uiState.newEncodingJobVideoEnd?.let { videoEnd -> - startEncoding(position, muted, videoStart, videoEnd) - model.encodingStarted() + uiState.newEncodingJobSpeedIndex?.let { speedIndex -> + startEncoding(position, muted, videoStart, videoEnd, speedIndex) + model.encodingStarted() + } } } } @@ -324,7 +326,13 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { * @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?) { + private fun startEncoding( + position: Int, + muted: Boolean, + videoStart: Float?, + videoEnd: Float?, + speedIndex: Int + ) { val originalUri = model.getPhotoData().value!![position].imageUri // Having a meaningful suffix is necessary so that ffmpeg knows what to put in output @@ -342,13 +350,32 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(ffmpegCompliantUri(inputUri)).mediaInformation val totalVideoDuration = mediaInformation?.duration?.toFloatOrNull() - val mutedString = if(muted) "-an" else null - val startString: List = if(videoStart != null) listOf("-ss", "$videoStart") else listOf(null, null) + val speed = VideoEditActivity.speedChoices[speedIndex] - val endString: List = if(videoEnd != null) listOf("-to", "${videoEnd - (videoStart ?: 0f)}") else listOf(null, null) + //TODO also have audio when speed is changed? + val mutedString = if(muted || speedIndex != 1) "-an" else null + val startString: List = if(videoStart != null) listOf("-ss", "${videoStart/speed.toFloat()}") else listOf(null, null) + + val endString: List = if(videoEnd != null) listOf("-to", "${videoEnd/speed.toFloat() - (videoStart ?: 0f)/speed.toFloat()}") else listOf(null, null) + + val speedString: List = if(speedIndex!= 1) + listOf("-filter:v", "setpts=PTS/${speed}") + // 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 = if(speedIndex != 1) listOf("-c:v", "libx264", "-preset", "ultrafast") else listOf(null, null, null, null) val session: FFmpegSession = - FFmpegKit.executeWithArgumentsAsync(listOfNotNull(startString[0], startString[1], "-i", ffmpegCompliantUri, endString[0], endString[1], "-c", "copy", mutedString, "-y", outputVideoPath).toTypedArray(), + FFmpegKit.executeWithArgumentsAsync(listOfNotNull( + startString[0], startString[1], + "-i", ffmpegCompliantUri, + speedString[0], speedString[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 @@ -387,7 +414,9 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() { timeInMilliseconds?.let { if (timeInMilliseconds > 0) { val completePercentage = totalVideoDuration?.let { - val newTotalDuration = it - (videoStart ?: 0f) - (it - (videoEnd ?: it)) + val speedupDurationModifier = VideoEditActivity.speedChoices[speedIndex].toFloat() + + val newTotalDuration = (it - (videoStart ?: 0f) - (it - (videoEnd ?: it)))/speedupDurationModifier timeInMilliseconds / (10*newTotalDuration) } resultHandler.post { 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 219f1256..75e9b35a 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -58,6 +58,7 @@ data class PostCreationActivityUiState( val newEncodingJobPosition: Int? = null, val newEncodingJobMuted: Boolean? = null, + val newEncodingJobSpeedIndex: Int? = null, val newEncodingJobVideoStart: Float? = null, val newEncodingJobVideoEnd: Float? = null, ) @@ -96,6 +97,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null currentUiState.copy( newEncodingJobPosition = null, newEncodingJobMuted = null, + newEncodingJobSpeedIndex = null, newEncodingJobVideoStart = null, newEncodingJobVideoEnd = null, ) @@ -332,7 +334,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null Toast.LENGTH_SHORT).show() val intent = Intent(getApplication(), MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - //TODO make the activity launch this instead (and surrounding toasts too)L + //TODO make the activity launch this instead (and surrounding toasts too) getApplication().startActivity(intent) } catch (exception: IOException) { Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_error), @@ -359,21 +361,24 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null fun modifyAt(position: Int, data: Intent): Unit? { val result: PhotoData = photoData.value?.getOrNull(position)?.run { if (video) { - val muted: Boolean = data.getBooleanExtra(VideoEditActivity.MUTED, false) - val videoStart: Float? = data.getFloatExtra(VideoEditActivity.VIDEO_START, -1f).let { - if(it == -1f) null else it - } val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false) - val videoEnd: Float? = data.getFloatExtra(VideoEditActivity.VIDEO_END, -1f).let { - if(it == -1f) null else it - } if(modified){ + val muted: Boolean = data.getBooleanExtra(VideoEditActivity.MUTED, false) + val speedIndex: Int = data.getIntExtra(VideoEditActivity.SPEED, 1) + val videoStart: Float? = data.getFloatExtra(VideoEditActivity.VIDEO_START, -1f).let { + if(it == -1f) null else it + } + val videoEnd: Float? = data.getFloatExtra(VideoEditActivity.VIDEO_END, -1f).let { + if(it == -1f) null else it + } + videoEncodeProgress = 0 sessionMap[position]?.let { FFmpegKit.cancel(it) } _uiState.update { currentUiState -> currentUiState.copy( newEncodingJobPosition = position, newEncodingJobMuted = muted, + newEncodingJobSpeedIndex = speedIndex, newEncodingJobVideoStart = videoStart, newEncodingJobVideoEnd = videoEnd ) 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 19b573a1..02562c87 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 @@ -93,16 +93,15 @@ class ImageCarousel( if (position != RecyclerView.NO_POSITION && field != position) { val thisProgress = data?.getOrNull(position)?.encodeProgress if (thisProgress != null) { + binding.encodeInfoCard.visibility = VISIBLE 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 + binding.encodeInfoCard.visibility = GONE } - } else binding.encodeProgress.visibility = INVISIBLE + } else if(position == RecyclerView.NO_POSITION) binding.encodeInfoCard.visibility = GONE if (position != RecyclerView.NO_POSITION && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { recyclerView.smoothScrollToPosition(position) @@ -567,8 +566,7 @@ class ImageCarousel( data?.getOrNull(position)?.encodeProgress = progress if(currentPosition == position) { if (progress == null) { - binding.encodeProgress.visibility = INVISIBLE - binding.encodeInfoText.visibility = VISIBLE + binding.encodeProgress.visibility = GONE if(error){ binding.encodeInfoText.setText(R.string.encode_error) binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error), @@ -581,8 +579,8 @@ class ImageCarousel( } } else { binding.encodeProgress.visibility = VISIBLE + binding.encodeInfoCard.visibility = VISIBLE binding.encodeProgress.progress = progress - binding.encodeInfoText.visibility = VISIBLE binding.encodeInfoText.text = 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 3981b82f..ec58f38d 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 @@ -5,6 +5,7 @@ import android.app.AlertDialog import android.content.Intent import android.media.AudioManager import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper @@ -21,6 +22,7 @@ import androidx.media.AudioAttributesCompat import androidx.media2.common.MediaMetadata import androidx.media2.common.UriMediaItem import androidx.media2.player.MediaPlayer +import androidx.media2.player.MediaPlayer.PlayerCallback import com.arthenica.ffmpegkit.* import com.bumptech.glide.Glide import com.google.android.material.slider.RangeSlider @@ -37,6 +39,17 @@ class VideoEditActivity : BaseThemedWithBarActivity() { private lateinit var mediaPlayer: MediaPlayer private var videoPosition: Int = -1 + + //TODO react to change of playbackSpeed (when changed in the player itself) + private var speed: Int = 1 + set(value) { + field = value + + mediaPlayer.playbackSpeed = speedChoices[value].toFloat() + + if(speed != 1) binding.muter.callOnClick() + } + private lateinit var binding: ActivityVideoEditBinding // Map photoData indexes to FFmpeg Session IDs private val sessionList: ArrayList = arrayListOf() @@ -98,9 +111,13 @@ class VideoEditActivity : BaseThemedWithBarActivity() { mediaPlayer.prepare() + binding.muter.setOnClickListener { if(!binding.muter.isSelected) mediaPlayer.playerVolume = 0f - else mediaPlayer.playerVolume = 1f + else { + mediaPlayer.playerVolume = 1f + speed = 1 + } binding.muter.isSelected = !binding.muter.isSelected } @@ -130,6 +147,24 @@ class VideoEditActivity : BaseThemedWithBarActivity() { } + + 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() + } + + val thumbInterval: Float? = duration?.div(7) thumbInterval?.let { @@ -188,7 +223,9 @@ class VideoEditActivity : BaseThemedWithBarActivity() { it[0] == 0f && it[2] == binding.videoRangeSeekBar.valueTo } val muted = binding.muter.isSelected - return !muted && videoPositions + val speedUnchanged = speed == 1 + + return !muted && videoPositions && speedUnchanged } @@ -197,6 +234,7 @@ class VideoEditActivity : BaseThemedWithBarActivity() { .apply { putExtra(PhotoEditActivity.PICTURE_POSITION, videoPosition) putExtra(MUTED, binding.muter.isSelected) + putExtra(SPEED, speed) putExtra(MODIFIED, !noEdits()) putExtra(VIDEO_START, binding.videoRangeSeekBar.values.first()) putExtra(VIDEO_END, binding.videoRangeSeekBar.values[2]) @@ -267,6 +305,9 @@ class VideoEditActivity : BaseThemedWithBarActivity() { companion object { const val VIDEO_TAG = "VideoEditTag" const val MUTED = "VideoEditMutedTag" + const val SPEED = "VideoEditSpeedTag" + // List of choices of speeds + val speedChoices: List = listOf(0.5, 1, 2, 4, 8) const val VIDEO_START = "VideoEditVideoStartTag" const val VIDEO_END = "VideoEditVideoEndTag" const val MODIFIED = "VideoEditModifiedTag" diff --git a/app/src/main/res/drawable/speed.xml b/app/src/main/res/drawable/speed.xml new file mode 100644 index 00000000..1add8a09 --- /dev/null +++ b/app/src/main/res/drawable/speed.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 fe3feb98..1e1c7b13 100644 --- a/app/src/main/res/layout/activity_video_edit.xml +++ b/app/src/main/res/layout/activity_video_edit.xml @@ -25,13 +25,27 @@ android:layout_height="40dp" android:layout_marginStart="16dp" android:layout_marginBottom="48dp" - android:contentDescription="@string/add_comment" + android:contentDescription="@string/mute_video" android:background="?attr/selectableItemBackgroundBorderless" android:padding="4dp" android:src="@drawable/selector_mute" app:layout_constraintBottom_toTopOf="@+id/thumbnail1" app:layout_constraintStart_toStartOf="parent" /> + + + - + android:id="@+id/encodeInfoCard" + android:layout_marginEnd="24dp"> - + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 364d37bc..91691ac2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -269,6 +269,8 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Encode success! Encode %1$d%% Select what to keep of the video + Mute video + Change video speed One or more videos are still encoding. Wait for them to finish before uploading Create new post New post diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2dd2aa88..392c7177 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -4257,6 +4257,11 @@ + + + + + @@ -5338,6 +5343,14 @@ + + + + + + + + @@ -5346,6 +5359,14 @@ + + + + + + + + @@ -5394,6 +5415,14 @@ + + + + + + + + @@ -5578,6 +5607,14 @@ + + + + + + + + @@ -7555,6 +7592,14 @@ + + + + + + + + @@ -7621,6 +7666,14 @@ + + + + + + + + @@ -8302,6 +8355,14 @@ + + + + + + + +