分类 音视频 下的文章

MediaCodec编码速度和清晰度均衡(转)

原文地址:https://aijishu.com/a/1060000000079293
本文地址:https://h89.cn/archives/10.html
最新更新地址:https://gitee.com/chenjim/chenjimblog

概述

在安卓平台为了实现h264视频编码,我们通常可以使用libx264, ffmpeg等第三方视频编码库,但是如果对编码的速度有一定的要求,要实现实时甚至超实时的高速视频编码,我们并没有太多选项,只能使用Android提供的MediaCodec硬编码模块。
MediaCodec模块在实际使用中会遇到很多问题,本文主要讨论使用MediaCodec来对OpenGL渲染的画面进行编码视频时,如何达到速度快和画面清晰的均衡。

注意,本文将默认你已经熟悉使用MediaCodec,配合SurfaceTexture进行OpenGL画面编码的基本流程

分析

影响编码速度的因素

出去设备硬件的因素,影响MediaCodec对视频画面进行编码的速度的其他因素并不多,我们实践探索下来主要发现以下几点:

  1. 画面尺寸
    画面尺寸就是要编码的视频画面的宽高了,它直接定义了每一帧要编码的画面数据的大小,所以它也主要决定了Mediacodec在进行每帧编码时的任务量。这个很容易理解。
  2. 帧速率
    帧速率指的是每一秒时长的视频所包含的静止画面的帧数。由于Mediacodec在编码时是以帧为单位,对每一个静止帧画面进行编码,所以,帧速率直接决定了MediaCodec在编码固定时长的视频时需要编码的画面数量。
    比如,同样编码一段10秒钟的视频,在不考虑画面内容和其他因素的影响下,如果帧速率是30帧每秒,那么MediaCodec的编码工作量大致就是帧速率15帧每秒的情况的2倍,所以其耗时也大致是其2倍。
  3. 同步模式下的Time Out设置
    MediaCodec有两种工作模式,同步模式和异步模式,其中异步模式只有在Android 5.0以上的系统才支持。如果要兼容5.0以下的系统,那么就必须用到同步模式,而网上流传的众多对MediaCodec的编码流程讲解(比如经典的bigflake)也都是基于同步模式的。
    那么在这些文档和案例指导下,大家在使用MediaCodec进行编码时,也都使用着大致相同的设置。
    其中,同步模式下,在编码流程中,我们需要调用MediaCodec的dequeueOutputBuffer接口来从队列里获取编码输出缓冲区的内容。 dequeueOutputBuffer接口的第二个参数,timeoutUs是一个以微妙为单位的时间值,它定义了从MediaCodec获取输出缓冲区内容时的超时期限。
    目前流传的文档教程和案例中,大多将该值设置为10000微秒,也就是10毫秒。但是当我们把这个值降低到1毫秒甚至更低时,会发现,MediaCodec整个编码流程的速度会得到大幅度的提升,而基于我们的测试,编码结果也并未因此受到太多可视的影响。
    当然,Android官方文档中也并未对timeoutUs的合适取值范围给出具体的建议,我这里得出的结论是在我们项目实际开发中测试得出,并且其结果满足我们的需求。
    如果你的项目不需要保持对5.0以下系统的支持,我还是建议你使用异步模式来进行编码,这样可以直接规避这种同步等待的问题。
  4. Encoder Profile
    Profile是对视频压缩特性的描述,它主要是定义了编码工具的集合。Profile越高,就说明其采用了越高级的压缩特性。而通常,更高级的压缩方式通常需要耗费更多的压缩时间,也就是说,当profile设置的越高时,其编码耗费的时间往往更多。
    例如,当我们在使用ffmpeg进行编码时,如果我们把编码配置设置为ultra-fast时,profile会被强制设置为baseline。

