chenjim 发布的文章

Android性能优化--Perfetto抓取trace

本文首发地址 https://blog.csdn.net/CSqingchen/article/details/128900541
最新更新地址 https://gitee.com/chenjim/chenjimblog
Perfetto 官方链接地址 https://github.com/google/perfetto/

介绍

Perfetto 是基于 Android 的系统追踪服务, Android的trace跟踪服务在 Android11(R) 之后是默认打开的,但是如果你是 Android 9 ( P ) 或者 10 ( Q ) ,那么就需要手动设置一下相应的 prop 属性。
adb shell setprop persist.traced.enable 1


使用 adb 抓取

adb shell perfetto -o /data/misc/perfetto-traces/trace_file.perfetto-trace -t 60s sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory
-o /data/misc/perfetto-traces/trace_file.perfetto-trace 输出trace的路径
-t 60s 最多抓取时长,可以Ctrl+C停止
sched ... memory 要抓取相关模块
更多配置说明,参见文档:https://perfetto.dev/docs/concepts/config
使用配置文件,通过adb抓取,参考如下
adb push atrace.cfg /data/local/tmp/atrace.cfg
adb shell "cat /data/local/tmp/atrace.cfg | perfetto --txt -c - -o /data/misc/perfetto-traces/trace.perfetto-trace"
atrace.cfg 是配置信息,默认最多只抓取10秒,可以修改更长
可以Ctrl+C停止,需执行adb shell
再执行 cat /data/local/tmp/atrace.cfg | perfetto --txt -c - -o /data/misc/perfetto-traces/trace.perfetto-trace
更多配置参见 https://github.com/google/perfetto/blob/master/test/configs
然后 adb pull /data/misc/perfetto-traces/trace.perfetto-trace ,在 https://perfetto.dev/ 导入文件分析


通过 perfetto 网页抓取

打开 https://ui.perfetto.dev/#!/record
"Add ADB Device" 选择手机设备
如下图,有一些参数配置,根据自己的需要添加修改

最后右上角 "Start Recording"


直接在手机上抓取

开启开发者模式:系统设置--关于手机--连续点击。部分机型可网上搜索开启方式。
进入开发者模式,选择"系统跟踪",里面有一些相关设置类别,可以选择要抓取的数据种类
抓取的trace信息会直接存储在手机 /data/local/traces,"查看跟踪文件",可以分享
此方式比较适合没有电脑,随时抓取


使用 record_android_trace 抓取

执行如下命令,会自动抓取,并打开 https://perfetto.dev/ 分析
python record_android_trace -c atrace.cfg -o out.perfetto.trace
record_android_trace 是 perfetto 中文件,具体参见以上命令的链接
atrace.cfg 是配置信息,更多配置说明参见文档:https://perfetto.dev/docs/concepts/config
默认只抓取10秒,可以修改更长,可以Ctrl+C停止,更多配置参见 https://github.com/google/perfetto/blob/master/test/configs
首次运行会从 googleapis.com 下载文件,如果遇到超时,可能需要修改如下
Windows可参考 Android基于perfetto分析native内存泄露 放文件

diff --git a/tools/heap_profile b/tools/heap_profile
@@ -243,8 +243,9 @@ def download_or_get_cached(file_name, url, sha256):
    if needs_download:
        # Either the filed doesn't exist or the SHA256 doesn't match.
        tmp_path = bin_path + '.tmp'
+    proxy_7890='http://127.0.0.1:7890'
        print('Downloading ' + url)
-    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
+    subprocess.check_call(['curl', '-f', '-L', '-#','-x',proxy_7890, '-o', tmp_path, url])
        with open(tmp_path, 'rb') as fd:
        actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
        if actual_sha256 != sha256:

熟悉 perfetto 快捷键,会有事半功倍效果


