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

1

17

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://mcmap.net/q/746911/-how-to-trim-video-with-mediacodec 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.

Heptachord answered 7/2, 2019 at 12:33 Comment(0)
A
3
  1. Why does it occur?

audio/ac3 is an unsupported mime type.

MediaMuxer.addTrack() (native) calls MPEG4Writer.addSource(), which prints this log message before returning an error.

EDIT

My aim was not to provide an answer to each of your sub-questions, but to give you some insight into the fundamental problem. The library you have chosen relies on the Android's MediaMuxer component. For whatever reason, the MediaMuxer developers did not add support for this particular audio format. We know this because the software prints out an explicit message to that effect, then immediately throws the IllegalStateException mentioned in your question.

Because the issue only involves a particular audio format, when you provide a video-only input, everything works fine.

To fix the problem, you can either alter the library to provide for the missing functionality, or find a new library that better suits your needs. sannies/mp4parser may be one such alternative, although it has different limitations (if I recall correctly, it requires all media to be in RAM during the mastering process). I do not know if it supports ac3 explicitly, but it should provide a framework to which you can add support for arbitrary mime types.

I would encourage you to wait for a more complete answer. There may be far better ways to do what you are trying to do. But it is apparent that the library you are using simply does not support all possible mime types.

Allegro answered 7/2, 2019 at 18:43 Comment(10)
How do you know it's not supported? What can I do about it? Are there any alternatives? How come WhatsApp and Google Photos trim it fine? How come I can play the video fine? It's ok to convert it if needed. Is it possible or maybe there is another alternative solution ? If not, is there any way to detect it from the start, so that I won't have to tell the user that I can't trim only after he took the steps for it?Heptachord
Actually it used it in the original code : github.com/titansgroup/k4l-video-trimmer/blob/develop/… and it crashes with the same file, but I got away from using it, as it required that I give it a File object (or path), even if all I have is a uri which can't be a real File. I even wrote about it here: github.com/sannies/mp4parser/issues/357 . So for the actual trimming I used what the framework already provides... Since I think I wrote a confusing background for this, I've updated the question.Heptachord
Well, right, but it's using MediaMuxer for the writing (which is why you got your error), whereas it could've written via mp4parser, instead.Allegro
I can't use mp4parser as long as I can't figure out how to handle Uri and InputStream and not just File. I really tried. The sample crashed because it couldn't handle Uri (I chose the file using "Files" app of Google...). Do you know how to "enjoy both worlds: (handle the problematic file, and even if we use Uri to get to it) ?Heptachord
Well, that's a separate question that I haven't looked into. But I think you may have missed my point -- you're writing your output to a File. mp4parser works fine with files.Allegro
The output is always a file. I'm talking about the input. The input can be just a Uri that you can open InputStream from. That's because there are many apps that offer a Uri instead of a File, even if behind the scenes it's a File.Heptachord
We're talking at cross-purposes, my friend. I am talking about output. Your crash happened because the class you chose to write the file didn't support a particular mime type. I am simply pointing out that you could've used mp4parser to accomplish the file writing, since your output is a File. The question of how to adapt mp4parser to a URI input is entirely valid, but it is not germaine to this question.Allegro
Again, this is incorrect. I can't use mpparser because it can't handle the input as a Uri instead of a File. The output is always a file, on both cases. It's the same question. I want to trim a video into a file, and I need to handle it whether it's from a File or Uri. The library mp4parser doesn't work with Uri. Only File. If you know how to handle it (without just copying the video into a normal file), please write it.Heptachord
Perhaps it is not clear, but you can read the samples in using MediaExtractor, then write them out using mp4parser. In this way, the question of how to "input" a Uri to mp4parser never comes up.Allegro
Let us continue this discussion in chat.Heptachord

© 2022 - 2024 — McMap. All rights reserved.