影响画面清晰度的因素

  1. Encoder Profile Level
    Profile是对视频压缩特性的描述,它主要是定义了编码工具的集合。Profile越高,就说明其采用了越高级的压缩特性,相应的,同样配置下所编码得到的视频文件的清晰度就越高,并且码流越小。
    安卓系统支持的H264编码profile一共有3种
    Baseline ProfileMain ProfileHigh Profile
    其中Baseline是从Android 3.0之后开始支持,据我们测试,Main Profile及更高的Profile的自定义设置时是从Android 7.0之后开始支持,之前版本Android系统调用相应接口对其设置均不起作用。而就算Android7.0以后的系统,也存在部分机型(比如部分OPPO和华为机型)不支持的情况。所以如果你的产品需要保持对主流机型的支持,Profile的可靠选择也只有Baseline了。
  2. Biterate
    Biterate, 即视频码率,是视频数据(视频色彩量、亮度量、像素量)每秒输出的位数。一般用的单位是kbps。
    通常情况下,码率越高则视频清晰度越高。但是由于人肉眼分辨的范围有限,码率设置过高所带来的画面清晰度替身也很难被人眼辨别,所以码率设置过高也没有意义。
    通常情况下,我推荐一个公式来计算如何对编码器设置对应的码率:
    Biterate = Width Height FrameRate * Factor
    其中,Width、Height和FrameRate分别代表视频的宽度,高度和帧速率,而Factor则是一个系数,用来控制码率。
    通常情况下,在网络流媒体使用场景中,可以将Factor设置为0.1~0.2,这样能在保证画面损失不严重的情况下生成出来的视频文件大小较小;在普通本地浏览的使用场景中,可以将Factor设置为0.25~0.5,这样可以保证画面清晰度不会因为编码而造成过多肉眼可见的损失,这样生成出来的视频文件也相对较大;在高清视频处理的使用场景中,可以将Factor设置为0.5以上。
    不过,当视频画面颜色越丰富、画面变化越快时,视频编码需要的码率就更高,如果遇到这种视频画面场景,需要适当提高码率来保证清晰度。
    需要注意的是,大多数安卓机型在对Bitrate的支持都有一个上限,如果设置的Bitrate值超过了上限,可能导致编码器抛出异常,进而编码流程失败。所以在设计Bitrate的值时,需要通过MediaCodecInfo.CodecCapabilities提供的相关接口来检查系统支持的Bitrate上限。
  3. Biterate Mode
    虽然我们可以通过接口为编码器设置相关的码率,但是编码器在编码过程中如果处理我们设置的码率也是有几种不同的模式的。

    • BITRATE_MODE_CQ

    忽略用户设置的码率,由编码器自己控制码率,并尽可能保证画面清晰度和码率的均衡。

    • BITRATE_MODE_CBR

    无论视频的画面内容如果,尽可能遵守用户设置的码率

    • BITRATE_MODE_VBR

    尽可能遵守用户设置的码率,但是会根据帧画面之间运动矢量(通俗理解就是帧与帧之间的画面变化程度)来动态调整码率,如果运动矢量较大,则在该时间段将码率调高,如果画面变换很小,则码率降低。
    所以,我们在设置码率的同时,也要注意对Bitrate Mode的设置。不同的设置对于生成出来的视频文件的大小和清晰度的影响都是不同的。

  4. 时间戳和帧速率
    我们知道,在使用MediaCodec对OpenGL渲染内容进行编码的过程中,可以通过设置MediaFormat的MediaFormat.KEY_FRAME_RATE字段来设置编码器的帧速率;也可以通过设置MediaCodec.BufferInfo对象的presentationTimeUs值来设置每一帧画面的时间戳。但是通常会忽略,我们在运行完OpenGL相关绘制命令,在调用swapbuffer之前需要调用eglPresentationTimeANDROID接口来设置当前帧的时间戳。
    如果没有调用eglPresentationTimeANDROID来设置当前帧的时间戳,只设置了MediaCodec.BufferInfo对象的presentationTimeUs,编码产生的视频在播放时不会有任何时间上的异常,所以很多开发者往往忽略了eglPresentationTimeANDROID接口的调用。但实际上,如果不调用eglPresentationTimeANDROID,编码出的视频在清晰度上会有额外的损失。
    由于MediaCodec的设计是面向实时视频画面流编码的使用场景,所以MediaCodec会根据用户向其输入画面的速度来对编码的速度进行调节。如果我们不通过eglPresentationTimeANDROID来在编码之前对画面的时间戳进行设置,那么MediaCodec往往会将我们向其输入画面的速度默认为实时速度,来对编码速度进行调节。这种调节会造成码率降低,视频画面清晰度降低。
    当我们通过eglPresentationTimeANDROID在编码之前对画面的时间戳进行了正确的设置,MediaCodec会以实际每帧的时间戳为依据来对编码素材进行调节。这样可以保证其不会对码率进行额外的降低,保证画面清晰度设置生效。