注意事项

  • 最好先执行adb shell,然后执行perfetto相关命令,因为在某些情况 Ctrl+C不通过ADB传播而无法停止.
  • 在 Android 12 之前的非 root 设备上,由于过度限制的 SELinux 规则,配置只能通过 (cat config | adb shell perfetto -c -) 传递 。由于 Android 12 /data/misc/perfetto-configs可用于存储配置。
  • 在 Android 10 之前的设备上,adb 无法直接拉取 /data/misc/perfetto-traces. 可以用 adb shell cat /data/misc/perfetto-traces/trace > trace变通。
  • 当捕获较长的跟踪时,例如在基准测试或 CI 的上下文中,使用 PID=$(perfetto --background)然后kill $PID停止。


  • 一个参考配置示例如下
    更多可参见 https://github.com/google/perfetto/blob/master/test/configs
      buffers: {
          size_kb: 707200
          fill_policy: RING_BUFFER
      }
      buffers: {
          size_kb: 707200
          fill_policy: RING_BUFFER
      }
      data_sources: {
          config {
              name: "linux.process_stats"
              target_buffer: 1
              process_stats_config {
                  scan_all_processes_on_start: true
                  proc_stats_poll_ms: 1000
              }
          }
      }
      data_sources: {
          config {
              name: "android.log"
              android_log_config {
              }
          }
      }
      data_sources: {
          config {
              name: "android.surfaceflinger.frametimeline"
          }
      }
      data_sources: {
          config {
              name: "linux.sys_stats"
              sys_stats_config {
                  meminfo_period_ms: 1000
                  vmstat_period_ms: 1000
                  stat_period_ms: 1000
                  stat_counters: STAT_CPU_TIMES
                  stat_counters: STAT_FORK_COUNT
              }
          }
      }
      data_sources: {
          config {
              name: "android.heapprofd"
              target_buffer: 0
              heapprofd_config {
                  sampling_interval_bytes: 4096
                  shmem_size_bytes: 8388608
                  block_client: true
              }
          }
      }
      data_sources: {
          config {
              name: "android.java_hprof"
              target_buffer: 0
              java_hprof_config {
              }
          }
      }
      data_sources: {
          config {
              name: "linux.ftrace"
              ftrace_config {
                  ftrace_events: "sched/sched_switch"
                  ftrace_events: "power/suspend_resume"
                  ftrace_events: "sched/sched_wakeup"
                  ftrace_events: "sched/sched_wakeup_new"
                  ftrace_events: "sched/sched_waking"
                  ftrace_events: "power/cpu_frequency"
                  ftrace_events: "power/cpu_idle"
                  ftrace_events: "sched/sched_process_exit"
                  ftrace_events: "sched/sched_process_free"
                  ftrace_events: "task/task_newtask"
                  ftrace_events: "task/task_rename"
                  ftrace_events: "lowmemorykiller/lowmemory_kill"
                  ftrace_events: "oom/oom_score_adj_update"
                  ftrace_events: "ftrace/print"
                  ftrace_events: "binder/*"
                  atrace_categories: "input"
                  atrace_categories: "gfx"
                  atrace_categories: "view"
                  atrace_categories: "webview"
                  atrace_categories: "camera"
                  atrace_categories: "dalvik"
                  atrace_categories: "power"
                  atrace_categories: "wm"
                  atrace_categories: "am"
                  atrace_categories: "ss"
                  atrace_categories: "sched"
                  atrace_categories: "freq"
                  atrace_categories: "binder_driver"
                  atrace_categories: "aidl"
                  atrace_categories: "binder_lock"
                  atrace_apps: "*"
              }
          }
      }
      duration_ms: 300000
      flush_period_ms: 30000
      incremental_state_config {
          clear_period_ms: 5000
      }
      write_into_file: true

相关文章连接
Android性能优化--Perfetto抓取trace
Android性能优化--perfetto分析native内存泄露
Android性能优化--Perfetto用SQL性能分析

Android性能优化--Perfetto分析native内存泄露

本地首发地址 https://blog.csdn.net/CSqingchen/article/details/128382445
最新更新地址 https://gitee.com/chenjim/chenjimblog
官方文档(可在Chome直接翻译) https://perfetto.dev/docs/data-sources/native-heap-profiler
示例 raw-trace 资源地址 https://download.csdn.net/download/CSqingchen/87321798
本文示例是windows,这里使用了python工具,在Linux和mac同样适用

  1. 首先安装python3环境,参见 https://www.python.org/downloads/

  2. 下载 perfetto ,地址在 https://github.com/google/perfetto
    后面需要用到这里的 perfetto\tools\heap_profile 本文放在了目录 D:\tools\perfetto

  3. 抓取一次某个应用的内存命令如下,注意提前关闭其它adb程序,如AS
    python D:\tools\perfetto\tools\heap_profile -n com.app.package.name

    这里只能抓到一次内存的快照,如果想连续记录多次内存的数据需要能Root手机
    首次运行会有如下联网下载
    Downloading https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v32.1/windows-amd64/traceconv.exe
    如果下载失败,Windows系统请 从连接下载 并参考里面说明放文件。
    或者参考如下,修改tools/heap_profile 中 curl 下载方式

     diff --git a/tools/heap_profile b/tools/heap_profile
     @@ -243,8 +243,9 @@ def download_or_get_cached(file_name, url, sha256):
        if needs_download:
          # Either the filed doesn't exist or the SHA256 doesn't match.
          tmp_path = bin_path + '.tmp'
     +    proxy_7890='http://127.0.0.1:7890'
          print('Downloading ' + url)
     -    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
     +    subprocess.check_call(['curl', '-f', '-L', '-#', '-x', proxy_7890, '-o', tmp_path, url])
          with open(tmp_path, 'rb') as fd:
            actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
          if actual_sha256 != sha256:
  4. 连续抓取多次内存快照
    adb shell killall -USR1 heapprofd 需要Root权限,上一步骤不要停止
    每执行一次,上一步会记录一次
    这里我上传了一份自己抓的数据,下载地址 https://download.csdn.net/download/CSqingchen/87321798

  5. 使用 perfetto 分析抓到的 raw-trace 文件,即从 https://ui.perfetto.dev/ 打开 raw-trace 文件

    通过点击方块,对比不用时刻的内存。
    可以看到第一个大块有内存一直上升,结合其中的栈堆,分析并解决即可。
    有时很小泄露,不容易看出,可以反复很多次操作应用后,对比前后数据
    下载资源 raw-trace.02 是解决问题后,抓取的tarce,可以看到问题已解决

