基于 ffmpeg 的跨平台播放器实现

https://www.qcloud.com/community/article/309889001486708756

背景:

随着游戏娱乐等直播业务的增长,在移动端观看直播的需求也日益迫切。但是移动端原生的播放器对各种直播流的支持却不是很好。Android 原生的 MediaPlayer 不支持 flv、hls 直播流,iOS 只支持标准的 HLS 流。本文介绍一种基于 ffplay 框架下的跨平台播放器的实现,且兼顾硬解码的实现。

播放器原理:

直观的讲,我们播放一个媒体文件一般需要5个基本模块,按层级顺序:文件读取模块(Source)、解复用模块(Demuxer)、视频频解码模块(Decoder)、色彩空间转换模块(Color Space Converter)、音视频渲染模块(Render)。数据的流向如下图所示,其中 ffmpeg 框架包含了文件读取、音视频解复用的模块。

  1. 文件读取模块(Source)的作用是为下级解复用模块(Demuxer)以包的形式源源不断的提供数据流,对于下一级的Demuxer来说,本地文件和网络数据是一样的。在ffmpeg框架中,文件读取模块可分为3层:

    • 协议层: pipe,tcp,udp,http等这些具体的本地文件或网络协议
    • 抽象层:URLContext结构来统一表示底层具体的本地文件或网络协议
    • 接口层用:AVIOContext结构来扩展URLProtocol结构成内部有缓冲机制的广泛意义上的文件,并且仅仅由最上层用AVIOContext对模块外提供服务,实现读媒体文件功能。
  2. 解复用模块(Demuxer):的作用是识别文件类型,媒体类型,分离出音频、视频、字幕原始数据流,打上时戳信息后传给下级的视频频解码模块(Decoder)。可以简单的分为两层,底层是 AVIContext,TCPContext,UDPContext 等等这些具体媒体的解复用结构和相关的基础程序,上层是 AVInputFormat 结构和相关的程序。上下层之间由 AVInputFormat 相对应的 AVFormatContext 结构的 priv_data 字段关联 AVIContext 或 TCPContext 或 UDPContext 等等具体的文件格式。AVInputFormat 和具体的音视频编码算法格式由 AVFormatContext 结构的 streams 字段关联媒体格式,streams 相当于 Demuxer 的 output pin,解复用模块分离音视频裸数据通过 streams 传递给下级音视频解码器。

  3. 视频频解码模块(Decoder)的作用就是解码数据包,并且把同步时钟信息传递下去。

  4. 色彩空间转换模块(Color Space Converter)颜色空间转换过滤器的作用是把视频解码器解码出来的数据转换成当前显示系统支持的颜色格式

  5. 音视频渲染模块(Render)的作用就是在适当的时间渲染相应的媒体,对视频媒体就是直接显示图像,对音频就是播放声音

跨平台实现

在播放器得5个模块中文件读取模块(Source)、解复用模块(Demuxer)和色彩空间转换模块(Color Space Converter)这三个模块都可以用 ffmpeg 的框架进行实现,而f fmpeg 本身就是跨平台的。因此,实现跨平台的播放器的就需要抽象一层平台无关的音视频解码、渲染接口。Android、iOS、Window 等平台只需要实现各自平台的渲染、硬件解码(如果支持的话)就可以构建一个标准的基于 ffmpeg 的播放器了。

下图是基于ffplay的基本播放流程图:

图中红色部分是需要抽象的接口的,结构如下:

其中 FF_Pipenode.run_sync 视频解码线程,默认有 libavcodec 的软解码实现,其他平台可以增加自己的硬解码实现。SDL_VideoOut 为视频渲染抽象层,这里 overlay 可以是 Android的 NativeWindow,或者是 OpenGL 的 Texture。SDL_AudioOut 是音频播放抽象层,可以直接操作声卡驱动,SDL2.0 里就支持 ALSA、OSS 接口,当然也可以用 Android、iOS SDK 中的音频 API 实现。

这里顺便提下,随着 Android、iOS 平台的普及,ffmpeg 版本的也逐步支持了 Android、iOS 的硬件解码器,如f fmpeg 在很早之前就支持了 libstagefright,最新的 ffmpeg2.8 也已经支持了 iOS 的硬件解码库 VideoToolBox。从下面重点介绍下视频硬解码以及音视频渲染模块在移动平台上的实现。

Android

1.硬解码模块:

Android 的硬解码模块目前有 2 种实现方案:

libstagefright_h264:

libstagefright 是 Android2.3 之后版本的多媒体库,ffmpeg 早在 0.9 版本时就已经将libstagefright_h264 收录到自己的解码库中了,从 libstagefright.cpp 包括的头文件路径来看,是基于 Android2.3 版本的源码。因此编译 libstagefright 需要 Android2.3 的相关源码以及动态链接库。