解决方案

结合以上的分析,为了当我们在使用MediaCodec进行视频编码时,为了达到编码速度和画面清晰度的平衡,我们需要在通过几个方面进行综合的优化。

1. Profile方法

综合上文的介绍,为了保证app在各个Android机型上的完美适配,其实我们在Profile方面能做的选择不多,最安全的情况是将Profile设置为Baseline。

2. Bitrate方法

Bitrate的设置我推荐使用Biterate = Width Height FrameRate * Factor的公式结合产品的使用场景进行设置。

3. Biterate Mode方法

Biterate Mode的默认设置是BITRATE_MODE_VBR,我推荐在系统和机型支持的情况下尽量将Biterate Mode设置为BITRATE_MODE_CQ。
在BITRATE_MODE_CQ情况下,编码器自身对码率和编码速度的调节往往能达到理想的效果,生成的视频文件不至于过多,但是画面清晰度优秀。
当然也要注意做好系统和机型的适配,进行异常处理,因为在某些机型上可能出现不支持BITRATE_MODE_CQ而导致MediaCodec在configure方法时失败,那么此时,我们需要回退到系统默认的Biterate Mode了。

4. 时间戳正确设置

我们在完成每帧OpenGL画面渲染,调用swapbuffer前一定要先调用eglPresentationTimeANDROID方法来正确设置该帧的时间戳。
eglPresentationTimeANDROID默认并不直接暴露,我们在包含相应头文件前先定义拓展宏来载入该函数。下面是载入eglPresentationTimeANDROID函数的相关代码:

#define EGL_EGLEXT_PROTOTYPES
#include <EGL/eglext.h>

eglPresentationTimeANDROID(mDisplay, mSurface, (EGLnsecsANDROID) time); // 在调用eglSwapBuffers先正确设置当前时间戳
eglSwapBuffers(mDisplay, mSurface);

@[toc]

安卓 webrtc 开启 h264 软编解码

