中超直播
当前位置: 首页 > 资讯 > 篮球资讯 > Android音视频学习系列(九) — Android端实现rtmp推流

Android音视频学习系列(九) — Android端实现rtmp推流

前面讲解了如何搭建 rtmp 直播服务器,和如何开发一款具有拉流功能的 Android 播放器。那么现在有了播放端和直播服务器还缺少推流端。该篇文章我们就一起来实现 Android 端的 rtmp 推流,想要实现 Android 端推流必须要经过如下几个阶段,见下图:

该篇文章主要完成上图黄颜色功能部分,下面就开始进入正题,代码编写了。

软编码

硬编码

文章末尾会介绍软硬编解码。

Android SDK 提供了两套音频采集的 API ,分别是 MediaRecorder 、AudioRecord 。前者是一个上层 API ,它可以直接对手机麦克风录入的音频数据进行编码压缩(如 AMR/MP3) 等,并存储为文件;后者则更接近底层,能够更加自由灵活地控制,其可以让开发者得到内存中的 PCM 原始音频数据流。如果想做一个简单的录音机,输出音频文件则推荐使用 MediaRecorder ; 如果需要对音频做进一步的算法处理,或者需要采用第三方的编码库进行编码,又或者需要用到网络传输等场景中,那么只能使用 AudioRecord 或者 OpenSL ES ,其实 MediaRecorder 底层也是调用了 AudioRecord 与 Android Framework 层的 AudioFlinger 进行交互的。而我们该篇的场景更倾向于第二种实现方式,即使用 AudioRecord 来采集音频。

如果想要使用 AudioRecord 这个 API ,则需要在应用 AndroidManifest.xml 的配置文件中进行如下配置:

当然,如果你想把采集到的 PCM 原始数据,存储 sdcard 中,还需要额外添加写入权限:

接下来了解一下 AudioRecord 的工作流程。

1. 初始化 AudioRecord

首先来看一下 AudioRecord 的配置参数,AudioRecord 是通过构造函数来配置参数的,其函数原型如下:

上述参数所代表的函数及其在各种场景下应该传递的值的含义参考如下说明:

该参数指的是音频采集的输入源,可选值以常量的形式定义在类 AudioSource (MediaRecorder 中的一个内部类)中,常用的值包过:

DEFAULT(默认)

VOICE_RECOGNITION (用于语音识别,等同于默认)

MIC (由手机麦克风输入)

VOICE_COMMUNICATION (用于 VOIP 应用场景)

用于指定以多大的采样频率来采集音频,现在用的最多的兼容最好是 44100 (44.1KHZ)采样频率。

该参数用于指定录音器采集几个声道的声音,可选值以常量的形式定义在 AudioFormat 类中,常用的值包括:

CHANNEL_IN_MONO 单声道 (移动设备上目前推荐使用)

CHANNEL_IN_STEREO 立体声

采样格式,以常量的形式定义在 AudioFormat 类中,常用的值包括:

ENCODING_PCM_16BIT (16bit 兼容大部分 Android 手机)

ENCODING_PCM_8BIT (8bit)

配置内部音频缓冲区的大小(配置的缓存值越小,延时就越低),而具体的大小,有可能在不同的手机上会有不同的值,那么可以使用如下 API 进行确定缓冲大小:

配置好之后,检查一下 AudioRecord 当前的状态是否可以进行录制,可以通过 AudioRecord##getState 来获取当前的状态:

STATE_UNINITIALIZED 还没有初始化,或者初始化失败了

STATE_INITIALIZED 已经初始化成功了。

2. 开启采集

创建好 AudioRecord 之后,就可以开启音频数据的采集了,可以通过调用下面的函数进行控制麦克风的采集:

3. 提取数据

执行完上一步之后,需要开启一个子线程用于不断的从 AudioRecord 缓冲区读取 PCM 数据,调用如下函数进行读取数据:

4. 停止采集

如果想要停止采集,那么只需要调用 AudioRecord 的 stop 方法来实现,最后可以通过一个变量先控制子线程停止读取数据,然后在调用 stop 停止最后释放 AudioRecord 实例。

视频画面的采集主要是使用各个平台提供的摄像头 API 来实现的,在为摄像头设置了合适的参数之后,将摄像头实时采集的视频帧渲染到屏幕上提供给用户预览,然后将该视频帧传递给编码通道,进行编码。

1. 权限配置

2. 打开摄像头

2.1 检查摄像头

