Android MediaCodec以异步模式进行编解码

23

我正在尝试从文件中解码视频并使用新的异步模式(Asynchronous Mode),在API Level 21及以上(安卓5.0 Lollipop)中支持的MediaCodec将其编码为不同的格式。

有许多关于在站点如Big Flake、Google的Grafika和StackOverflow上执行同步模式(Synchronous Mode)操作的示例,但是它们都不支持异步模式。

我不需要在过程中显示视频。

我相信一般的流程是用MediaExtractor读取文件作为MediaCodec(解码器)的输入,在解码器输出时渲染到一个Surface中,该表面也是共享输入进入MediaCodec(编码器),最后通过MediaMuxer写入编码器输出文件。这个Surface是在设置编码器时创建并与解码器共享的。

我可以将视频解码到TextureView中,但将Surface与编码器共享而不是显示屏并不成功。

我为我的两个编解码器设置了 MediaCodec.Callback(),但我认为问题在于我不知道如何在编码器回调的 onInputBufferAvailable() 函数中执行操作。我不知道该如何将数据从 Surface 复制到编码器中 - 这应该自动发生(就像使用 codec.releaseOutputBuffer(outputBufferId, true); 在解码器输出上所做的那样)。然而,我认为在 onInputBufferAvailable 中需要调用 codec.queueInputBuffer 才能正常工作。我只是不知道如何设置参数,而不必像解码时使用 MediaExtractor 之类的东西来获取数据。

如果您有一个示例可以打开视频文件,使用异步的 MediaCodec 回调解码并将其编码为不同的分辨率或格式,然后保存为文件,请分享您的代码。

=== 编辑 ===

这里有一个在同步模式下运行的示例,它展示了我正在尝试在异步模式下完成的内容:ExtractDecodeEditEncodeMuxTest.java:https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/ExtractDecodeEditEncodeMuxTest.java 这个示例在我的应用程序中可以正常工作。

Android MediaCodec


Ping,你看到 https://github.com/mstorsjo/android-decodeencodetest 上的完整示例了吗?对你有用吗?还是你觉得它还有不足之处? - mstorsjo
我对 @mstorsjo 的工作印象深刻!你好像已经将我开始使用的非弃用的 async MediaCodec.Callback 方法转换到了编码器和解码器中,正如我所期望的。看起来你的所有更改都限于 “ExtractDecodeEditEncodeMuxTest.java” 文件。在我实施之后,我会再回复你。谢谢。 - David Manpearl
是的,大部分更改都限于此文件。不过我还需要对InputSurface.java进行一些小的更改(请参见https://github.com/mstorsjo/android-decodeencodetest/commit/0cad97666a278c88a751400a1fdfcdb054b52662),以及对OutputSurface.java进行一项更改(https://github.com/mstorsjo/android-decodeencodetest/commit/4053871ac53807b95c477403403cd2226b5b6a50),但在将解码器回调移动到不同线程之后(https://github.com/mstorsjo/android-decodeencodetest/commit/23a0621390404785e02a1ae7c24dfb67f9854129),这个更改就不再必要了。 - mstorsjo
再次感谢 @mstorsjo。您知道为什么最后一帧没有被处理吗?每次都会触发此操作:assertEquals("encoded and decoded video frame counts should match", mVideoDecodedFrameCount, mVideoEncodedFrameCount); 因为编码计数始终比解码计数少一个。 - David Manpearl
嗯,不,我从未在我的设备上看到过这个问题,但是现在重新检查时,我认为在信号流结束时存在一个小的竞争条件。请参见 https://github.com/mstorsjo/android-decodeencodetest/commit/8e3fa5ac4aba64697db01fcaeaf0ab9c67ee8503 以获取可能的修复方案。 - mstorsjo
2个回答

19
我认为你不需要在编码器的onInputBufferAvailable()回调中做任何事情 - 你不应该调用encoder.queueInputBuffer()。就像在同步模式下进行表面输入编码时,您从未手动调用encoder.dequeueInputBuffer()encoder.queueInputBuffer()一样,在异步模式下也不应该这样做。

当您调用decoder.releaseOutputBuffer(outputBufferId, true);(在同步和异步模式下),这会在内部(使用您提供的Surface)从表面中出队一个输入缓冲区,将输出呈现到其中,并将其重新排队到表面(到编码器)。同步和异步模式之间唯一的区别是公共API中暴露的缓冲区事件的方式,但是在使用表面输入时,它使用了一个不同的(内部)API来访问相同的内容,因此同步与异步模式对此没有影响。

因此根据我所知(尽管我自己没有尝试过),您应该只需将编码器的onInputBufferAvailable()回调留空即可。

编辑: 所以,我自己尝试了一下,结果几乎就像上面描述的那么简单。

如果直接将编码器输入表面配置为解码器的输出(没有介于其间的SurfaceTexture),则只需使用同步解码-编码循环转换为异步即可。

但是,如果您使用SurfaceTexture,则可能会遇到一些小问题。与调用线程相关的等待帧到达SurfaceTexture的方式存在问题,请参见https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/DecodeEditEncodeTest.java#106https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/EncodeDecodeTest.java#104以及https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/OutputSurface.java#113了解详细信息。

我发现问题在于awaitNewImage函数,例如在https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/OutputSurface.java#240中。如果onFrameAvailable回调应在主线程上调用,那么如果awaitNewImage调用也在主线程上运行,则会出现问题。如果onOutputBufferAvailable 回调也在主线程上调用并且从那里调用awaitNewImage,则会出现问题,因为您将等待无法运行的回调(使用wait()阻塞整个线程),直到当前方法返回。

因此,我们需要确保onFrameAvailable回调位于与调用awaitNewImage的线程不同的线程上。一种相

    private HandlerThread mHandlerThread = new HandlerThread("CallbackThread");
    private Handler mHandler;
...
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper());
...
        mSurfaceTexture.setOnFrameAvailableListener(this, mHandler);