本文首发地址 https://blog.csdn.net/CSqingchen/article/details/120199702
最新更新地址 https://gitee.com/chenjim/chenjimblog
本文基于libmediasoupclient 3.2.0 和 webrtc branch-heads/4147(m84)
本文得熟悉相关基础,参考 文1文2
除了需要加编译参数 rtc_use_h264=true, 还需要以下修改以支持h264软编解码
网络搜索到的有很多复制、粘贴,缺少部分内容的,或者版本环境不匹配,很让人头疼。。。。
  1. 修改 ffmpeg_generated.gni 开启 openh264 编解码
    安卓平台默认未支持 h264 解码,会显示黑屏
    third_party/ffmpeg/ffmpeg_generated.gni 中我们可以开到,默认未支持安卓平台
    修改 ffmpeg_generated.gniuse_linux_config,添加 || is_android以支持,结果如下
    use_linux_config = is_linux || is_fuchsia || is_android
    参考自 https://www.codeleading.com/article/3215969775
    而对于软编解码,android webrtc采用openh264 +ffmpeg,这两块的代码都在webrtc源码src/third_party当中
  2. 增加 codec_list parser_list h264 支持
    third_party/ffmpeg/chromium/config/Chrome/android/arm64/libavcodec/parser_list.c 中添加 &ff_h264_parser,
    third_party/ffmpeg/chromium/config/Chrome/android/arm64/libavcodec/codec_list.c 中添加 &ff_h264_decoder,
    third_party/ffmpeg/chromium/config/下有 Chrome ChromeOS Chromium,这里用的是 Chrome,(其它可能有问题。。。)
    需要在编译参数添加 ffmpeg_branding="Chrome"
    这里路径 android/arm64 是需要配合参数 --arch "arm64-v8a",
    如果是 armeabi-v7a 需要同步修改目录 android/arm-neon/libavcodec下内容,
    最终编译参数参考:
    ./tools_webrtc/android/build_aar.py --extra-gn-args 'rtc_use_h264=true ffmpeg_branding="Chrome" rtc_enable_protobuf=false use_rtti=true use_custom_libcxx=false' --arch "arm64-v8a"
  3. 增加文件 LibH264Decoder.java
    复制 sdk/android/api/org/webrtc/LibvpxVp8Decoder.java为同目录 LibH264Decoder.java,修改后如下:

    package org.webrtc;
    public class LibH264Decoder extends WrappedNativeVideoDecoder {
      @Override
      public long createNativeVideoDecoder() {
     return nativeCreateDecoder();
      }
      static native long nativeCreateDecoder();
    }
  4. 增加文件 LibH264Encoder.java
    复制 sdk/android/api/org/webrtc/LibvpxVp8Encoder.java 为同目录 LibH264Encoder.java,修改后如下:

    package org.webrtc;
    public class LibH264Encoder extends WrappedNativeVideoEncoder {
      @Override
      public long createNativeVideoEncoder() {
     return nativeCreateEncoder();
      }
      static native long nativeCreateEncoder();
      @Override
      public boolean isHardwareEncoder() {return false;}
    }
    
  5. 增加 h264_codec.cc
    复制 sdk/android/src/jni/vp8_codec.cc 为同目录 h264_codec.cc,修改后如下:

    #include <jni.h>
    #include "modules/video_coding/codecs/h264/include/h264.h"
    #include "sdk/android/generated_libH264_jni/LibH264Decoder_jni.h"
    #include "sdk/android/generated_libH264_jni/LibH264Encoder_jni.h"
    #include "sdk/android/src/jni/jni_helpers.h"
    namespace webrtc {
    namespace jni {
    static jlong JNI_LibH264Encoder_CreateEncoder(JNIEnv* jni) {
      return jlongFromPointer(H264Encoder::Create().release());
    }
    static jlong JNI_LibH264Decoder_CreateDecoder(JNIEnv* jni) {
      return jlongFromPointer(H264Decoder::Create().release());
    }
    }  // namespace jni
    }  // namespace webrtc
  6. 修改H264 Create()
    添加上面要用到的 H264Encoder::Create(),修改 diff 如下:

    --- a/modules/video_coding/codecs/h264/h264.cc
    +++ b/modules/video_coding/codecs/h264/h264.cc
    @@ -84,6 +84,16 @@ std::vector<SdpVideoFormat> SupportedH264Codecs() {
                         "0")};
     }
    
    +std::unique_ptr<H264Encoder> H264Encoder::Create() {
  7. RTC_LOG(LS_INFO) << "Creating H264EncoderImpl.";
  8. return std::make_unique(cricket::VideoCodec("H264"));
    +#else
  9. RTC_NOTREACHED();
  10. return nullptr;
    +#endif
  11. }
    +
    std::unique_ptr H264Encoder::Create(
    const cricket::VideoCodec& codec) {
    RTC_DCHECK(H264Encoder::IsSupported());

    --- a/modules/video_coding/codecs/h264/include/h264.h
    +++ b/modules/video_coding/codecs/h264/include/h264.h
    @@ -43,6 +43,7 @@ std::vector SupportedH264Codecs();

    class RTC_EXPORT H264Encoder : public VideoEncoder {
    public:

  12. static std::unique_ptr Create();
    static std::unique_ptr Create(const cricket::VideoCodec& codec);
    // If H.264 is supported (any implementation).
    static bool IsSupported();

  13. 修改 sdk/android/BUILD.gn
    将其中 vp8 相关地方复制添加 h264 相关,我的一份修改 diff 如下,若编译有问题或者无效,需仔细核对此处。。。

    @@ -45,6 +45,7 @@ if (is_android) {
        ":java_audio_device_module_java",
        ":libjingle_peerconnection_java",
  14. ":libH264_java",

    ":libvpx_vp8_java",
    ":libvpx_vp9_java",
    ":logging_java",

    @@ -489,6 +490,20 @@ if (is_android) {
    ]
    }

  15. rtc_android_library("libH264_java") {
  16. visibility = [ "*" ]
  17. sources = [
  18. "api/org/webrtc/LibH264Decoder.java",
  19. "api/org/webrtc/LibH264Encoder.java",
  20. ]
  21. deps = [
  22. ":base_java",
  23. ":video_api_java",
  24. ":video_java",
  25. "//rtc_base:base_java",
  26. ]
  27. }
    +
    rtc_android_library("libvpx_vp9_java") {
    visibility = [ "*" ]
    sources = [
    @@ -512,6 +527,7 @@ if (is_android) {

    deps = [

    ":base_java",
  28. ":libH264_java",

    ":libvpx_vp8_java",
    ":libvpx_vp9_java",
    ":video_api_java",

    @@ -783,6 +799,18 @@ if (current_os == "linux" || is_android) {
    ]
    }

  29. rtc_library("libH264_jni") {
  30. visibility = [ "*" ]
  31. allow_poison = [ "software_video_codecs" ]
  32. sources = [ "src/jni/h264_codec.cc" ]
  33. deps = [
  34. ":base_jni",
  35. ":generated_libH264_jni",
  36. ":video_jni",
  37. "../../modules/video_coding:webrtc_h264",
  38. ]
  39. }
    +
    rtc_library("libvpx_vp9_jni") {
    visibility = [ "*" ]
    allow_poison = [ "software_video_codecs" ]
    @@ -799,6 +827,7 @@ if (current_os == "linux" || is_android) {
    visibility = [ "*" ]
    allow_poison = [ "software_video_codecs" ]
    deps = [
  40. ":libH264_jni",

    ":libvpx_vp8_jni",
    ":libvpx_vp9_jni",

    ]
    @@ -1203,6 +1232,16 @@ if (current_os == "linux" || is_android) {
    jni_generator_include = "//sdk/android/src/jni/jni_generator_helper.h"
    }

  41. generate_jni("generated_libH264_jni") {
  42. sources = [
  43. "api/org/webrtc/LibH264Decoder.java",
  44. "api/org/webrtc/LibH264Encoder.java",
  45. ]
    +
  46. namespace = "webrtc::jni"
  47. jni_generator_include = "//sdk/android/src/jni/jni_generator_helper.h"
  48. }
    +
    generate_jni("generated_libvpx_vp9_jni") {
    sources = [

    "api/org/webrtc/LibvpxVp9Decoder.java",
  49. 修改 SoftwareVideoDecoderFacoty.java 和 SoftwareVideoEncoderFacoty.java
    需要 分别注册 H.264 并添加创建 codec 的代码, 我的修改 DIFF 如下

    --- a/sdk/android/api/org/webrtc/SoftwareVideoDecoderFactory.java
    +++ b/sdk/android/api/org/webrtc/SoftwareVideoDecoderFactory.java
    @@ -26,6 +26,10 @@ public class SoftwareVideoDecoderFactory implements VideoDecoderFactory {
    @Nullable
    @Override
  50. if (codecType.getName().equalsIgnoreCase("H264")) {
  51. return new LibH264Decoder();
  52. }
    +
    if (codecType.getName().equalsIgnoreCase("VP8")) {

    return new LibvpxVp8Decoder();

    }
    @@ -45,6 +49,9 @@ public class SoftwareVideoDecoderFactory implements VideoDecoderFactory {
    List codecs = new ArrayList();

    codecs.add(new VideoCodecInfo("VP8", new HashMap<>()));
    +

  53. codecs.add(new VideoCodecInfo("H264", new HashMap<>()));
    +
    if (LibvpxVp9Decoder.nativeIsSupported()) {

    codecs.add(new VideoCodecInfo("VP9", new HashMap<>()));

    }

    --- a/sdk/android/api/org/webrtc/SoftwareVideoEncoderFactory.java
    +++ b/sdk/android/api/org/webrtc/SoftwareVideoEncoderFactory.java
    @@ -19,6 +19,9 @@ public class SoftwareVideoEncoderFactory implements VideoEncoderFactory {
    @Nullable
    @Override
    public VideoEncoder createEncoder(VideoCodecInfo info) {

  54. if (info.name.equalsIgnoreCase("H264")) {
  55. return new LibH264Encoder();
  56. }
    if (info.name.equalsIgnoreCase("VP8")) {

    return new LibvpxVp8Encoder();

    }
    @@ -38,6 +41,9 @@ public class SoftwareVideoEncoderFactory implements VideoEncoderFactory {
    List codecs = new ArrayList();

    codecs.add(new VideoCodecInfo("VP8", new HashMap<>()));
    +

  57. codecs.add(new VideoCodecInfo("H264", new HashMap<>()));
    +
    if (LibvpxVp9Encoder.nativeIsSupported()) {

    codecs.add(new VideoCodecInfo("VP9", new HashMap<>()));

    }

  58. 编译使用吧,祝好运。。。

本文参考自 webrtc M75支持android安卓H264软编解遇到的一些坑
在其基础补充、完善部分说明和详细修改结果,如有问题欢迎反馈


其它相关文档

@[toc]

安卓mediasoup输出H264流(支持H264编码)

本文首发地址 https://blog.csdn.net/CSqingchen/article/details/120218832
最新更新地址 https://gitee.com/chenjim/chenjimblog
首先得让mediasoup支持H264编解码,参见 前文

默认视频编码是VP8源码分析

相关源码流程、注释如下

//文件 RoomClient.java 中 
@WorkerThread
private void joinImpl() {
    mMediasoupDevice = new Device();
    //从服务端获取编解码能力
    String routerRtpCapabilities = mProtoo.syncRequest("getRouterRtpCapabilities");
    //最终会调用到 libmediasoupclient/src/Device.cpp 中  Device::Load 
    mMediasoupDevice.load(routerRtpCapabilities);
}

//文件 libmediasoupclient/src/Device.cpp 中 
void Device::Load(json routerRtpCapabilities, const PeerConnection::Options* peerConnectionOptions){
  ...
  //将设备编码能力和服务端编码能力匹配
  this->extendedRtpCapabilities = ortc::getExtendedRtpCapabilities(nativeRtpCapabilities, routerRtpCapabilities);
  ...
}

//最终视频编码方案代码在  文件 libmediasoupclient/src/Transport.cpp 中 
SendTransport::SendTransport(...){
    ...
    auto sendingRtpParametersByKindV= ortc::getSendingRtpParameters("video", *extendedRtpCapabilities);
}

//查看 libmediasoupclient/src/ortc.cpp 中 getSendingRtpParameters 我们可以看到如下
json getSendingRtpParameters(const std::string& kind, const json& extendedRtpCapabilities){
  ...
  for (const auto& extendedCodec : extendedRtpCapabilities["codecs"]){
    ...
    //找到一个编码器,就 break,跳出了
    // NOTE: We assume a single media codec plus an optional RTX codec.
        break;
  }
  ...
}

修改支持H264编码

通过上节分析,我们需要使 routerRtpCapabilities"mimeType": "video/H264", 字段靠前
可以在 RoomClient.java 中修改,也可以在 Device.cppDevice::Load 修改
以下是用后者的修改方案,只保留H264,参考自 mediasoup支持h264

--- a/src/Device.cpp
+++ b/src/Device.cpp
@@ -55,9 +55,25 @@ namespace mediasoupclient
      if (this->loaded)
              MSC_THROW_INVALID_STATE_ERROR("already loaded");

+     auto &remoteCaps = routerRtpCapabilities["codecs"];
+     std::string mimeTypeH264 = "video/H264";
+     std::transform(mimeTypeH264.begin(), mimeTypeH264.end(), mimeTypeH264.begin(), ::tolower);
+     for (nlohmann::json::iterator itr=remoteCaps.begin(); itr!= remoteCaps.end();) {
+         nlohmann::json tmp = *itr;
+         std::string tmpCodec = tmp["mimeType"];
+         std::transform(tmpCodec.begin(), tmpCodec.end(), tmpCodec.begin(), ::tolower);
+         if (tmpCodec != mimeTypeH264 && tmp["kind"] == "video") {
+             itr = remoteCaps.erase(itr);
+         } else {
+             itr++;
+         }
+     }
+
      // This may throw.
      ortc::validateRtpCapabilities(routerRtpCapabilities);

到这里,SDP已经支持H264编码,最终还得依赖设备的H264编码能力,参考 前文

我们可以在 https://v3demo.mediasoup.org 看到设备推出流的编码信息,如下图


其它相关文档