Saturday, 9 February 2019

How can I trim a video from Uri, including files that `mp4parser` library can handle, but using Android's framework instead?

Background

Over the past few days, I've worked on making a customizable, more updated version of a library for video trimming, here (based on this library)

The problem

While for the most part, I've succeeded making it customizable and even converted all files into Kotlin, it had a major issue with the trimming itself.

It assumes the input is always a File, so if the user chooses an item from the apps chooser that returns a Uri, it crashes. The reason for this is not just the UI itself, but also because a library that it uses for trimming (mp4parser) assumes an input of only File (or filepath) and not a Uri (wrote about it here). I tried multiple ways to let it get a Uri instead, but failed. Also wrote about it here.

That's why I used a solution that I've found on StackOverflow (here)for the trimming itself. The good thing about it is that it's quiet short and uses just Android's framework itself. However, it seems that for some video files, it always fails to trim them. As an example of such files, there is one on the original library repository, here (issue reported here).

Looking at the exception, this is what I got:

E: Unsupported mime 'audio/ac3'
E: FATAL EXCEPTION: pool-1-thread-1
    Process: life.knowledge4.videocroppersample, PID: 26274
    java.lang.IllegalStateException: Failed to add the track to the muxer
        at android.media.MediaMuxer.nativeAddTrack(Native Method)
        at android.media.MediaMuxer.addTrack(MediaMuxer.java:626)
        at life.knowledge4.videotrimmer.utils.TrimVideoUtils.genVideoUsingMuxer(TrimVideoUtils.kt:77)
        at life.knowledge4.videotrimmer.utils.TrimVideoUtils.genVideoUsingMp4Parser(TrimVideoUtils.kt:144)
        at life.knowledge4.videotrimmer.utils.TrimVideoUtils.startTrim(TrimVideoUtils.kt:47)
        at life.knowledge4.videotrimmer.BaseVideoTrimmerView$initiateTrimming$1.execute(BaseVideoTrimmerView.kt:220)
        at life.knowledge4.videotrimmer.utils.BackgroundExecutor$Task.run(BackgroundExecutor.java:210)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:458)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)

What I've found

  1. Reported about the issue here. I don't think it will get an answer, as the library hasn't updated in years...
  2. Looking at the exception, I tried to also trim without sound. This works, but it's not a good thing, because we want to trim normally.
  3. Thinking that this code might be based on someone else's code, I tried to find the original one. I've found that it is based on some old Google code on its gallery app, here, in a class called "VideoUtils.java" in package of "Gallery3d". Sadly, I don't see any new version for it. Latest one that I see is of Gingerbread, here.

The code that I've made out of it looks as such:

object TrimVideoUtils {
    private const val DEFAULT_BUFFER_SIZE = 1024 * 1024

    @JvmStatic
    @WorkerThread
    fun startTrim(context: Context, src: Uri, dst: File, startMs: Long, endMs: Long, callback: VideoTrimmingListener) {
        dst.parentFile.mkdirs()
        //Log.d(TAG, "Generated file path " + filePath);
        val succeeded = genVideoUsingMuxer(context, src, dst.absolutePath, startMs, endMs, true, true)
        Handler(Looper.getMainLooper()).post { callback.onFinishedTrimming(if (succeeded) Uri.parse(dst.toString()) else null) }
    }

    //https://stackoverflow.com/a/44653626/878126 https://android.googlesource.com/platform/packages/apps/Gallery2/+/634248d/src/com/android/gallery3d/app/VideoUtils.java
    @JvmStatic
    @WorkerThread
    private fun genVideoUsingMuxer(context: Context, uri: Uri, dstPath: String, startMs: Long, endMs: Long, useAudio: Boolean, useVideo: Boolean): Boolean {
        // Set up MediaExtractor to read from the source.
        val extractor = MediaExtractor()
        //       val isRawResId=uri.scheme == "android.resource" && uri.host == context.packageName && !uri.pathSegments.isNullOrEmpty())
        val fileDescriptor = context.contentResolver.openFileDescriptor(uri, "r")!!.fileDescriptor
        extractor.setDataSource(fileDescriptor)
        val trackCount = extractor.trackCount
        // Set up MediaMuxer for the destination.
        val muxer = MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        // Set up the tracks and retrieve the max buffer size for selected tracks.
        val indexMap = SparseIntArray(trackCount)
        var bufferSize = -1
        try {
            for (i in 0 until trackCount) {
                val format = extractor.getTrackFormat(i)
                val mime = format.getString(MediaFormat.KEY_MIME)
                var selectCurrentTrack = false
                if (mime.startsWith("audio/") && useAudio) {
                    selectCurrentTrack = true
                } else if (mime.startsWith("video/") && useVideo) {
                    selectCurrentTrack = true
                }
                if (selectCurrentTrack) {
                    extractor.selectTrack(i)
                    val dstIndex = muxer.addTrack(format)
                    indexMap.put(i, dstIndex)
                    if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
                        val newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
                        bufferSize = if (newSize > bufferSize) newSize else bufferSize
                    }
                }
            }
            if (bufferSize < 0)
                bufferSize = DEFAULT_BUFFER_SIZE
            // Set up the orientation and starting time for extractor.
            val retrieverSrc = MediaMetadataRetriever()
            retrieverSrc.setDataSource(fileDescriptor)
            val degreesString = retrieverSrc.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
            if (degreesString != null) {
                val degrees = Integer.parseInt(degreesString)
                if (degrees >= 0)
                    muxer.setOrientationHint(degrees)
            }
            if (startMs > 0)
                extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
            // Copy the samples from MediaExtractor to MediaMuxer. We will loop
            // for copying each sample and stop when we get to the end of the source
            // file or exceed the end time of the trimming.
            val offset = 0
            var trackIndex: Int
            val dstBuf = ByteBuffer.allocate(bufferSize)
            val bufferInfo = MediaCodec.BufferInfo()
//        try {
            muxer.start()
            while (true) {
                bufferInfo.offset = offset
                bufferInfo.size = extractor.readSampleData(dstBuf, offset)
                if (bufferInfo.size < 0) {
                    //InstabugSDKLogger.d(TAG, "Saw input EOS.");
                    bufferInfo.size = 0
                    break
                } else {
                    bufferInfo.presentationTimeUs = extractor.sampleTime
                    if (endMs > 0 && bufferInfo.presentationTimeUs > endMs * 1000) {
                        //InstabugSDKLogger.d(TAG, "The current sample is over the trim end time.");
                        break
                    } else {
                        bufferInfo.flags = extractor.sampleFlags
                        trackIndex = extractor.sampleTrackIndex
                        muxer.writeSampleData(indexMap.get(trackIndex), dstBuf,
                                bufferInfo)
                        extractor.advance()
                    }
                }
            }
            muxer.stop()
            return true
            //        } catch (e: IllegalStateException) {
            // Swallow the exception due to malformed source.
            //InstabugSDKLogger.w(TAG, "The source video file is malformed");
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            muxer.release()
        }
        return false
    }

}

The exception is thrown on val dstIndex = muxer.addTrack(format) . For now, I've wrapped it in try-catch, to avoid a real crash.

I tried to search for newer versions of this code (assuming that it got fixed later), but failed.

  1. Searching on the Internet and here, I've found only one similar question, here, but it's not the same at all.

The questions

  1. Is it possible to use Android's framework to trim such problematic files? Maybe there is a newer version of the trimming of the videos code? I'm interested of course only for the pure implementation of video trimming, like the function I wrote above, of "genVideoUsingMuxer" .

  2. As a temporary solution, is it possible to detect problematic input videos, so that I won't let the user start to trim them, as I know they will fail?

  3. Is there maybe another alternative to both of those, that have a permissive license and doesn't bloat the app? For mp4parser, I wrote a separate question, here.



from How can I trim a video from Uri, including files that `mp4parser` library can handle, but using Android's framework instead?

No comments:

Post a Comment