相关问题思考

  • AndroidStudio Profile 也可以Dump内存,但内存分析没有这个直接
  • LeakCanary 也可以分析内存,主要是用来分析View视图,没法分析这个native内存的数据

相关文章
Android性能优化--Perfetto抓取trace
Android性能优化--perfetto分析native内存泄露
Android性能优化--Perfetto用SQL性能分析

  每个人每个时间段都有不同的焦虑,三十岁那年,我最焦虑的是“她在哪”和还能写多久的代码。写代码固然能过好自己的生活,要想让家人都都过上无忧的生活,还是有很长的路。趁着年轻进入创业公司赌一把,成,财富自由,败,也不会更差。四五年经历让我明白,要想成为那1%的创业成功者,还是挺难的。个人观点:能进大公司,一定要进大公司,能不去外包企业不去外包公司,创业一定要深入了解你的合伙人。

  2017年底从通铭公司离职,偶然碰上也从通铭离开的孙老板,然后开始创业之路,成立了H公司,很想把事情做成、做大,结果我们没跳出那99%。在这苦逼的三年中,不仅薪资打折,险些陷入法律纠纷,最大的收获是组建了家庭。

  2020年7月,外派驻场上汽赛可人工智能公司,智能座舱项目的开始规划注定就很难成功,项目结束团队也就散了。

  2021年6月,建勖项老板邀请合伙加入,销售出身,曾是H公司的销售总监,有庞大关系网络和销售能力,看起来够兄弟义气,口头答应可以远程办公,画了一些“大饼”,我就进入了。半年后还是选择了离开,理想总是丰满的,现实很骨感,“兄弟”两字也显得那么苍白。五个开发(Java服务端、Web前端、C#、安卓、外包算法)想做成功一个科技公司很难。入职前的“每周可以在家办公”,变成项老板口中“用屁股想就知道从未有过的约定”,然而研发中只有我配置入职笔记本,“口头承诺千万别信,只相信盖章的白纸黑字”。

  2022年1月底(春节前后)是老婆的预产期,12月中跟项老板提年前周五需要陪老婆产检,老板爽快答应了,提醒我别忘带上电脑写代码,还说要结娃娃亲,转了两千元给我祝福兄弟生二胎,让邮件补一个请假说明带薪不用走OA。1月11日上午11点,老婆说不舒服要去医院,我就跟老板口头请假回去送老婆去医院了,办完手续因我报告未出不准陪护,晚上在家加班到深夜完成需求提交测试版本,第二天赶到医院,在医院观察了两天,并无大问题,13日周四下午四点回到家,跟进已发版本问题,梳理验证后发现主要问题服务端修改下接口就好,就邮件回复了下。14日周五,因为刚出院产检移到下周一17日,我就去公司了,正好有些需要需要当面交流,领导和产品又提了一些UI样式的需求,因为一个偶现的加载中如何显示问题,项老板在工作群批“没有灵魂的代码从业者”,十几年从没这么被领导批过,我跟项老板说一起聊聊,结果他回“在金山客户”。

  1月17日周一,陪老婆产检,回来后加班至深夜开发需求,晚上发一个18号在家办公申请及调假说明。18号晚在家发了一个19号在家办公的申请。19号一早看到老板回我“开发人员禁止在家办公”,吃过饭就去了公司,我约9:30一起聊聊,结果老板9:45途中折回来顺便(还有其他事)给我一根烟的时间,说晚上继续聊,结果晚上陪客户去了。20日周四陪老婆产检,一早给老板电话请假不接,回来后加班至深夜开发需求。21日周五按照之前的请假可以不去,OA申请19-28日陪产假,领导驳回“没有权限批补假”,让找人事,人事回我“出生那天开始才可以请陪产假”,老板电话三次不接。工作是为了生活,家人永远比工作重要。22日周六,完成火烧眉毛的需求并发版后,提交了“离职申请”。老板很快批复,我们知道正常交接是一个月,算上陪产假、年假、育儿假、春节国假,就差不多一个月,“想得美”,1月24日完成交接。这十来天,除了12日在医院未携带电脑,其他每天都在加班,对得起公司,问心无愧,结果这些天按事假不计薪,更狗血的是项老板还找理由我故意留BUG,威胁要起诉我。我退回了二千元,“兄弟”两个字只是有些人对男人的统称。

  2022年1月25日,农历腊月二十三,北方小年,小宝贝祺祺出生,愿一切都是新的开始,愿各位虎年快乐,万事如意~

本文首发地址:https://blog.csdn.net/CSqingchen/article/details/121426660
最新更新地址:https://gitee.com/chenjim/chenjimblog

在 ITelephony.aidl 8.0 源码9.0 源码 中存在 endCall() 接口
10 源码 中,已经没有 endCall() 接口
在 Android 10 之前可以通过如下方式 挂断 电话

//详细 参见   https://www.jianshu.com/p/a5662fad84b5  
public void endCall() {
    try {
        //1.通过类加载器加载相应类的class文件
        //Class<?> forName = Class.forName("android.os.ServiceManager");
        Class<?> loadClass = getClassLoader().loadClass("android.os.ServiceManager");
        //2.获取类中相应的方法
        //name : 方法名
        //parameterTypes : 参数类型
        Method method = loadClass.getDeclaredMethod("getService", String.class);
        //3.执行方法,获取返回值
        //receiver : 类的实例
        //args : 具体的参数
        IBinder invoke = (IBinder) method.invoke(null, Context.TELEPHONY_SERVICE);
        //aidl
        ITelephony iTelephony = ITelephony.Stub.asInterface(invoke);
        //挂断电话
        iTelephony.endCall();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

那 Android 10 如何挂断电话呢?
在 TelephonyManager.java 9.0 源码 10.0 源码 中我们可以看到如下

    /**
     * @removed Use {@link android.telecom.TelecomManager#endCall()} instead.
     * @hide
     * @removed
     */
    @Deprecated
    @SystemApi
    @RequiresPermission(android.Manifest.permission.CALL_PHONE)
    public boolean endCall() {
        return false;
    }

是不是可以使用 android.telecom.TelecomManager#endCall() 呢 ?

看下 TelecomManager.java 源码 9.010.0,可以看到如下

    @RequiresPermission(Manifest.permission.ANSWER_PHONE_CALLS)
    @Deprecated
    public boolean endCall() {
        try {
            if (isServiceConnected()) {
                return getTelecomService().endCall(mContext.getPackageName());
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Error calling ITelecomService#endCall", e);
        }
        return false;
    }

所以问题不大了,最终代码如下

AndroidManifest.xml 需 添加以下权限
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.CALL_PHONE" />

//注意动态申请权限(Permission.ANSWER_PHONE_CALLS)
private fun endCallAction() {
    try {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            val tcm = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
            tcm.endCall()
        } else {
            val loadClass: Class<*> =
                javaClass.classLoader.loadClass("android.os.ServiceManager")
            val method: Method = loadClass.getDeclaredMethod("getService", String::class.java)
            val invoke: IBinder = method.invoke(null, Context.TELEPHONY_SERVICE) as IBinder
            val iTelephony: ITelephony = ITelephony.Stub.asInterface(invoke)
            iTelephony.endCall()
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

这就完了,还没有,我们再看下 TelecomManager.java 中 endcall 注释

     * @deprecated Companion apps for wearable devices should use the {@link InCallService} API
     * instead.  Apps performing call screening should use the {@link CallScreeningService} API instead.
     */
    @RequiresPermission(Manifest.permission.ANSWER_PHONE_CALLS)
    @Deprecated
    public boolean endCall() {   ...   }

@Deprecated @Deprecated @Deprecated
也就是将来也可能会移除,那时怎么做呢?
上面注释中看到可穿戴设备用 InCallService, 其它APP用 CallScreeningService
我们看下 InCallService.java 10.0 源码 和 CallScreeningService.java 10.0 源码
哇,类注释都非常详细,需要时参考下就好,本文暂时结束。

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);