LiTr:适用于Android的轻量级视频/音频转码器

2020-01-09 17:27:01 浏览数 (1)

如果一张图片胜过千言万语,那么视频呢?

文 / Izzat Bahadirov

译 / 屈健宁

原文:https://engineering.linkedin.com/blog/2019/litr-a-lightweight-video-audio-transcoder-for-android

在2017年,我们启动了视频共享功能,使我们的会员能够通过LinkedIn移动应用程序或Web浏览器在feed上共享视频内容。从Android设备发布视频时,成员可以使用其设备摄像头应用程序录制视频,也可以从图库中选择已经拍摄好的视频。上传后,视频将被转换为消费格式,并作为更新显示在Feed中。

该功能成功启动并且开始流行后,我们立即着手进行性能改进。由于视频是庞大的数据消耗,因此任何性能提升都将显著地改善用户体验。我们首先假设用户最有可能直接从他们捕获的移动设备上分享内容。这使我们将重点放在查看典型的捕获参数上。

当时,Android摄像机的开箱即用视频录制分辨率约为720至1080p,比特率为12至17 Mbps。这与720p / 5Mbps的最高格式有很大不同,因为我们实际上创建了很多字节发送到后端,然后被服务器转码丢弃。

解决这种“丢弃数据”问题的方法很简单:在通过网络发送视频之前,先对设备上的视频进行转码以丢弃这些字节。为此,我们需要一个设备上代码转换器。我们在android-transcoder中发现了一个开源的解决方案,该解决方案在Android上执行了基本的硬件加速视频/音频转码。但是,当我们预测需要实现的更改时,我们意识到它将需要使用API中断进行大量重写。

此外,我们希望能够修改android-transcoder无法做到的视频帧。我们决定从头开始编写一个库,并在完成后与android-transcoder项目进行协作。 android-transcoder及其分支(由selsamman,MP4Composer-android,Transcoder进行编辑)的流行表明,Android媒体社区中需要视频/音频转码/修改工具。因此,LiTr诞生了。

今年秋天,在开源之后不久,我在Demuxed 2019大会上介绍了LiTr。在这篇文章中,我将对该演讲进行高层概述,包括我们如何构建LiTr架构,如何使用它来转换媒体以及为什么我们选择MediaCodec来访问硬件编码器。请参阅此处以录制谈话内容。

介绍

在Android上可以使用软件或硬件编码器进行转码。软件编码器(例如ffmpeg的Android端口)提供了多种受支持的编解码器和容器,并具有执行编辑操作(合并/拆分视频,合并/解复用轨道,修改帧等)的功能。但是,它们可能会消耗大量电池和CPU。硬件编码器的编解码器选择有限,但性能和功率效率更高。

经过一些实验,我们得出的结论是,硬件编码器将更适合我们的需求和约束。我们的用例非常简单:降低视频分辨率和/或其比特率,以减少“丢弃”多余的像素。使用硬件编码器将提供实时帧速率并降低电池消耗,这是移动设备用户体验的两个重要考虑因素。在格式兼容性方面,我们认为存在一定的风险,但风险很低。成员通常选择共享可以在其设备上播放的视频,这意味着它们可以被解码。而且由于大多数Android设备都以H.264压缩方式录制视频,因此我们可以使用该编解码器对视频进行编码。

适用于Android的轻量级硬件加速视频/音频转码器,或简称LiTr。

媒体编解码器(MediaCodec)媒体编解码器(MediaCodec)

为了访问编码器硬件,LiTr使用Android的MediaCodec API。而要使用MediaCodec,客户端必须首先请求框架来创建它的实例。例如,客户端可以告诉框架它需要一个用于“ video / avc”的解码器,此时,如果不支持该格式,则系统可以返回MediaCodec的新实例或null。创建编解码器实例后,必须为其配置一组参数,例如分辨率,比特率,帧速率等。如果不支持所需的参数(例如,如果我们尝试解码4K视频,则配置可能会失败)在不支持4K分辨率的硬件上)。创建并配置MediaCodec实例后,就可以启动它并将其用于处理帧。