2.2 检查摄像头的个数

检查完摄像头服务后,还需要检查手机上摄像头的个数,如果个数为 0,则说明手机上没有摄像头,这样的话也是不能进行后续操作的。

在上面的方法中,需要传入一个是否先开启背面摄像头的 boolean 变量,如果变量为 true,则把背面摄像头放在列表第一个,之后打开摄像头的时候,直接获取列表中第一个摄像头相关参数,然后进行打开。这样的设计使得切换摄像头也变得十分简单,切换摄像头时,先关闭当前摄像头,然后变化摄像头列表中的顺序,然后再打开摄像头即可,也就是每次打开摄像头都打开摄像头列表中第一个摄像头参数所指向的摄像头。

2.3 打开摄像头

打开摄像头之前,先从摄像头列表中获取第一个摄像头参数,之后根据参数中的 CameraId 来打开摄像头,打开成功后改变相关状态。相关代码如下:

上面需要注意的是,在 Android 提供的 Camera 源码中,Camera.open(cameraData.cameraID) 抛出异常则说明Camera 不可用,否则说明 Camera 可用,但是在一些手机上 Camera.open(cameraData.cameraID) 不是抛出异常,而是返回 null。

3. 配置摄像头参数

在给摄像头设置参数后,需要记录这些参数,以方便其他地方使用。比如记录当前摄像头是否有闪光点,从而可以决定 UI 界面上是否显示打开闪光灯按钮。在直播项目中使用 CameraData 来记录这些参数,CameraData 类如下所示:

给摄像头设置参数的时候,有一点需要注意:设置的参数不生效会抛出异常,因此需要每个参数单独设置,这样就避免一个参数不生效后抛出异常,导致之后所有的参数都没有设置。

4. 摄像头开启预览

设置预览界面有两种方式:1、通过 SurfaceView 显示;2、通过 GLSurfaceView 显示。当为 SurfaceView 显示时,需要传给 Camera 这个 SurfaceView 的 SurfaceHolder。当使用 GLSurfaceView 显示时,需要使用Renderer 进行渲染,先通过 OpenGL 生成纹理,通过生成纹理的纹理 id 生成 SurfaceTexture ,将SurfaceTexture 交给 Camera ,那么在 Render 中便可以使用这个纹理进行相应的渲染,最后通过GLSurfaceView 显示。

4.1 设置预览回调

当设置预览好预览回调的图片格式后,需要设置预览回调的 Callback。

Android 推荐的 PreViewFormat 是 NV21,在 PreviewCallback 中会返回 Preview 的 N21 图片。如果是软编的话,由于 H264 支持 I420 的图片格式,因此需要将 N21格式转为 I420 格式,然后交给 x264 编码库。如果是硬编的话,由于 Android 硬编编码器支持 I420(COLOR_FormatYUV420Planar) 和NV12(COLOR_FormatYUV420SemiPlanar),因此可以将 N21 的图片转为 I420 或者 NV12 ,然后交给硬编编码器。

4.2 设置预览图像大小

在摄像头相关处理中,一个比较重要的是 的处理。在 Android 中,摄像头有一系列的 PreviewSize,我们需要从中选出适合的 PreviewSize 。选择合适的摄像头 PreviewSize 的代码如下所示:

在设置好最适合的 PreviewSize 之后,将 size 信息存储在 CameraData 中。当选择了 SurfaceView 显示的方式,可以将 SurfaceView 放置在一个 LinearLayout 中,然后根据摄像头 PreviewSize 的比例改变 SurfaceView 的大小,从而使得两者比例一致,确保图像正常。当选择了GLSurfaceView 显示的时候,可以通过裁剪纹理,使得纹理的大小比例和 GLSurfaceView 的大小比例保持一致,从而确保图像显示正常。

4.3 图像旋转

在 Android 中摄像头出来的图像需要进行一定的旋转,然后才能交给屏幕显示,而且如果应用支持屏幕旋转的话,也需要根据旋转的状况实时调整摄像头的角度。在 Android 中旋转摄像头图像同样有两种方法,一是通过摄像头的 setDisplayOrientation(result) 方法,一是通过 OpenGL 的矩阵进行旋转。下面是通过setDisplayOrientation(result) 方法进行旋转的代码:

4.4 设置预览帧率

通过 Camera.Parameters 中 getSupportedPreviewFpsRange() 可以获得摄像头支持的帧率变化范围,从中选取合适的设置给摄像头即可。相关的代码如下:

4.5 设置相机对焦

一般摄像头对焦的方式有两种:手动对焦和触摸对焦。下面的代码分别是设置自动对焦和触摸对焦的模式:

对于自动对焦这样设置后就完成了工作,但是对于触摸对焦则需要设置对应的对焦区域。要准确地设置对焦区域,有三个步骤:一、得到当前点击的坐标位置;二、通过点击的坐标位置转换到摄像头预览界面坐标系统上的坐标;三、根据坐标生成对焦区域并且设置给摄像头。整个摄像头预览界面定义了如下的坐标系统,对焦区域也需要对应到这个坐标系统中。

如果摄像机预览界面是通过 SurfaceView 显示的则比较简单,由于要确保不变形,会将 SurfaceView 进行拉伸,从而使得 SurfaceView 和预览图像大小比例一致,因此整个 SurfaceView 相当于预览界面,只需要得到当前点击点在整个 SurfaceView 上对应的坐标,然后转化为相应的对焦区域即可。如果摄像机预览界面是通过GLSurfaceView 显示的则要复杂一些,由于纹理需要进行裁剪,才能使得显示不变形,这样的话,我们要还原出整个预览界面的大小,然后通过当前点击的位置换算成预览界面坐标系统上的坐标,然后得到相应的对焦区域,然后设置给摄像机。当设置好对焦区域后,通过调用 Camera 的 autoFocus() 方法即可完成触摸对焦。 整个过程代码量较多,请自行阅读项目源码。

4.6 设置缩放

当检测到手势缩放的时候,我们往往希望摄像头也能进行相应的缩放,其实这个实现还是比较简单的。首先需要加入缩放的手势识别,当识别到缩放的手势的时候,根据缩放的大小来对摄像头进行缩放。代码如下所示:

4.7 闪光灯操作

一个摄像头可能有相应的闪光灯,也可能没有,因此在使用闪光灯功能的时候先要确认是否有相应的闪光灯。检测摄像头是否有闪光灯的代码如下:

切换闪光灯的代码如下:

4.8 开始预览

当打开了摄像头,并且设置好了摄像头相关的参数后,便可以通过调用 Camera 的 startPreview() 方法开始预览。有一个需要说明,无论是 SurfaceView 还是 GLSurfaceView ,都可以设置 SurfaceHolder.Callback ,当界面开始显示的时候打开摄像头并且开始预览,当界面销毁的时候停止预览并且关闭摄像头,这样的话当程序退到后台,其他应用也能调用摄像头。

5. 停止预览

停止预览只需要释放掉相机资源即可:

AudioRecord 采集完之后需要对 PCM 数据进行实时的编码 (软编利用 通过 NDK 交叉编译静态库、硬编使用 Android SDK MediaCodec 进行编码)。

软编

语音软编这里们用主流的编码库 libfaac 进行编码 AAC 语音格式数据。

1. 编译 libfaac

1.1 下载 libfaac

1.2 编写交叉编译脚本

2. CMakeLists.txt 配置

3. 配置 faac 编码参数

4. 配置 AAC 包头

在发送 rtmp 音视频包的时候需要将语音包头第一个发送

5. 开始实时编码

6. 释放编码器

在不需要编码或者退出编码的时候需要主动释放编码器,释放 native 内存,可以通过如下函数来实现释放编码器的操作:

硬编

软编码介绍完了下面利用 Android SDK 自带的 MediaCodec 函数进行对 PCM 编码为 AAC 的格式音频数据。使用 MediaCodec 编码 AAC 对 Android 系统是有要求的,必须是 4.1系统以上,即要求 Android 的版本代号在 Build.VERSION_CODES.JELLY_BEAN (16) 以上。MediaCodec 是 Android 系统提供的硬件编码器,它可以利用设备的硬件来完成编码,从而大大提高编码的效率,还可以降低电量的使用,但是其在兼容性方面不如软编号,因为 Android 设备的锁片化太严重,所以读者可以自己衡量在应用中是否使用 Android 平台的硬件编码特性。

1. 创建 "audio/mp4a-latm" 类型的硬编码器

2. 配置音频硬编码器

3. 开启音频硬编码器

4. 拿到硬编码输入(PCM)输出(AAC) ByteBufferer

