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

背景:

随着游戏娱乐等直播业务的增长,在移动端观看直播的需求也日益迫切。但是移动端原生的播放器对各种直播流的支持却不是很好。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 的内部实现,只是重新设计了渲染接口。

相关推荐

零基础读懂视频播放器控制原理——ffplay播放器源代码分析

【腾讯云的1001种玩法】 Laravel 整合微视频上传管理能力,轻松打造视频App后台

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

许斌盛的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JarvanMo的IT专栏

基于ExoPlayer的ExoPlayerVideoView

在Android设备中,播放视频和音乐是非常普遍的。Android框架提供了一个对于媒体的操作的最省代码的解决方案:MediaPlayer。它提供了低等级的媒体...

1043
来自专栏進无尽的文章

多媒体-图片、音频、视频的基本实现

AVFoundation的录音和播放 音频的录制与播放主要和三个类有关AVAudioSession,AVAudioRecorder,AVAudioPlayer...

431
来自专栏移动开发之家

Android音频播放(本地/网络)绘制数据波形,根据特征有节奏的改变颜色

MediaPlayer、MediaRecord、AudioRecord,这三个都是大家耳目能详的Android多媒体类(= =没听过的也要假装听过),包含了音视...

1762
来自专栏青蛙要fly的专栏

项目需求讨论-WebView进度加载条

又到了每次的实际项目开发中的需求讨论了。这次是因为做的项目是原生内嵌WebView,所以当我们的WebView在加载网页的时候,需要有个加载进度条,当然这时候有...

973
来自专栏androidBlog

ARouter 使用教程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gdutxiaoxu/article/de...

981
来自专栏移动开发

结合 MultiType 实现加载更多

MultiType 是一个分发管理类,帮助我们轻松实现复杂布局.建议大家阅读源码,作者的思路并不复杂但很巧妙.

882
来自专栏codelang

纯手工打造Easy支付库

1254
来自专栏緣來來來

安卓基础干货(七):安卓广播的学习

android应用程序里面的电台:系统内置的一个服务,会把事件(电量不足、电量充满、开机启动完成)作为一个广播消息发送其他的接收者;

631
来自专栏流媒体

Android平台下使用FFmpeg进行RTMP推流(摄像头推流)

前面讲到了在Android平台下使用FFmpeg进行RTMP推流(视频文件推流),里面主要是介绍如何解析视频文件并进行推流,今天要给大家介绍如何在Android...

1604
来自专栏落影的专栏

iOS音视频播放(Audio Unit播放音频+OpenGL ES绘制视频)

前言 相关文章: 使用VideoToolbox硬编码H.264 使用VideoToolbox硬解码H.264 使用AudioToolbox编码AAC 使...

4609

扫码关注云+社区