当客户端连续在MediaCodec上向缓冲区加载数据并接收回缓冲区时,使用缓冲区队列与MediaCodec实例进行交互:

  1. 客户端从MediaCodec中使输入缓冲区出队,并在可用时接收。
  2. 客户端用帧数据填充缓冲区,并将其连同元数据(起始索引,字节数,帧显示时间,标志)一起释放回MediaCodec。
  3. MediaCodec处理数据。
  4. 客户端使MediaCodec的输出缓冲区出队,并在可用时接收一个缓冲区。
  5. 客户端使用输出数据并将缓冲区释放回MediaCodec。
媒体编解码器(MediaCodec)过程示意图媒体编解码器(MediaCodec)过程示意图

重复该过程,直到处理完所有帧。客户端不拥有缓冲区,使用完缓冲区后必须将其释放回MediaCodec。否则,在某些时候,所有出队尝试将始终失败。当不再需要MediaCodec实例时,它将停止并释放它。

使用MediaCodec进行转码

要进行代码转换,我们将需要两个MediaCodec实例:一个作为解码器运行,另一个作为编码器运行。解码器使用并解码已编码的源帧。例如,视频解码器将采用H.264编码的视频帧并将其解码为像素,而音频解码器会将压缩的AAC音频帧解码为未压缩的PCM帧。然后,编码器使用已解码的帧,以生成所需目标格式的编码帧。例如,将使用视频压缩编解码器(例如H.264或VP9)对视频帧进行编码。在某些情况下,解码器的输出可以直接发送到编码器。这种情况的一个很好的例子是在不修改帧内容的情况下改变了压缩比特率(例如,在不将立体声通道合并为单声道的情况下重新压缩音频)。在其他情况下(例如调整视频大小),必须引入渲染层以将解码器输出转换为编码器输入。

在处理视频时,我们可以将MediaCodec配置为与ByteBuffer或Surface一起用作输入/输出。当需要访问原始像素时使用ByteBuffer,它通常较慢,而Surface则较快,但不提供对像素的直接访问。但是,可以使用OpenGL帧着色器修改表面像素。

LiTr将Surface模式用于视频编解码器,将ByteBuffer模式用于音频编解码器。视频渲染器使用OpenGL调整帧的大小(更改视频分辨率时)。并且由于OpenGL使我们能够绘制视频帧,因此视频渲染器支持自定义滤镜,从而允许客户端应用程序使用OpenGL着色器修改视频帧。

在ByteBuffer模式下运行编解码器时,可以执行相同的操作。除了使用OpenGL的情况外,所有渲染和帧修改都必须在软件中完成。以较低的性能为代价,这种方法允许使用软件解码器或帧内容感知逻辑(ML过滤器,超缩放等)。

LiTr结构

上面描述的代码转换过程是如何对单个轨道进行代码转换。使用MediaExtractor读取源数据,并使用MediaMuxer写入目标数据,二者均由Android媒体堆栈提供。对于每种轨道类型(视频,音频,其他),LiTr使用特定的轨道代码转换器:

  1. 视频轨道代码转换器可以调整帧大小并更改编码比特率。如有必要,它还可以使用客户端提供的 滤镜来修改帧像素。它在Surface模式下同时运行编码器和解码器编解码器,并使用OpenGL将解码器的输出渲染到编码器的输入上。
  2. 音轨转码器只能更改比特率(目前)。
  3. 所有所有非视频和非音频帧都使用直通轨道转码器“按原样”写出。

在进行代码转换时,LiTr会连续迭代所有轨道代码转换器,直到每个轨道代码转换器报告其已完成工作。当带有END_OF_STREAM标志的帧经过每个转码步骤时,轨道转码器认为其工作已完成。转码完成后,将发信号通知MediaMuxer最终确定目标媒体,MediaExtractor释放源媒体。

开始实践

首先,将LiTr导入您的Android应用程序:

代码语言:javascript复制
implementation ‘com.linkedin.android.litr:litr:1.1.0’

然后,使用可以访问源/目标媒体的Context实例化MediaTransformer(主入口点类)。通常,这就是您应用的ApplicationContext。

代码语言:javascript复制
MediaTransformermediaTransformer=new MediaTransformer(getApplicationContext());

现在,您可以转换媒体:

这里需要注意的几件事:

  1. 客户端必须提供唯一的String requestId,这是转码请求的标志。由于LiTr接受多个代码转换请求,因此需要一种方法来识别每个代码转换请求。
  2. 应该从实例化MediaTranscoder时使用的上下文访问源视频URI。转码时会保留源轨道计数和顺序。
  3. 视频将被转换为H.264,并以提供的文件路径保存在MP4容器中。
  4. 目标视频和音频格式是设置了所有所需参数的Android MediaFormat的实例。该格式将应用于该类型的所有轨道。空格式表示该类型的轨道不会被转码,而是“原样”写出。
  5. 将使用所有代码转换更新来调用侦听器:开始,进度完成,错误,取消。每个侦听器回调中都会提供一个请求令牌。
  6. 粒度是所需的进度更新数量。默认值为100(以匹配在UI中显示的百分比)。传递0将在每个帧上回调。
  7. GlFilter的可选列表将您的自定义修改应用于视频帧。

可以使用提供的代码取消正在进行的转码:

代码语言:javascript复制
mediaTransformer.cancel(requestId);

当不再需要MediaTransformer时,必须将其释放:

代码语言:javascript复制
mediaTransformer.release();

LiTr还在配套的“过滤器包”库中提供过滤器实现。如果要使用过滤器,请导入litr-filters库:

代码语言:javascript复制
implementation ‘com.linkedin.android.litr:litr-filters:1.1.1'

该库中目前有两个过滤器,一个静态位图叠加层和一个帧序列动画叠加层(例如动画GIF)。我们正在努力实施更多过滤器,并欢迎做出贡献。

如果出现问题(MediaCodec初始化失败,解码器出错等),MediaTransformer将不会引发异常。相反,它将失败,并使用自定义异常调用侦听器的onError方法,然后客户端可以对其进行分析。

转换完成也可能包含详细的统计信息(跟踪元数据,转换持续时间等)。它们打算在生产环境中用于跟踪或调试目的。请注意,将来,LiTr API及其行为可能会更改,因此在这里主要将它们用于说明目的。

底层转换API

让我们退后一步,从概念上更深入地看一下转码过程。我们将看到有五个不同的步骤:

  1. 读取编码的源数据。
  2. 解码编码的源数据。
  3. 将解码器输出渲染到编码器输入上。
  4. 编码渲染的数据。
  5. 编写编码的目标数据。

每个步骤执行特定功能,并且与上一个和/或下一个步骤具有明确定义的交互。 LiTr提取了将视频转码为接口的每个步骤。我们将每个这样的接口称为“组件”。抽象为客户端提供了强大的功能,可通过插入其自己的组件实现来修改转码过程,而无需修改LiTr源代码。例如,可以实现自定义MediaSource来从Android的MediaExtractor不支持的容器中读取数据,或者自定义编码器可能会引入将代码转码为编码器硬件(例如AV1)不支持的编解码器的功能。

转码过程的逐步概述图转码过程的逐步概述图

LiTr即开即用,提供默认的组件实现,这些实现包装了Android的MediaCodec类。要传递自定义组件实现,客户端应使用“底层” LiTr API:

由于此API为客户端提供了更多控制权,因此也更容易被破坏。客户必须确保组件可以成功地相互交互。例如,MediaSource以Decoder期望的格式生成编码的帧,或者OpenGL Renderer不与在ByteBuffer模式下运行的Decoder和/或Encoder一起使用。

对LiTr的贡献

LiTr是一个开源项目,欢迎您的贡献!只需在GitHub上,提交拉取请求或打开问题,让我们知道您想看到哪些新功能。我们将继续积极开展LiTr的开发,但可以设想其发展和演变以成为社区的努力。

致谢

非常感谢Tanaka Yuya(AKA ypresto)开拓了android-transcoder项目,该项目激发了许多惊人的Android媒体项目,包括LiTr。感谢Google的AOSP CTS团队在OpenGL中编写“表面到表面”渲染实现,该实现成为LiTr中GlRenderer的基础。向我的亲爱的同事Amita Sahasrabudhe,Long Peng和Keerthi Korrapati表示感谢,感谢他们各自的贡献和代码审查。向我们的设计师Mauroof Ahmed呐喊,为LiTr提供视觉识别的内容!

0 人点赞