Update video sample app to read and write from private app storage.
This commit is contained in:
parent
8727f0d90d
commit
076df8c429
6 changed files with 100 additions and 104 deletions
|
@ -39,6 +39,7 @@ import org.thoughtcrime.video.app.transcode.composables.ConfigureEncodingParamet
|
|||
import org.thoughtcrime.video.app.transcode.composables.SelectInput
|
||||
import org.thoughtcrime.video.app.transcode.composables.SelectOutput
|
||||
import org.thoughtcrime.video.app.transcode.composables.TranscodingJobProgress
|
||||
import org.thoughtcrime.video.app.transcode.composables.WorkState
|
||||
import org.thoughtcrime.video.app.ui.theme.SignalTheme
|
||||
|
||||
/**
|
||||
|
@ -67,7 +68,7 @@ class TranscodeTestActivity : AppCompatActivity() {
|
|||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
val transcodingJobs = viewModel.getTranscodingJobsAsState().collectAsState(emptyList())
|
||||
if (transcodingJobs.value.isNotEmpty()) {
|
||||
TranscodingJobProgress(transcodingJobs = transcodingJobs.value, resetButtonOnClick = { viewModel.reset() })
|
||||
TranscodingJobProgress(transcodingJobs = transcodingJobs.value.map { WorkState.fromInfo(it) }, resetButtonOnClick = { viewModel.reset() })
|
||||
} else if (viewModel.selectedVideos.isEmpty()) {
|
||||
SelectInput { pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly)) }
|
||||
} else if (viewModel.outputDirectory == null) {
|
||||
|
|
|
@ -7,7 +7,6 @@ package org.thoughtcrime.video.app.transcode
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
|
@ -15,7 +14,6 @@ import androidx.work.WorkManager
|
|||
import androidx.work.WorkQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.signal.core.util.readToList
|
||||
import org.thoughtcrime.securesms.video.TranscodingPreset
|
||||
import java.util.UUID
|
||||
import kotlin.math.absoluteValue
|
||||
|
@ -100,42 +98,12 @@ class TranscodeTestRepository(context: Context) {
|
|||
workManager.pruneWork()
|
||||
}
|
||||
|
||||
fun cleanFailedTranscodes(context: Context, folderUri: Uri) {
|
||||
val docs = queryChildDocuments(context, folderUri)
|
||||
docs.filter { it.documentId.endsWith(".tmp") }.forEach {
|
||||
val fileUri = DocumentsContract.buildDocumentUriUsingTree(folderUri, it.documentId)
|
||||
DocumentsContract.deleteDocument(context.contentResolver, fileUri)
|
||||
fun cleanFailedTranscodes(context: Context) {
|
||||
context.filesDir.listFiles()?.filter { it.name.endsWith(TranscodeWorker.TEMP_FILE_EXTENSION) }?.forEach {
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryChildDocuments(context: Context, folderUri: Uri): List<FileMetadata> {
|
||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||
folderUri,
|
||||
DocumentsContract.getTreeDocumentId(folderUri)
|
||||
)
|
||||
|
||||
context.contentResolver.query(
|
||||
childrenUri,
|
||||
arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_SIZE),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use { cursor ->
|
||||
if (cursor == null) {
|
||||
return emptyList()
|
||||
}
|
||||
return cursor.readToList {
|
||||
FileMetadata(
|
||||
documentId = it.getString(0),
|
||||
label = it.getString(1),
|
||||
size = it.getLong(2)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class FileMetadata(val documentId: String, val label: String, val size: Long)
|
||||
|
||||
data class CustomTranscodingOptions(val videoResolution: VideoResolution, val videoBitrate: Int, val audioBitrate: Int, val enableFastStart: Boolean, val enableAudioRemux: Boolean)
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -87,7 +87,7 @@ class TranscodeTestViewModel : ViewModel() {
|
|||
|
||||
fun setOutputDirectoryAndCleanFailedTranscodes(context: Context, folderUri: Uri) {
|
||||
outputDirectory = folderUri
|
||||
repository.cleanFailedTranscodes(context, folderUri)
|
||||
repository.cleanFailedTranscodes(context)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
|
|
|
@ -21,7 +21,6 @@ import androidx.work.Data
|
|||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import org.signal.core.util.getLength
|
||||
import org.signal.core.util.readLength
|
||||
import org.thoughtcrime.securesms.video.StreamingTranscoder
|
||||
import org.thoughtcrime.securesms.video.TranscodingPreset
|
||||
|
@ -29,6 +28,8 @@ import org.thoughtcrime.securesms.video.postprocessing.Mp4FaststartPostProcessor
|
|||
import org.thoughtcrime.securesms.video.videoconverter.mediadatasource.InputStreamMediaDataSource
|
||||
import org.thoughtcrime.securesms.video.videoconverter.utils.VideoConstants
|
||||
import org.thoughtcrime.video.app.R
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.time.Instant
|
||||
|
@ -48,80 +49,82 @@ class TranscodeWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(
|
|||
return Result.failure()
|
||||
}
|
||||
|
||||
val notificationId = inputData.getInt(KEY_NOTIFICATION_ID, -1)
|
||||
if (notificationId < 0) {
|
||||
Log.w(TAG, "$logPrefix Notification ID was null!")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val inputUri = inputData.getString(KEY_INPUT_URI)
|
||||
if (inputUri == null) {
|
||||
Log.w(TAG, "$logPrefix Input URI was null!")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val outputDirUri = inputData.getString(KEY_OUTPUT_URI)
|
||||
if (outputDirUri == null) {
|
||||
Log.w(TAG, "$logPrefix Output URI was null!")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val postProcessForFastStart = inputData.getBoolean(KEY_ENABLE_FASTSTART, true)
|
||||
val transcodingPreset = inputData.getString(KEY_TRANSCODING_PRESET_NAME)
|
||||
val resolution = inputData.getInt(KEY_SHORT_EDGE, -1)
|
||||
val videoBitrate = inputData.getInt(KEY_VIDEO_BIT_RATE, -1)
|
||||
val audioBitrate = inputData.getInt(KEY_AUDIO_BIT_RATE, -1)
|
||||
val audioRemux = inputData.getBoolean(KEY_ENABLE_AUDIO_REMUX, true)
|
||||
|
||||
val input = DocumentFile.fromSingleUri(applicationContext, Uri.parse(inputUri))?.name
|
||||
if (input == null) {
|
||||
val inputParams = InputParams(inputData)
|
||||
val inputFilename = DocumentFile.fromSingleUri(applicationContext, inputParams.inputUri)?.name
|
||||
if (inputFilename == null) {
|
||||
Log.w(TAG, "$logPrefix Could not read input file name!")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val filenameBase = "transcoded-${Instant.now()}-$input"
|
||||
val filenameBase = "transcoded-${Instant.now()}-$inputFilename"
|
||||
val tempFilename = "$filenameBase$TEMP_FILE_EXTENSION"
|
||||
val finalFilename = "$filenameBase$OUTPUT_FILE_EXTENSION"
|
||||
|
||||
val tempFile = createFile(Uri.parse(outputDirUri), tempFilename)
|
||||
if (tempFile == null) {
|
||||
Log.w(TAG, "$logPrefix Could not create temp file!")
|
||||
return Result.failure()
|
||||
}
|
||||
setForeground(createForegroundInfo(-1, inputParams.notificationId))
|
||||
|
||||
val datasource = WorkerMediaDataSource(applicationContext, Uri.parse(inputUri))
|
||||
|
||||
val transcoder = if (resolution > 0 && videoBitrate > 0) {
|
||||
Log.d(TAG, "$logPrefix Initializing StreamingTranscoder with custom parameters: B:V=$videoBitrate, B:A=$audioBitrate, res=$resolution, audioRemux=$audioRemux")
|
||||
StreamingTranscoder.createManuallyForTesting(datasource, null, videoBitrate, audioBitrate, resolution, audioRemux)
|
||||
} else if (transcodingPreset != null) {
|
||||
StreamingTranscoder(datasource, null, TranscodingPreset.valueOf(transcodingPreset), DEFAULT_FILE_SIZE_LIMIT, audioRemux)
|
||||
} else {
|
||||
throw IllegalArgumentException("Improper input data! No TranscodingPreset defined, or invalid manual parameters!")
|
||||
}
|
||||
|
||||
setForeground(createForegroundInfo(-1, notificationId))
|
||||
applicationContext.contentResolver.openOutputStream(tempFile.uri).use { outputStream ->
|
||||
applicationContext.openFileOutput(tempFilename, Context.MODE_PRIVATE).use { outputStream ->
|
||||
if (outputStream == null) {
|
||||
Log.w(TAG, "$logPrefix Could not open temp file for I/O!")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
applicationContext.contentResolver.openInputStream(inputParams.inputUri).use { inputStream ->
|
||||
applicationContext.openFileOutput(inputFilename, Context.MODE_PRIVATE).use { outputStream ->
|
||||
Log.i(TAG, "Started copying input to internal storage.")
|
||||
inputStream?.copyTo(outputStream)
|
||||
Log.i(TAG, "Finished copying input to internal storage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val datasource = WorkerMediaDataSource(File(applicationContext.filesDir, inputFilename))
|
||||
|
||||
val transcoder = if (inputParams.resolution > 0 && inputParams.videoBitrate > 0) {
|
||||
Log.d(TAG, "$logPrefix Initializing StreamingTranscoder with custom parameters: B:V=${inputParams.videoBitrate}, B:A=${inputParams.audioBitrate}, res=${inputParams.resolution}, audioRemux=${inputParams.audioRemux}")
|
||||
StreamingTranscoder.createManuallyForTesting(datasource, null, inputParams.videoBitrate, inputParams.audioBitrate, inputParams.resolution, inputParams.audioRemux)
|
||||
} else if (inputParams.transcodingPreset != null) {
|
||||
StreamingTranscoder(datasource, null, inputParams.transcodingPreset, DEFAULT_FILE_SIZE_LIMIT, inputParams.audioRemux)
|
||||
} else {
|
||||
throw IllegalArgumentException("Improper input data! No TranscodingPreset defined, or invalid manual parameters!")
|
||||
}
|
||||
|
||||
applicationContext.openFileOutput(tempFilename, Context.MODE_PRIVATE).use { outputStream ->
|
||||
transcoder.transcode({ percent: Int ->
|
||||
if (lastProgress != percent) {
|
||||
lastProgress = percent
|
||||
Log.v(TAG, "$logPrefix Updating progress percent to $percent%")
|
||||
setProgressAsync(Data.Builder().putInt(KEY_PROGRESS, percent).build())
|
||||
setForegroundAsync(createForegroundInfo(percent, notificationId))
|
||||
setForegroundAsync(createForegroundInfo(percent, inputParams.notificationId))
|
||||
}
|
||||
}, outputStream, { isStopped })
|
||||
}
|
||||
|
||||
Log.v(TAG, "$logPrefix Initial transcode completed successfully!")
|
||||
if (!postProcessForFastStart) {
|
||||
tempFile.renameTo(finalFilename)
|
||||
|
||||
val finalFile = createFile(inputParams.outputDirUri, finalFilename) ?: run {
|
||||
Log.w(TAG, "$logPrefix Could not create final file for faststart processing!")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
if (!inputParams.postProcessForFastStart) {
|
||||
applicationContext.openFileInput(tempFilename).use { tempFileStream ->
|
||||
if (tempFileStream == null) {
|
||||
Log.w(TAG, "$logPrefix Could not open temp file for I/O!")
|
||||
return Result.failure()
|
||||
}
|
||||
applicationContext.contentResolver.openOutputStream(finalFile.uri, "w").use { finalFileStream ->
|
||||
if (finalFileStream == null) {
|
||||
Log.w(TAG, "$logPrefix Could not open output file for I/O!")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
tempFileStream.copyTo(finalFileStream)
|
||||
}
|
||||
}
|
||||
Log.v(TAG, "$logPrefix Rename successful.")
|
||||
} else {
|
||||
val tempFileLength: Long
|
||||
applicationContext.contentResolver.openInputStream(tempFile.uri).use { tempFileStream ->
|
||||
applicationContext.openFileInput(tempFilename).use { tempFileStream ->
|
||||
if (tempFileStream == null) {
|
||||
Log.w(TAG, "$logPrefix Could not open temp file for I/O!")
|
||||
return Result.failure()
|
||||
|
@ -129,17 +132,14 @@ class TranscodeWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(
|
|||
|
||||
tempFileLength = tempFileStream.readLength()
|
||||
}
|
||||
val finalFile = createFile(Uri.parse(outputDirUri), finalFilename) ?: run {
|
||||
Log.w(TAG, "$logPrefix Could not create final file for faststart processing!")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
applicationContext.contentResolver.openOutputStream(finalFile.uri, "w").use { finalFileStream ->
|
||||
if (finalFileStream == null) {
|
||||
Log.w(TAG, "$logPrefix Could not open output file for I/O!")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val inputStreamFactory = { applicationContext.contentResolver.openInputStream(tempFile.uri) ?: throw IOException("Could not open temp file for reading!") }
|
||||
val inputStreamFactory = { applicationContext.openFileInput(tempFilename) ?: throw IOException("Could not open temp file for reading!") }
|
||||
val bytesCopied = Mp4FaststartPostProcessor(inputStreamFactory).processAndWriteTo(finalFileStream)
|
||||
|
||||
if (bytesCopied != tempFileLength) {
|
||||
|
@ -147,6 +147,7 @@ class TranscodeWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(
|
|||
return Result.failure()
|
||||
}
|
||||
|
||||
val tempFile = File(applicationContext.filesDir, tempFilename)
|
||||
if (!tempFile.delete()) {
|
||||
Log.w(TAG, "$logPrefix Failed to delete temp file after processing!")
|
||||
return Result.failure()
|
||||
|
@ -194,10 +195,9 @@ class TranscodeWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(
|
|||
return DocumentFile.fromTreeUri(applicationContext, treeUri)?.createFile(VideoConstants.VIDEO_MIME_TYPE, filename)
|
||||
}
|
||||
|
||||
private class WorkerMediaDataSource(context: Context, private val uri: Uri) : InputStreamMediaDataSource() {
|
||||
private class WorkerMediaDataSource(private val file: File) : InputStreamMediaDataSource() {
|
||||
|
||||
private val contentResolver = context.contentResolver
|
||||
private val size = contentResolver.getLength(uri) ?: throw IllegalStateException()
|
||||
private val size = file.length()
|
||||
|
||||
private var inputStream: InputStream? = null
|
||||
|
||||
|
@ -211,17 +211,29 @@ class TranscodeWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(
|
|||
|
||||
override fun createInputStream(position: Long): InputStream {
|
||||
inputStream?.close()
|
||||
val openedInputStream = contentResolver.openInputStream(uri) ?: throw IllegalStateException()
|
||||
val openedInputStream = FileInputStream(file)
|
||||
openedInputStream.skip(position)
|
||||
inputStream = openedInputStream
|
||||
return openedInputStream
|
||||
}
|
||||
}
|
||||
|
||||
private data class InputParams(private val inputData: Data) {
|
||||
val notificationId: Int = inputData.getInt(KEY_NOTIFICATION_ID, -1)
|
||||
val inputUri: Uri = Uri.parse(inputData.getString(KEY_INPUT_URI))
|
||||
val outputDirUri: Uri = Uri.parse(inputData.getString(KEY_OUTPUT_URI))
|
||||
val postProcessForFastStart: Boolean = inputData.getBoolean(KEY_ENABLE_FASTSTART, true)
|
||||
val transcodingPreset: TranscodingPreset? = inputData.getString(KEY_TRANSCODING_PRESET_NAME)?.let { TranscodingPreset.valueOf(it) }
|
||||
val resolution: Int = inputData.getInt(KEY_SHORT_EDGE, -1)
|
||||
val videoBitrate: Int = inputData.getInt(KEY_VIDEO_BIT_RATE, -1)
|
||||
val audioBitrate: Int = inputData.getInt(KEY_AUDIO_BIT_RATE, -1)
|
||||
val audioRemux: Boolean = inputData.getBoolean(KEY_ENABLE_AUDIO_REMUX, true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TranscodeWorker"
|
||||
private const val OUTPUT_FILE_EXTENSION = ".mp4"
|
||||
private const val TEMP_FILE_EXTENSION = ".tmp"
|
||||
const val TEMP_FILE_EXTENSION = ".tmp"
|
||||
private const val DEFAULT_FILE_SIZE_LIMIT: Long = 100 * 1024 * 1024
|
||||
const val KEY_INPUT_URI = "input_uri"
|
||||
const val KEY_OUTPUT_URI = "output_uri"
|
||||
|
|
|
@ -139,7 +139,7 @@ private fun PresetPicker(
|
|||
.fillMaxWidth()
|
||||
.selectableGroup()
|
||||
) {
|
||||
TranscodingPreset.values().forEach {
|
||||
TranscodingPreset.entries.forEach {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
|
@ -185,7 +185,7 @@ private fun CustomSettings(
|
|||
.fillMaxWidth()
|
||||
.selectableGroup()
|
||||
) {
|
||||
VideoResolution.values().forEach {
|
||||
VideoResolution.entries.forEach {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
|
|
|
@ -25,20 +25,20 @@ import org.thoughtcrime.video.app.ui.composables.LabeledButton
|
|||
* A view that shows the current encodes in progress.
|
||||
*/
|
||||
@Composable
|
||||
fun TranscodingJobProgress(transcodingJobs: List<WorkInfo>, resetButtonOnClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
fun TranscodingJobProgress(transcodingJobs: List<WorkState>, resetButtonOnClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
transcodingJobs.forEach { workInfo ->
|
||||
val currentProgress = workInfo.progress.getInt(TranscodeWorker.KEY_PROGRESS, -1)
|
||||
val currentProgress = workInfo.progress
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
val progressIndicatorModifier = Modifier.weight(3f)
|
||||
Text(
|
||||
text = "Job ${workInfo.id.toString().takeLast(4)}",
|
||||
text = "Job ${workInfo.id.takeLast(4)}",
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.weight(1f)
|
||||
|
@ -56,8 +56,23 @@ fun TranscodingJobProgress(transcodingJobs: List<WorkInfo>, resetButtonOnClick:
|
|||
}
|
||||
}
|
||||
|
||||
data class WorkState(val id: String, val state: WorkInfo.State, val progress: Int) {
|
||||
companion object {
|
||||
fun fromInfo(info: WorkInfo): WorkState {
|
||||
return WorkState(info.id.toString(), info.state, info.progress.getInt(TranscodeWorker.KEY_PROGRESS, -1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ProgressScreenPreview() {
|
||||
TranscodingJobProgress(emptyList(), resetButtonOnClick = {})
|
||||
TranscodingJobProgress(
|
||||
listOf(
|
||||
WorkState("abcde", WorkInfo.State.RUNNING, 47),
|
||||
WorkState("fghij", WorkInfo.State.ENQUEUED, -1),
|
||||
WorkState("klmnop", WorkInfo.State.FAILED, -1)
|
||||
),
|
||||
resetButtonOnClick = {}
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue