|
|
|
|
@ -10,6 +10,7 @@ import android.os.Build |
|
|
|
|
import android.os.Environment |
|
|
|
|
import android.os.IBinder |
|
|
|
|
import android.provider.MediaStore |
|
|
|
|
import androidx.core.content.FileProvider |
|
|
|
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL |
|
|
|
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS |
|
|
|
|
import com.arthenica.mobileffmpeg.FFmpeg |
|
|
|
|
@ -25,6 +26,7 @@ import net.pokeranalytics.android.util.extensions.findById |
|
|
|
|
import net.pokeranalytics.android.util.video.AnimatedGIFWriter |
|
|
|
|
import timber.log.Timber |
|
|
|
|
import java.io.File |
|
|
|
|
import java.io.FileOutputStream |
|
|
|
|
import java.util.* |
|
|
|
|
import kotlin.coroutines.CoroutineContext |
|
|
|
|
|
|
|
|
|
@ -47,19 +49,25 @@ class ReplayExportService : Service() { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
inner class LocalBinder : Binder() { |
|
|
|
|
|
|
|
|
|
fun getService(): ReplayExportService = this@ReplayExportService |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun videoExport(handHistoryId: String) { |
|
|
|
|
this@ReplayExportService.handHistoryId = handHistoryId |
|
|
|
|
startFFMPEGVideoExport() |
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { |
|
|
|
|
startFFMPEGVideoExport() |
|
|
|
|
} else { |
|
|
|
|
startFFMPEGVideoExportPreQ() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun gifExport(handHistoryId: String) { |
|
|
|
|
this@ReplayExportService.handHistoryId = handHistoryId |
|
|
|
|
startGIFExport() |
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { |
|
|
|
|
startGIFExport() |
|
|
|
|
} else { |
|
|
|
|
startGIFExportPreQ() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun startGIFExport() { |
|
|
|
|
@ -89,22 +97,17 @@ class ReplayExportService : Service() { |
|
|
|
|
// Add a specific media item. |
|
|
|
|
val resolver = applicationContext.contentResolver |
|
|
|
|
|
|
|
|
|
// Find all video files on the primary external storage device. |
|
|
|
|
val videoCollection = |
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { |
|
|
|
|
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) |
|
|
|
|
} else { |
|
|
|
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI |
|
|
|
|
} |
|
|
|
|
// Q version tested before calling the function |
|
|
|
|
val imageCollection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) |
|
|
|
|
|
|
|
|
|
val gifDetails = ContentValues().apply { |
|
|
|
|
put(MediaStore.Images.Media.DISPLAY_NAME, fileName) |
|
|
|
|
put(MediaStore.Images.Media.MIME_TYPE, "image/gif") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
val uri = resolver.insert(videoCollection, gifDetails) |
|
|
|
|
val uri = resolver.insert(imageCollection, gifDetails) |
|
|
|
|
|
|
|
|
|
if (uri != null) { |
|
|
|
|
|
|
|
|
|
val os = resolver.openOutputStream(uri) |
|
|
|
|
|
|
|
|
|
val writer = AnimatedGIFWriter(false) |
|
|
|
|
@ -128,12 +131,13 @@ class ReplayExportService : Service() { |
|
|
|
|
animationCount++ |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
writer.finishWrite(os) |
|
|
|
|
|
|
|
|
|
realm.close() |
|
|
|
|
notifyUser(uri, FileType.IMAGE) |
|
|
|
|
} else { |
|
|
|
|
Timber.w("Resolver insert ended without uri...") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
c.await() |
|
|
|
|
@ -159,7 +163,6 @@ class ReplayExportService : Service() { |
|
|
|
|
val height = square |
|
|
|
|
|
|
|
|
|
animator.configure(width.toFloat(), height.toFloat(), this@ReplayExportService) |
|
|
|
|
// animator.setDimension() |
|
|
|
|
val drawer = TableDrawer() |
|
|
|
|
drawer.configurePaints(context, animator) |
|
|
|
|
|
|
|
|
|
@ -170,16 +173,10 @@ class ReplayExportService : Service() { |
|
|
|
|
|
|
|
|
|
val formattedDate = Date().dateTimeFileFormatted |
|
|
|
|
val fileName = "hand_${formattedDate}.mp4" |
|
|
|
|
// val output = File( |
|
|
|
|
// Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), |
|
|
|
|
// fileName |
|
|
|
|
// ).path |
|
|
|
|
|
|
|
|
|
val outputDirectory = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES) ?: throw PAIllegalStateException("File is invalid") |
|
|
|
|
val output = "${outputDirectory.path}/$fileName" |
|
|
|
|
|
|
|
|
|
// Environment.getExternalStorageState(tmpDir) |
|
|
|
|
|
|
|
|
|
Timber.d("Assembling images for video...") |
|
|
|
|
|
|
|
|
|
FFmpeg.executeAsync("-f concat -safe 0 -i $dpath -vb 20M -vsync vfr -s ${width}x${height} -pix_fmt yuv420p $output") { id, rc -> |
|
|
|
|
@ -196,24 +193,22 @@ class ReplayExportService : Service() { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
File(dpath).delete() |
|
|
|
|
tmpDir.delete() |
|
|
|
|
|
|
|
|
|
val file = File(output) |
|
|
|
|
|
|
|
|
|
val resolver = applicationContext.contentResolver |
|
|
|
|
val videoCollection = |
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { |
|
|
|
|
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) |
|
|
|
|
} else { |
|
|
|
|
MediaStore.Video.Media.EXTERNAL_CONTENT_URI |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Q version tested before calling the function |
|
|
|
|
val videoCollection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) |
|
|
|
|
|
|
|
|
|
val fileDetails = ContentValues().apply { |
|
|
|
|
Timber.d("set file details = $fileName") |
|
|
|
|
put(MediaStore.Video.Media.DISPLAY_NAME, fileName) |
|
|
|
|
// put(MediaStore.Video.Media.CONTENT_TYPE, "video/mp4") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// copy video to nice path |
|
|
|
|
resolver.insert(videoCollection, fileDetails)?.let { uri -> |
|
|
|
|
|
|
|
|
|
Timber.d("copy file at uri = $uri") |
|
|
|
|
@ -222,7 +217,7 @@ class ReplayExportService : Service() { |
|
|
|
|
os?.write(file.readBytes()) |
|
|
|
|
os?.close() |
|
|
|
|
|
|
|
|
|
file.delete() |
|
|
|
|
file.delete() // delete temp file |
|
|
|
|
|
|
|
|
|
notifyUser(uri, FileType.VIDEO) |
|
|
|
|
|
|
|
|
|
@ -230,17 +225,6 @@ class ReplayExportService : Service() { |
|
|
|
|
Timber.w("Resolver insert ended without uri...") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// val uri = FileProvider.getUriForFile( |
|
|
|
|
// applicationContext, |
|
|
|
|
// applicationContext.packageName.toString() + ".fileprovider", |
|
|
|
|
// File(output) |
|
|
|
|
// ) |
|
|
|
|
|
|
|
|
|
// Timber.d("File exported at $output") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// notifyUser(output) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
@ -295,26 +279,159 @@ class ReplayExportService : Service() { |
|
|
|
|
// |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
private fun startGIFExportPreQ() { |
|
|
|
|
|
|
|
|
|
GlobalScope.launch(coroutineContext) { |
|
|
|
|
val c = GlobalScope.async { |
|
|
|
|
|
|
|
|
|
val realm = Realm.getDefaultInstance() |
|
|
|
|
realm.refresh() |
|
|
|
|
|
|
|
|
|
val handHistory = realm.findById<HandHistory>(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId") |
|
|
|
|
|
|
|
|
|
val context = this@ReplayExportService |
|
|
|
|
|
|
|
|
|
val animator = ReplayerAnimator(handHistory, true) |
|
|
|
|
|
|
|
|
|
val square = 1024 |
|
|
|
|
|
|
|
|
|
val width = square |
|
|
|
|
val height = square |
|
|
|
|
|
|
|
|
|
animator.configure(width.toFloat(), height.toFloat(), context) |
|
|
|
|
|
|
|
|
|
val formattedDate = Date().dateTimeFileFormatted |
|
|
|
|
val path = File( |
|
|
|
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), |
|
|
|
|
"hand_${formattedDate}.gif" |
|
|
|
|
).toString() |
|
|
|
|
|
|
|
|
|
val writer = AnimatedGIFWriter(false) |
|
|
|
|
val os = FileOutputStream(path) |
|
|
|
|
writer.prepareForWrite(os, width, height) |
|
|
|
|
|
|
|
|
|
val drawer = TableDrawer() |
|
|
|
|
drawer.configurePaints(context, animator) |
|
|
|
|
|
|
|
|
|
var animationCount = 0 |
|
|
|
|
animator.frames(context) { bitmap, count -> |
|
|
|
|
|
|
|
|
|
when { |
|
|
|
|
count > 10 -> { |
|
|
|
|
writer.writeFrame(os, bitmap, count * 8) |
|
|
|
|
animationCount = 0 |
|
|
|
|
} |
|
|
|
|
else -> { |
|
|
|
|
if (animationCount % 2 == 0) { |
|
|
|
|
writer.writeFrame(os, bitmap) |
|
|
|
|
} |
|
|
|
|
animationCount++ |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
writer.finishWrite(os) |
|
|
|
|
|
|
|
|
|
realm.close() |
|
|
|
|
notifyUser(path) |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
c.await() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun startFFMPEGVideoExportPreQ() { |
|
|
|
|
|
|
|
|
|
GlobalScope.launch(coroutineContext) { |
|
|
|
|
val async = GlobalScope.async { |
|
|
|
|
|
|
|
|
|
val realm = Realm.getDefaultInstance() |
|
|
|
|
val handHistory = realm.findById<HandHistory>(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId") |
|
|
|
|
|
|
|
|
|
val context = this@ReplayExportService |
|
|
|
|
|
|
|
|
|
val animator = ReplayerAnimator(handHistory, true) |
|
|
|
|
|
|
|
|
|
val square = 1024 |
|
|
|
|
|
|
|
|
|
val width = square |
|
|
|
|
val height = square |
|
|
|
|
|
|
|
|
|
animator.configure(width.toFloat(), height.toFloat(), this@ReplayExportService) |
|
|
|
|
val drawer = TableDrawer() |
|
|
|
|
drawer.configurePaints(context, animator) |
|
|
|
|
|
|
|
|
|
// generates all images and file descriptor |
|
|
|
|
Timber.d("Generating images for video...") |
|
|
|
|
val tmpDir = animator.generateVideoContent(this@ReplayExportService) |
|
|
|
|
val dpath = "${tmpDir.path}/$FFMPEG_DESCRIPTOR_FILE" |
|
|
|
|
|
|
|
|
|
val formattedDate = Date().dateTimeFileFormatted |
|
|
|
|
val output = File( |
|
|
|
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), |
|
|
|
|
"hand_${formattedDate}.mp4" |
|
|
|
|
).path |
|
|
|
|
|
|
|
|
|
Environment.getExternalStorageState(tmpDir) |
|
|
|
|
|
|
|
|
|
Timber.d("Assembling images for video...") |
|
|
|
|
FFmpeg.executeAsync("-f concat -safe 0 -i $dpath -vb 20M -vsync vfr -s ${width}x${height} -pix_fmt yuv420p $output") { id, rc -> |
|
|
|
|
|
|
|
|
|
if (rc == RETURN_CODE_SUCCESS) { |
|
|
|
|
Timber.d("FFMPEG command execution completed successfully") |
|
|
|
|
} else if (rc == RETURN_CODE_CANCEL) { |
|
|
|
|
Timber.d("Command execution cancelled by user.") |
|
|
|
|
} else { |
|
|
|
|
Timber.d(String.format("Command execution failed with rc=%d and the output below.", rc)) |
|
|
|
|
} |
|
|
|
|
// Delete descriptor and image files |
|
|
|
|
tmpDir.delete() |
|
|
|
|
File(dpath).delete() |
|
|
|
|
|
|
|
|
|
notifyUser(output) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
async.await() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun notifyUser(uri: Uri, type: FileType) { |
|
|
|
|
|
|
|
|
|
val title = getString(R.string.video_available) |
|
|
|
|
val body = getString(R.string.video_retrieval_message) + ": " + uri.path |
|
|
|
|
|
|
|
|
|
Timber.d("Show local notification, path of file: ${uri.path}") |
|
|
|
|
this.showNotification(title, body, uri, type.value) |
|
|
|
|
|
|
|
|
|
// val uri = FileProvider.getUriForFile( |
|
|
|
|
// this, |
|
|
|
|
// this.applicationContext.packageName.toString() + ".fileprovider", |
|
|
|
|
// File(path) |
|
|
|
|
// ) |
|
|
|
|
// |
|
|
|
|
// val type = when { |
|
|
|
|
// path.contains("gif") -> "image/gif" |
|
|
|
|
// else -> "video/*" |
|
|
|
|
// } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun notifyUser(path: String) { |
|
|
|
|
|
|
|
|
|
val title = getString(R.string.video_available) |
|
|
|
|
val body = getString(R.string.video_retrieval_message) + ": " + path |
|
|
|
|
|
|
|
|
|
Timber.d("Show local notification, path of file: ${path}") |
|
|
|
|
|
|
|
|
|
val uri = FileProvider.getUriForFile( |
|
|
|
|
this, |
|
|
|
|
this.applicationContext.packageName.toString() + ".fileprovider", |
|
|
|
|
File(path) |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
val type = when { |
|
|
|
|
path.contains("gif") -> "image/gif" |
|
|
|
|
else -> "video/*" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.showNotification(title, body, uri, type) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun showNotification(title: String, body: String, uri: Uri, type: String) { |
|
|
|
|
|
|
|
|
|
val intent = Intent(Intent.ACTION_VIEW) |
|
|
|
|
intent.setDataAndType(uri, type.value) |
|
|
|
|
intent.setDataAndType(uri, type) |
|
|
|
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK |
|
|
|
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) |
|
|
|
|
val chooser = Intent.createChooser(intent, getString(R.string.open_file_with)) |
|
|
|
|
|