ffmpeg 中的 libstagefright 目前只实现了 h264 格式的解码,由于 Android 机型、版本的碎片化相当严重,这种基于某个 Android 版本编译出来的 libstagefright 也存在很严重的兼容性问题,我在 Android4.4 的机型上就遇到无法解码的问题。

MediaCodec:

MediaCodec 是 Google 在 Android4.1(API16)以后新提供的硬件编解码 API,其工作原理如图所示:

以解码为例,先从 Codec 获取 inputBuffer,将待解码数据填充到 inputbuffer,再将 inputbuffer 交给Codec,接下来就可以从 Codec 的 outputBuffer 中拿到新鲜出炉的图像和声音信息了。下面的这段实例代码也许更能说明问题:

MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 codec.stop();
 codec.release();

令人沮丧的是,MediaCodec 只提供了 java 层的 API,而我们的播放器是基于 ffplay 架构的,核心的解码模块是不可能移到 java 层的。既然我们移不上去,就只能把 MediaCodec 拉到 Native 层了,通过 (*JNIEnv)->CallxxxMethod 的方式将 MediaCodec 相关的 API 在 Native 层做了一套接口。嗯,现在我们可以来实现视频的硬件解码了:

queue_picture 的实现如下图所示:

2.视频渲染模块:

在渲染之前,我们必须先指定一个渲染的画布,在android上这个画布可以是ImageView,SurfaceView,TextureView或者是GLSurfaceView。

关于在Native层渲染图片的方法,我曾看过一篇文章,文中介绍了四种渲染方法:

  • Java Surface JNI
  • OpenGL ES 2 Texture
  • NDK ANativeWindow API
  • Private C++ API

如果是用 ffmpeg 的 libavcodec 进行软解码,那么使用 NDK ANativeWindow API 将是最高效简单的方案,主要实现代码:

ANativeWindow* window = ANativeWindow_fromSurface(env, javaSurface);

ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(window, &buffer, NULL) == 0) {
  memcpy(buffer.bits, pixels,  w * h * 2);
  ANativeWindow_unlockAndPost(window);
}

ANativeWindow_release(window);

示例代码中的 javaSurface 来自 java 层的 SurfaceHolder,pixels 指向 RGB 图像数据。

如果是使用了 MediaCodec 进行解码,那么视频渲染将变得异常简单,只需在 MediaCodec 配置时(MediaCodec.configure)指定图像渲染的 Surface,然后再解码完每一帧图像的时候调用 releaseOutputBuffer (index, true),MediaCodec 内部就会将图像渲染到指定的 Surface 上。

3.音频播放模块

Android 支持 2 套音频接口,分别是 AudioTrack 和 OpenSL ES,这里以 AudioTrack 为例介绍下音频的部分流程:

由于 AudioTrack 只有 java 层的 API,我们也得像 MediaCodec 一样在 Native 层重做一套 AudioTrack 的接口。

这里解码和播放是 2 个独立的线程,audioCallback 负责从 Audio Frame queue 中获取解码后的音频数据,如果解码后的音频采样率不是 AudioTrack 所支持的,就需要用 libswresample 进行重采样。

iOS

1. 硬解码模块

从 iOS8 开始,开放了硬解码和硬编码 API,就是名为 VideoToolbox.framework 的 API,支持 h264 的硬件编解码,不过需要 iOS 8 及以上的版本才能使用。这套硬解码 API 是几个纯 C 函数,在任何 OC 或者 C++ 代码里都可以使用。首先要把 VideoToolbox.framework 添加到工程里,并且包含以下头文件。

#include

解码主要需要以下四个函数:

  • VTDecompositionSessionCreate 创建解码session
  • VTDecompressionSessionDecodeFrame 解码一个frame
  • VTDecompressionOutputCallback 解码完一个frame后的回调
  • VTDecompressionSessionInvalidate 销毁解码session

解码流程如图所示:

2. 视频渲染模块

视频的渲染采用 OpenGL ES2 纹理贴图的形式。

3. 音频播放模块

采用 iOS 的 AudioToolbox.frameworks 进行播放。数据流程和 Android 平台是相同,不同的是,Android 平台把 PCM 数据喂给 AudioTrack,iOS 上把 PCM 数据喂给 AudioQueue。

总结

其实 ffpmeg 自带的播放器实例 ffplay 就是一个跨平台的播放器,得益于其依赖的多媒体库 SDL 实现了多平台的音视频渲染。但是 SDL 库过于庞大,并不适合整体移植到移动端。本文介绍的跨平台实现方案也是借鉴了 SDL2.0 的内部实现,只是重新设计了渲染接口。

 

posted @ 2017-07-31 10:38  STN_LCD  阅读(3585)  评论(0编辑  收藏  举报