到了这一步说明,音频编码器配置完成并且也成功开启了,现在就可以从 MediaCodec 实例中获取两个 buffer ,一个是输入 buffer 一个是输出 buffer , 输入 buffer 类似于 FFmpeg 中的 AVFrame 存放待编码的 PCM 数据,输出 buffer 类似于 FFmpeg 的 AVPacket 编码之后的 AAC 数据, 其代码如下:

5. 开始 PCM 硬编码为 AAC

到此,所有初始化方法已实现完毕,下面来看一下 MediaCodec 的工作原理如下图所示,左边 Client 元素代表要将 PCM 放到 inputBuffer 中的某个具体的 buffer 中去,右边的 Client 元素代表将编码之后的原始 AAC 数据从 outputBuffer 中的某个具体 buffer 中取出来,ὄ; 左边的小方块代表各个 inputBuffer 元素,右边的小方块则代表各个 outputBuffer 元素。详细介绍可以看 。

代码具体实现如下:

6. AAC 打包为 flv

7. 释放编码器

在使用完 MediaCodec 编码器之后,就需要停止运行并释放编码器,代码如下:

Camera 采集完之后需要对 YUV 数据进行实时的编码 (软编利用 通过 NDK 交叉编译静态库、硬编使用 Android SDK MediaCodec 进行编码)。

软编

视频软编这里们用主流的编码库 x264 进行编码 H264 视频格式数据。

1. 交叉编译 x264

1.1 下载 x264

1.2 编写编译脚本

在编写脚本之前需要在 configure 中添加一处代码 -Werror=implicit-function-declaration,如下所示:

交叉编译脚本如下:

2. CMakeList.txt 配置

3. 配置并打开 x264 编码器

4. 开始编码

5. 释放编码器

当我们不需要编码的时候需要释放编码器,代码如下:

硬编

在 Android 4.3 系统以后,用 MediaCodec 编码视频成为了主流的使用场景,尽管 Android 的碎片化很严重,会导致一些兼容性问题,但是硬件编码器的性能以及速度是非常可观的,并且在 4.3 系统之后可以通过 Surface 来配置编码器的输入,大大降低了显存到内存的交换过程所使用的时间,从而使得整个应用的体验得到大大提升。由于输入和输出已经确定,因此接下来将直接编写 MediaCodec 编码视频帧的过程。

1. 创建 video/avc 类型的硬编码器

2. 配置视频编码器

3. 开启视频编码器

4. 拿到编码之后的数据

5. H264 打包为 flv

这个方法主要是从编码后的数据中解析得到NALU,然后判断NALU的类型,最后再把数据回调给 FlvPacker 去处理。

处理 spsPps:

处理视频帧:

6. 释放编码器,并释放 Surface

注: 实际项目 rtmp 需要先连接上才有后续操作。

rtmp 模块我们已在开发 播放器 的时候,将它和 ffmpeg 一并编译了。所以我们直接使用上次的静态库和头文件就可以了,如果对 rtmp 协议不了解的可以参考上一篇文章,里面也有介绍 搭建 RTMP 直播服务器。

到这里软编码和硬编码数据都已准备好了现在,需要发送给 rtmp 模块,也就是在 native 中,先看 java 发送出口:

1. Rtmp 链接

Rtmp 底层是 TCP 协议,所以你可以使用 Java Socket 进行连接,也可以使用 c++ librtmp 库来进行连接,咱们这里就使用 librtmp 来进行连接。

2. Native 音频模块接收 AAC Flv 打包数据

3. Native 视频模块接收 H264 Flv 打包数据

4. RTMP 发送数据

4.1 将接收到的数据入发送队列

4.2 发送

5. 关闭 RTMP

当不需要发送音视频数据的时候需要关闭 rtmp 连接

1. 区别

使用 CPU 进行编码。 使用 GPU 进行编码。

2. 比较

实现直接、简单,参数调整方便,升级容易,但 CPU 负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。 性能高,低码率下通常质量低于软编码器,但部分产品在 GPU 硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。

3. 使用场景

适用短时间操作,如录制短视频等。

长时间编码或者对视频质量要求高(VOIP 实时通话),可以推荐硬件编码 (前提是手机性能好)。

到这里 Android 端软编推流,硬编推流都分别实现了。在项目上可以根据实际情况来选择到底是硬编还是软编。

如有帮助到你,可以点击一波关注、点赞吗?感谢支持!

标签:

推荐新闻

球队排行

足球 篮球
英超 德甲 法甲 西甲 意甲
排名球队胜/平/负积分
NBA CBA
排名球队胜/负胜率
东部联盟 西部联盟