diff --git a/app/src/main/java/com/abedelazizshe/lightcompressor/MainActivity.kt b/app/src/main/java/com/abedelazizshe/lightcompressor/MainActivity.kt index fa87767..12a3de9 100644 --- a/app/src/main/java/com/abedelazizshe/lightcompressor/MainActivity.kt +++ b/app/src/main/java/com/abedelazizshe/lightcompressor/MainActivity.kt @@ -40,14 +40,12 @@ class MainActivity : AppCompatActivity() { const val REQUEST_CAPTURE_VIDEO = 1 } - private val uris = mutableListOf() - private val data = mutableListOf() private lateinit var adapter: RecyclerViewAdapter + private val compressors: MutableList = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) @@ -62,12 +60,14 @@ class MainActivity : AppCompatActivity() { } binding.cancel.setOnClickListener { - VideoCompressor.cancel() + compressors.forEach { + it.cancel() + } } val recyclerview = findViewById(R.id.recyclerview) recyclerview.layoutManager = LinearLayoutManager(this) - adapter = RecyclerViewAdapter(applicationContext, data) + adapter = RecyclerViewAdapter(applicationContext, mutableListOf()) recyclerview.adapter = adapter } @@ -108,24 +108,22 @@ class MainActivity : AppCompatActivity() { private fun handleResult(data: Intent?) { val clipData: ClipData? = data?.clipData + val uris = mutableListOf() if (clipData != null) { for (i in 0 until clipData.itemCount) { val videoItem = clipData.getItemAt(i) uris.add(videoItem.uri) } - processVideo() + processVideo(uris) } else if (data != null && data.data != null) { val uri = data.data uris.add(uri!!) - processVideo() + processVideo(uris) } } private fun reset() { - uris.clear() binding.mainContents.visibility = View.GONE - data.clear() - adapter.notifyDataSetChanged() } private fun setReadStoragePermission() { @@ -171,69 +169,61 @@ class MainActivity : AppCompatActivity() { } @SuppressLint("SetTextI18n") - private fun processVideo() { + private fun processVideo(uris: List) { binding.mainContents.visibility = View.VISIBLE - lifecycleScope.launch { - VideoCompressor.start( - context = applicationContext, - uris, - isStreamable = false, - sharedStorageConfiguration = SharedStorageConfiguration( - saveAt = SaveLocation.movies, - subFolderName = "my-demo-videos" - ), -// appSpecificStorageConfiguration = AppSpecificStorageConfiguration( -// -// ), - configureWith = Configuration( - quality = VideoQuality.LOW, - videoNames = uris.map { uri -> uri.pathSegments.last() }, - isMinBitrateCheckEnabled = true, - ), - listener = object : CompressionListener { - override fun onProgress(index: Int, percent: Float) { - //Update UI - if (percent <= 100) - runOnUiThread { - data[index] = VideoDetailsModel( - "", - uris[index], - "", - percent - ) - adapter.notifyDataSetChanged() - } - } - - override fun onStart(index: Int) { - data.add( - index, - VideoDetailsModel("", uris[index], "") - ) - adapter.notifyDataSetChanged() - } - - override fun onSuccess(index: Int, size: Long, path: String?) { - data[index] = VideoDetailsModel( - path, - uris[index], - getFileSize(size), - 100F - ) - adapter.notifyDataSetChanged() - } - - override fun onFailure(index: Int, failureMessage: String) { - Log.wtf("failureMessage", failureMessage) - } - - override fun onCancelled(index: Int) { - Log.wtf("TAG", "compression has been cancelled") - // make UI changes, cleanup, etc - } - }, - ) + uris.forEach { uri -> + lifecycleScope.launch { + val compressor = VideoCompressor.createInstance(uri = uri) + compressors.add(compressor) + compressor.start( + context = applicationContext, + isStreamable = false, + sharedStorageConfiguration = SharedStorageConfiguration( + saveAt = SaveLocation.movies, + subFolderName = "my-demo-videos" + ), + /*appSpecificStorageConfiguration = AppSpecificStorageConfiguration( + subFolderName = "temp-videos" + ),*/ + configureWith = Configuration( + quality = VideoQuality.LOW, + videoName = uri.pathSegments.last(), + isMinBitrateCheckEnabled = true, + ), + listener = object : CompressionListener { + override fun onProgress(percent: Float) { + //Update UI + if (percent <= 100) + runOnUiThread { + adapter.updateProgressForUri(uri, percent) + } + } + + override fun onStart() { + adapter.addData(VideoDetailsModel("", uri, "")) + } + + override fun onSuccess(size: Long, path: String?) { + adapter.refreshItem(VideoDetailsModel( + path, + uri, + getFileSize(size), + 100F + )) + } + + override fun onFailure(failureMessage: String) { + Log.wtf("failureMessage", failureMessage) + } + + override fun onCancelled() { + Log.wtf("TAG", "compression has been cancelled") + // make UI changes, cleanup, etc + } + }, + ) + } } } } diff --git a/app/src/main/java/com/abedelazizshe/lightcompressor/RecyclerViewAdapter.kt b/app/src/main/java/com/abedelazizshe/lightcompressor/RecyclerViewAdapter.kt index a3e1b33..b450dc8 100644 --- a/app/src/main/java/com/abedelazizshe/lightcompressor/RecyclerViewAdapter.kt +++ b/app/src/main/java/com/abedelazizshe/lightcompressor/RecyclerViewAdapter.kt @@ -1,6 +1,7 @@ package com.abedelazizshe.lightcompressor import android.content.Context +import android.net.Uri import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -10,7 +11,7 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide -class RecyclerViewAdapter(private val context: Context, private val list: List) : +class RecyclerViewAdapter(private val context: Context, private val list: MutableList) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -20,6 +21,28 @@ class RecyclerViewAdapter(private val context: Context, private val list: List + model.progress = progress + notifyDataSetChanged() + } + } + override fun onBindViewHolder(holder: ViewHolder, position: Int) { val itemsViewModel = list[position] diff --git a/app/src/main/java/com/abedelazizshe/lightcompressor/VideoDetailsModel.kt b/app/src/main/java/com/abedelazizshe/lightcompressor/VideoDetailsModel.kt index fb5007e..c728b98 100644 --- a/app/src/main/java/com/abedelazizshe/lightcompressor/VideoDetailsModel.kt +++ b/app/src/main/java/com/abedelazizshe/lightcompressor/VideoDetailsModel.kt @@ -6,5 +6,5 @@ data class VideoDetailsModel( val playableVideoPath: String?, val uri: Uri, val newSize: String, - val progress: Float = 0F + var progress: Float = 0F ) diff --git a/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/CompressionInterface.kt b/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/CompressionInterface.kt index 1aeea48..ef3180e 100644 --- a/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/CompressionInterface.kt +++ b/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/CompressionInterface.kt @@ -9,22 +9,22 @@ import androidx.annotation.WorkerThread */ interface CompressionListener { @MainThread - fun onStart(index: Int) + fun onStart() @MainThread - fun onSuccess(index: Int, size: Long, path: String?) + fun onSuccess(size: Long, path: String?) @MainThread - fun onFailure(index: Int, failureMessage: String) + fun onFailure(failureMessage: String) @WorkerThread - fun onProgress(index: Int, percent: Float) + fun onProgress(percent: Float) @WorkerThread - fun onCancelled(index: Int) + fun onCancelled() } interface CompressionProgressListener { - fun onProgressChanged(index: Int, percent: Float) - fun onProgressCancelled(index: Int) + fun onProgressChanged(percent: Float) + fun onProgressCancelled() } diff --git a/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/VideoCompressor.kt b/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/VideoCompressor.kt index e97290d..e56a7c9 100644 --- a/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/VideoCompressor.kt +++ b/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/VideoCompressor.kt @@ -9,8 +9,7 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import androidx.annotation.RequiresApi -import com.abedelazizshe.lightcompressorlibrary.compressor.Compressor.compressVideo -import com.abedelazizshe.lightcompressorlibrary.compressor.Compressor.isRunning +import com.abedelazizshe.lightcompressorlibrary.compressor.Compressor import com.abedelazizshe.lightcompressorlibrary.config.* import com.abedelazizshe.lightcompressorlibrary.video.Result import kotlinx.coroutines.* @@ -24,10 +23,18 @@ enum class VideoQuality { VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW } -object VideoCompressor : CoroutineScope by MainScope() { +class VideoCompressor private constructor(private val uri: Uri): CoroutineScope by MainScope() { + + companion object { + fun createInstance(uri: Uri): VideoCompressor { + return VideoCompressor(uri = uri) + } + } private var job: Job? = null + private val compressor = Compressor() + /** * This function compresses a given list of [uris] of video files and writes the compressed * video file at [SharedStorageConfiguration.saveAt] directory, or at [AppSpecificStorageConfiguration.subFolderName] @@ -61,11 +68,8 @@ object VideoCompressor : CoroutineScope by MainScope() { * [Configuration.videoHeight] which is a custom height for the video. Must be specified with [Configuration.videoWidth] * [Configuration.videoWidth] which is a custom width for the video. Must be specified with [Configuration.videoHeight] */ - @JvmStatic - @JvmOverloads fun start( context: Context, - uris: List, isStreamable: Boolean = false, sharedStorageConfiguration: SharedStorageConfiguration? = null, appSpecificStorageConfiguration: AppSpecificStorageConfiguration? = null, @@ -74,11 +78,10 @@ object VideoCompressor : CoroutineScope by MainScope() { ) { // Only one is allowed assert(sharedStorageConfiguration == null || appSpecificStorageConfiguration == null) - assert(configureWith.videoNames.size == uris.size) doVideoCompression( context, - uris, + uri, isStreamable, sharedStorageConfiguration, appSpecificStorageConfiguration, @@ -90,15 +93,14 @@ object VideoCompressor : CoroutineScope by MainScope() { /** * Call this function to cancel video compression process which will call [CompressionListener.onCancelled] */ - @JvmStatic fun cancel() { job?.cancel() - isRunning = false + compressor.isRunning = false } private fun doVideoCompression( context: Context, - uris: List, + uri: Uri, isStreamable: Boolean, sharedStorageConfiguration: SharedStorageConfiguration?, appSpecificStorageConfiguration: AppSpecificStorageConfiguration?, @@ -106,70 +108,65 @@ object VideoCompressor : CoroutineScope by MainScope() { listener: CompressionListener, ) { var streamableFile: File? = null - for (i in uris.indices) { - - job = launch { - - val job = async { getMediaPath(context, uris[i]) } - val path = job.await() + job = launch { + + val job = async { getMediaPath(context, uri) } + val path = job.await() + + val desFile = saveVideoFile( + context, + path, + sharedStorageConfiguration, + appSpecificStorageConfiguration, + isStreamable, + configuration.videoName, + shouldSave = false + ) - val desFile = saveVideoFile( + if (isStreamable) + streamableFile = saveVideoFile( context, path, sharedStorageConfiguration, appSpecificStorageConfiguration, - isStreamable, - configuration.videoNames[i], + null, + configuration.videoName, shouldSave = false ) - if (isStreamable) - streamableFile = saveVideoFile( + desFile?.let { + compressor.isRunning = true + listener.onStart() + val result = startCompression( + context, + uri, + desFile.path, + streamableFile?.path, + configuration, + listener, + ) + + // Runs in Main(UI) Thread + if (result.success) { + saveVideoFile( context, - path, + result.path, sharedStorageConfiguration, appSpecificStorageConfiguration, - null, - configuration.videoNames[i], - shouldSave = false - ) - - desFile?.let { - isRunning = true - listener.onStart(i) - val result = startCompression( - i, - context, - uris[i], - desFile.path, - streamableFile?.path, - configuration, - listener, + isStreamable, + configuration.videoName, + shouldSave = true ) - // Runs in Main(UI) Thread - if (result.success) { - saveVideoFile( - context, - result.path, - sharedStorageConfiguration, - appSpecificStorageConfiguration, - isStreamable, - configuration.videoNames[i], - shouldSave = true - ) - - listener.onSuccess(i, result.size, result.path) - } else { - listener.onFailure(i, result.failureMessage ?: "An error has occurred!") - } + listener.onSuccess(result.size, result.path) + } else { + listener.onFailure(result.failureMessage ?: "An error has occurred!") } } } } private suspend fun startCompression( - index: Int, context: Context, srcUri: Uri, destPath: String, @@ -177,20 +174,19 @@ object VideoCompressor : CoroutineScope by MainScope() { configuration: Configuration, listener: CompressionListener, ): Result = withContext(Dispatchers.Default) { - return@withContext compressVideo( - index, + return@withContext compressor.compressVideo( context, srcUri, destPath, streamableFile, configuration, object : CompressionProgressListener { - override fun onProgressChanged(index: Int, percent: Float) { - listener.onProgress(index, percent) + override fun onProgressChanged(percent: Float) { + listener.onProgress(percent) } - override fun onProgressCancelled(index: Int) { - listener.onCancelled(index) + override fun onProgressCancelled() { + listener.onCancelled() } }, ) diff --git a/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/compressor/Compressor.kt b/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/compressor/Compressor.kt index a4b2c3e..9cafd12 100644 --- a/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/compressor/Compressor.kt +++ b/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/compressor/Compressor.kt @@ -27,23 +27,25 @@ import java.nio.ByteBuffer * Created by AbedElaziz Shehadeh on 27 Jan, 2020 * elaziz.shehadeh@gmail.com */ -object Compressor { +class Compressor { - // 2Mbps - private const val MIN_BITRATE = 2000000 + companion object { + // 2Mbps + private const val MIN_BITRATE = 2000000 - // H.264 Advanced Video Coding - private const val MIME_TYPE = "video/avc" - private const val MEDIACODEC_TIMEOUT_DEFAULT = 100L + // H.264 Advanced Video Coding + private const val MIME_TYPE = "video/avc" + private const val MEDIACODEC_TIMEOUT_DEFAULT = 100L + + private const val INVALID_BITRATE = + "The provided bitrate is smaller than what is needed for compression " + + "try to set isMinBitRateEnabled to false" + } - private const val INVALID_BITRATE = - "The provided bitrate is smaller than what is needed for compression " + - "try to set isMinBitRateEnabled to false" var isRunning = true suspend fun compressVideo( - index: Int, context: Context, srcUri: Uri, destination: String, @@ -61,7 +63,6 @@ object Compressor { } catch (exception: IllegalArgumentException) { printException(exception) return@withContext Result( - index, success = false, failureMessage = "${exception.message}" ) @@ -88,7 +89,6 @@ object Compressor { if (rotationData.isNullOrEmpty() || bitrateData.isNullOrEmpty() || durationData.isNullOrEmpty()) { // Exit execution return@withContext Result( - index, success = false, failureMessage = "Failed to extract video meta-data, please try again" ) @@ -101,7 +101,7 @@ object Compressor { // Check for a min video bitrate before compression // Note: this is an experimental value if (configuration.isMinBitrateCheckEnabled && bitrate <= MIN_BITRATE) - return@withContext Result(index, success = false, failureMessage = INVALID_BITRATE) + return@withContext Result(success = false, failureMessage = INVALID_BITRATE) //Handle new bitrate value val newBitrate: Int = @@ -132,7 +132,6 @@ object Compressor { } return@withContext start( - index, newWidth!!, newHeight!!, destination, @@ -148,7 +147,6 @@ object Compressor { @Suppress("DEPRECATION") private fun start( - id: Int, newWidth: Int, newHeight: Int, destination: String, @@ -288,9 +286,8 @@ object Compressor { extractor ) - compressionProgressListener.onProgressCancelled(id) + compressionProgressListener.onProgressCancelled() return Result( - id, success = false, failureMessage = "The compression has stopped!" ) @@ -368,7 +365,6 @@ object Compressor { inputSurface.setPresentationTime(bufferInfo.presentationTimeUs * 1000) compressionProgressListener.onProgressChanged( - id, bufferInfo.presentationTimeUs.toFloat() / duration.toFloat() * 100 ) @@ -386,7 +382,7 @@ object Compressor { } catch (exception: Exception) { printException(exception) - return Result(id, success = false, failureMessage = exception.message) + return Result(success = false, failureMessage = exception.message) } dispose( @@ -431,7 +427,6 @@ object Compressor { } } return Result( - id, success = true, failureMessage = null, size = resultFile.length(), @@ -440,7 +435,6 @@ object Compressor { } return Result( - id, success = false, failureMessage = "Something went wrong, please try again" ) diff --git a/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/config/Configuration.kt b/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/config/Configuration.kt index 1e1a84a..a526e41 100644 --- a/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/config/Configuration.kt +++ b/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/config/Configuration.kt @@ -10,7 +10,7 @@ data class Configuration( val keepOriginalResolution: Boolean = false, var videoHeight: Double? = null, var videoWidth: Double? = null, - var videoNames: List + var videoName: String ) data class AppSpecificStorageConfiguration( diff --git a/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Result.kt b/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Result.kt index d1beceb..e26b959 100644 --- a/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Result.kt +++ b/lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Result.kt @@ -1,7 +1,6 @@ package com.abedelazizshe.lightcompressorlibrary.video data class Result( - val index: Int, val success: Boolean, val failureMessage: String?, val size: Long = 0,