Compare commits

...

9 Commits

@ -9,8 +9,8 @@ android {
applicationId "io.lowrespalmtree.harvestdawn"
minSdkVersion 24
targetSdkVersion 29
versionCode 1
versionName "1.0"
versionCode 2
versionName "1.1"
}
buildTypes {
@ -36,8 +36,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.4'
implementation 'com.arthenica:mobile-ffmpeg-min-gpl:4.3.2'
}

@ -23,6 +23,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<provider

@ -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
}

@ -12,4 +12,9 @@
android:orderInCategory="100"
android:title="@string/action_share"
app:showAsAction="never" />
<item
android:id="@+id/action_clear_cache"
android:orderInCategory="100"
android:title="@string/action_clear_cache"
app:showAsAction="never" />
</menu>

@ -6,4 +6,5 @@
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
<string name="send">Share</string>
<string name="action_save">Save</string>
<string name="action_clear_cache">Clear cache</string>
</resources>

@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath 'com.android.tools.build:gradle:4.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

@ -1,6 +1,6 @@
#Sun Mar 07 15:30:37 CET 2021
#Wed Mar 17 16:33:26 CET 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

Loading…
Cancel
Save