我希望这足以帮助您解决问题,如果您需要我编辑公共示例以实现异步回调,请告诉我。
编辑2: 此外,由于GL渲染可能是从onOutputBufferAvailable回调中完成的,所以这可能是一个不同于设置EGL上下文的线程。因此,在这种情况下,需要在设置它的线程中释放EGL上下文,如下所示:
mEGL.eglMakeCurrent(mEGLDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);

并在渲染之前将其重新附加到另一个线程中:

mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);

编辑3: 此外,如果编码器和解码器回调在同一个线程上接收,则进行呈现的解码器onOutputBufferAvailable可能会阻止传递编码器回调。 如果未传递它们,则渲染可以被无限期地阻塞,因为编码器不会返回输出缓冲区。 可以通过确保视频解码器回调在不同的线程上接收来解决此问题,这避免了使用onFrameAvailable回调的问题。

我尝试在ExtractDecodeEditEncodeMuxTest之上实现所有这些,并且似乎已经成功运行,可以查看https://github.com/mstorsjo/android-decodeencodetest。 我最初导入了未更改的测试,并分别对异步模式和繁琐细节进行了修复,以便轻松查看提交日志中的各个修复。


这是我见过的关于编码最全面的答案,其中包含了很多在其他一些猴子香蕉示例中被忽略的细节。多线程GL上下文切换的细节尤其先进 - 总是要确保在创建资源的同一线程上释放GL资源。如果忽略了这些细节,可能会导致ANR、OOM、NPE、Zombies等问题。 - Dominic Cerisano
感谢@mstorsjo的回答。请问在异步模式下,如何为mediacodec生成的每个帧添加演示时间呢?谢谢。 - gbenroscience
演示文稿时间戳在异步模式和同步模式下的处理方式完全相同。上面链接的示例包含了处理它所需的一切。 - mstorsjo
1
这不仅适用于异步情况。如果您只发送原始编码器输出,则其中没有时间戳。编码器的输出与包含时间戳的BufferInfo对象一起传输,然后由容器(例如mp4文件)处理时间戳。如果您不使用BufferInfo对象的presentationTime字段,并且不将BufferInfo对象传递给MediaMuxer等,则信息将丢失。 - mstorsjo
1
25fps的默认值只是在没有其他信息时显示的默认值。对于没有时间信息的原始帧,也没有关于帧速率的信息 - 它只是一堆帧。 - mstorsjo
显示剩余6条评论

1
可以在MediaEncoder中设置Handler。
---> AudioEncoderCallback(aacSamplePreFrameSize),mHandler);

MyAudioCodecWrapper myMediaCodecWrapper;

我的音频编解码器封装器myMediaCodecWrapper;
public MyAudioEncoder(long startRecordWhenNs){
    super.startRecordWhenNs = startRecordWhenNs;
}

@RequiresApi(api = Build.VERSION_CODES.M)
public MyAudioCodecWrapper prepareAudioEncoder(AudioRecord _audioRecord , int aacSamplePreFrameSize)  throws Exception{
    if(_audioRecord==null || aacSamplePreFrameSize<=0)
        throw new Exception();

    audioRecord = _audioRecord;
    Log.d(TAG, "audioRecord:" + audioRecord.getAudioFormat() + ",aacSamplePreFrameSize:" + aacSamplePreFrameSize);

    mHandlerThread.start();
    mHandler = new Handler(mHandlerThread.getLooper());

    MediaFormat audioFormat = new MediaFormat();
    audioFormat.setString(MediaFormat.KEY_MIME, MIMETYPE_AUDIO_AAC);
    //audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE );
    audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
    audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, audioRecord.getSampleRate());//44100
    audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, audioRecord.getChannelCount());//1(單身道)
    audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
    audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16384);
    MediaCodec codec = MediaCodec.createEncoderByType(MIMETYPE_AUDIO_AAC);
    codec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    codec.setCallback(new AudioEncoderCallback(aacSamplePreFrameSize),mHandler);
    //codec.start();

    MyAudioCodecWrapper myMediaCodecWrapper = new MyAudioCodecWrapper();
    myMediaCodecWrapper.mediaCodec = codec;

    super.mediaCodec = codec;

    return myMediaCodecWrapper;

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接