|
|
|
@ -6,9 +6,7 @@ import android.content.ContentValues
|
|
|
|
|
import android.content.Intent
|
|
|
|
|
import android.content.pm.PackageManager
|
|
|
|
|
import android.net.Uri
|
|
|
|
|
import android.os.AsyncTask
|
|
|
|
|
import android.os.Build
|
|
|
|
|
import android.os.Bundle
|
|
|
|
|
import android.os.*
|
|
|
|
|
import android.provider.MediaStore
|
|
|
|
|
import android.util.Log
|
|
|
|
|
import android.view.Menu
|
|
|
|
@ -20,6 +18,7 @@ import android.widget.Toast
|
|
|
|
|
import android.widget.VideoView
|
|
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
|
|
|
import androidx.core.content.FileProvider
|
|
|
|
|
import androidx.core.net.toUri
|
|
|
|
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
|
|
|
|
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
|
|
|
|
|
import com.arthenica.mobileffmpeg.FFmpeg
|
|
|
|
@ -44,13 +43,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
|
|
|
|
|
|
|
ensureWritePermission()
|
|
|
|
|
|
|
|
|
|
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
|
|
|
|
|
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener {
|
|
|
|
|
if (hasWritePermission) {
|
|
|
|
|
openCamera()
|
|
|
|
|
} else {
|
|
|
|
|
ensureWritePermission()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (intent.action == Intent.ACTION_SEND) {
|
|
|
|
|
intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM)?.also {
|
|
|
|
|
processVideoUri(it as Uri)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
|
|
@ -59,6 +64,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onSaveInstanceState(outState: Bundle) {
|
|
|
|
|
outState.putString("cvp", currentVideoPath)
|
|
|
|
|
outState.putParcelable("cvi", currentVideoUri)
|
|
|
|
|
super.onSaveInstanceState(outState)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
|
|
|
|
currentVideoPath = savedInstanceState.getString("cvp")
|
|
|
|
|
currentVideoUri = savedInstanceState.getParcelable("cvi")
|
|
|
|
|
super.onRestoreInstanceState(savedInstanceState)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
|
|
|
// Handle action bar item clicks here. The action bar will
|
|
|
|
|
// automatically handle clicks on the Home/Up button, so long
|
|
|
|
@ -70,6 +87,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
|
|
R.id.action_save -> {
|
|
|
|
|
save(); true
|
|
|
|
|
}
|
|
|
|
|
R.id.action_clear_cache -> {
|
|
|
|
|
clearVideoCache(); true
|
|
|
|
|
}
|
|
|
|
|
else -> super.onOptionsItemSelected(item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -79,7 +99,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
|
|
REQ_TAKE_VID -> {
|
|
|
|
|
if (resultCode != Activity.RESULT_OK)
|
|
|
|
|
return
|
|
|
|
|
data?.data?.let { processVideo(it) }
|
|
|
|
|
data?.data?.let {
|
|
|
|
|
processVideoUri(it)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else -> super.onActivityResult(requestCode, resultCode, data)
|
|
|
|
|
}
|
|
|
|
@ -130,27 +152,33 @@ class MainActivity : AppCompatActivity() {
|
|
|
|
|
startActivityForResult(intent, REQ_TAKE_VID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun processVideo(videoUri: Uri) {
|
|
|
|
|
currentVideoPath = null
|
|
|
|
|
currentVideoUri = null
|
|
|
|
|
|
|
|
|
|
private fun processVideoUri(inputVideoUri: Uri) {
|
|
|
|
|
// Save captured video for FFMPEG. Use MP4 extension, completely arbitrary.
|
|
|
|
|
val inputFile = File.createTempFile("input", ".mp4", cacheDir)
|
|
|
|
|
contentResolver.openInputStream(videoUri).use { inputStream ->
|
|
|
|
|
contentResolver.openInputStream(inputVideoUri).use { inputStream ->
|
|
|
|
|
if (inputStream == null)
|
|
|
|
|
return Unit .also { Log.e(TAG, "Could not open input file") }
|
|
|
|
|
FileOutputStream(inputFile).use { fos ->
|
|
|
|
|
fos.buffered().write(inputStream.buffered().readBytes())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
processVideo(inputFile.canonicalPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val mediaInfo = FFprobe.getMediaInformation(inputFile.canonicalPath)
|
|
|
|
|
private fun processVideo(inputVideoPath: String) {
|
|
|
|
|
currentVideoPath = null
|
|
|
|
|
currentVideoUri = null
|
|
|
|
|
|
|
|
|
|
val mediaInfo = FFprobe.getMediaInformation(inputVideoPath)
|
|
|
|
|
// Detect vertical/horizontal resolution, because at the moment we downscale everything to
|
|
|
|
|
// fit Telegram's media preview requirements. If FFprobe fails to returns data, we consider
|
|
|
|
|
// by default the video to be vertical.
|
|
|
|
|
val isVertical = mediaInfo?.let {
|
|
|
|
|
it.streams.getOrNull(0)?.let { streamInformation ->
|
|
|
|
|
streamInformation.height > streamInformation.width
|
|
|
|
|
if (streamInformation.height != null && streamInformation.width != null)
|
|
|
|
|
streamInformation.height > streamInformation.width
|
|
|
|
|
else
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
} ?: true
|
|
|
|
|
val duration = mediaInfo?.duration ?: 0
|
|
|
|
@ -170,7 +198,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
|
|
val outputDir = getShareableDir() ?: return
|
|
|
|
|
val outputFile = File.createTempFile("npc", ".mp4", outputDir)
|
|
|
|
|
MixTask(WeakReference(this), duration, isVertical)
|
|
|
|
|
.execute(inputFile.canonicalPath, soundFile.canonicalPath, outputFile.canonicalPath)
|
|
|
|
|
.execute(inputVideoPath, soundFile.canonicalPath, outputFile.canonicalPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun getShareableDir(): File? {
|
|
|
|
@ -180,6 +208,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
|
|
return outputDir
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun clearVideoCache() {
|
|
|
|
|
val currentFileName = currentVideoPath?.let { File(it).name }
|
|
|
|
|
getShareableDir()?.apply {
|
|
|
|
|
listFiles()?.forEach { file ->
|
|
|
|
|
// Don't delete current file.
|
|
|
|
|
if (currentFileName == null || currentFileName != file.name)
|
|
|
|
|
file.delete()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class MixTask(
|
|
|
|
|
private val activity: WeakReference<MainActivity>,
|
|
|
|
|
private val duration: Long,
|
|
|
|
@ -201,14 +240,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
|
|
val command = (
|
|
|
|
|
"-i $videoPath -i $audioPath"
|
|
|
|
|
+ " -filter_complex amix=duration=longest"
|
|
|
|
|
+ " -c:v libx264 -crf 26 -vf scale=$width:-1 -pix_fmt yuv420p"
|
|
|
|
|
+ " -preset ultrafast"
|
|
|
|
|
+ " -c:v libx264 -crf 26 -vf scale=$width:trunc(ow/a/2)*2 -pix_fmt yuv420p"
|
|
|
|
|
+ " $durationOpt -y $outputPath"
|
|
|
|
|
)
|
|
|
|
|
Log.d(TAG, "Calling FFmpeg with command: $command")
|
|
|
|
|
return when (val rc = FFmpeg.execute(command)) {
|
|
|
|
|
RETURN_CODE_SUCCESS -> true.also { Log.i(TAG, "Mix succeeded.") }
|
|
|
|
|
RETURN_CODE_CANCEL -> false.also { Log.i(TAG, "Mix cancelled.") }
|
|
|
|
|
else -> false .also { Log.e(TAG, "Mix failed!") }
|
|
|
|
|
else -> false .also { Log.e(TAG, "Mix failed! rc = $rc") }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -252,33 +292,41 @@ class MainActivity : AppCompatActivity() {
|
|
|
|
|
private fun save(): Boolean {
|
|
|
|
|
if (currentVideoPath == null)
|
|
|
|
|
return false
|
|
|
|
|
val videoStore =
|
|
|
|
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q)
|
|
|
|
|
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
|
|
|
else
|
|
|
|
|
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
|
|
|
|
val videoDetails = ContentValues().apply {
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
|
|
|
put(MediaStore.Video.Media.IS_PENDING, 1)
|
|
|
|
|
val currentFile = File(currentVideoPath)
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
|
|
|
val store = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
|
|
|
val videoDetails = ContentValues()
|
|
|
|
|
videoDetails.put(MediaStore.Video.Media.IS_PENDING, 1)
|
|
|
|
|
val uri = contentResolver.insert(store, videoDetails)
|
|
|
|
|
?: return false .also { toast("Could not put video to media store.") }
|
|
|
|
|
contentResolver.openFileDescriptor(uri, "w").use { pfd ->
|
|
|
|
|
FileOutputStream(pfd?.fileDescriptor).use { fos ->
|
|
|
|
|
FileInputStream(currentVideoPath).use { fis ->
|
|
|
|
|
fos.buffered().write(fis.buffered().readBytes())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
currentVideoUri = contentResolver.insert(videoStore, videoDetails)
|
|
|
|
|
?: return false .also { toast("Could not save video to media store.") }
|
|
|
|
|
contentResolver.openFileDescriptor(currentVideoUri!!, "w").use { pfd ->
|
|
|
|
|
FileOutputStream(pfd?.fileDescriptor).use { fos ->
|
|
|
|
|
FileInputStream(currentVideoPath).use { fis ->
|
|
|
|
|
videoDetails.clear()
|
|
|
|
|
videoDetails.put(MediaStore.Video.Media.IS_PENDING, 0)
|
|
|
|
|
contentResolver.update(uri, videoDetails, null, null)
|
|
|
|
|
currentVideoUri = uri
|
|
|
|
|
toast("Video saved to media store!")
|
|
|
|
|
} else {
|
|
|
|
|
val externalDir = File(
|
|
|
|
|
Environment.getExternalStorageDirectory(),
|
|
|
|
|
"Movies/HarvestDawn"
|
|
|
|
|
)
|
|
|
|
|
if (!externalDir.isDirectory)
|
|
|
|
|
externalDir.mkdirs()
|
|
|
|
|
val savedFile = File(externalDir, currentFile.name)
|
|
|
|
|
FileOutputStream(savedFile).use { fos ->
|
|
|
|
|
FileInputStream(currentFile).use { fis ->
|
|
|
|
|
fos.buffered().write(fis.buffered().readBytes())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
currentVideoUri = savedFile.toUri()
|
|
|
|
|
toast("Video saved to external storage!")
|
|
|
|
|
}
|
|
|
|
|
videoDetails.apply {
|
|
|
|
|
clear()
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
|
|
|
put(MediaStore.Video.Media.IS_PENDING, 0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
contentResolver.update(currentVideoUri!!, videoDetails, null, null)
|
|
|
|
|
toast("Video saved to media store!")
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|