系列文章

Android音视频学习系列(一) — JNI从入门到精通

Android音视频学习系列(二) — 交叉编译动态库、静态库的入门

Android音视频学习系列(三) — Shell脚本入门

Android音视频学习系列(四) — 一键编译32/64位FFmpeg4.2.2

Android音视频学习系列(五) — 掌握音频基础知识并使用AudioTrack、OpenSL ES渲染PCM数据

Android音视频学习系列(六) — 掌握视频基础知识并使用OpenGL ES 2.0渲染YUV数据

Android音视频学习系列(七) — 从0~1开发一款Android端播放器(支持多协议网络拉流本地文件)

Android音视频学习系列(八) — 基于Nginx搭建(rtmp、http)直播服务器

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

Android音视频学习系列(十) — 基于FFmpeg + OpenSL ES实现音频万能播放器

前言

前面讲解了如何搭建 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 的配置文件中进行如下配置:

 <uses-permission android:name="android.permission.RECORD_AUDIO"></uses-permission>

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

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

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

1. 初始化 AudioRecord

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

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes)

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

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

  • DEFAULT(默认)

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

  • MIC (由手机麦克风输入)

  • VOICE_COMMUNICATION (用于 VOIP 应用场景)

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

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

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

  • CHANNEL_IN_STEREO 立体声

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

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

  • ENCODING_PCM_8BIT (8bit)

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

AudioRecord.getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);

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

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

  • STATE_INITIALIZED 已经初始化成功了。

2. 开启采集

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

mAudioRecord.startRecording();

3. 提取数据

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

int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes);

4. 停止采集

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

    public void stopEncode() {//停止的变量标记mStopFlag = true;if(mAudioEncoder != null) {//停止采集mAudioEncoder.stop();//释放内存mAudioEncoder = null;}}

视频采集

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

1. 权限配置

<uses-permission android:name="android.permission.CAMERA"></uses-permission>

2. 打开摄像头

2.1 检查摄像头

public static void checkCameraService(Context context)throws CameraDisabledException {// Check if device policy has disabled the camera.DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);if (dpm.getCameraDisabled(null)) {throw new CameraDisabledException();}
}

2.2 检查摄像头的个数

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

public static List<CameraData> getAllCamerasData(boolean isBackFirst) {ArrayList<CameraData> cameraDatas = new ArrayList<>();Camera.CameraInfo cameraInfo = new Camera.CameraInfo();int numberOfCameras = Camera.getNumberOfCameras();for (int i = 0; i < numberOfCameras; i++) {Camera.getCameraInfo(i, cameraInfo);if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {CameraData cameraData = new CameraData(i, CameraData.FACING_FRONT);if(isBackFirst) {cameraDatas.add(cameraData);} else {cameraDatas.add(0, cameraData);}} else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {CameraData cameraData = new CameraData(i, CameraData.FACING_BACK);if(isBackFirst) {cameraDatas.add(0, cameraData);} else {cameraDatas.add(cameraData);}}}return cameraDatas;
}

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

2.3 打开摄像头

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

public synchronized Camera openCamera()throws CameraHardwareException, CameraNotSupportException {CameraData cameraData = mCameraDatas.get(0);if(mCameraDevice != null && mCameraData == cameraData) {return mCameraDevice;}if (mCameraDevice != null) {releaseCamera();}try {Log.d(TAG, "open camera " + cameraData.cameraID);mCameraDevice = Camera.open(cameraData.cameraID);} catch (RuntimeException e) {Log.e(TAG, "fail to connect Camera");throw new CameraHardwareException(e);}if(mCameraDevice == null) {throw new CameraNotSupportException();}mCameraData = cameraData;mState = State.OPENED;return mCameraDevice;
}

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

3. 配置摄像头参数

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

public class CameraData {public static final int FACING_FRONT = 1;public static final int FACING_BACK = 2;public int cameraID;            //camera的idpublic int cameraFacing;        //区分前后摄像头public int cameraWidth;         //camera的采集宽度public int cameraHeight;        //camera的采集高度public boolean hasLight;        //camera是否有闪光灯public int orientation;         //camera旋转角度public boolean supportTouchFocus;   //camera是否支持手动对焦public boolean touchFocusMode;      //camera是否处在自动对焦模式public CameraData(int id, int facing, int width, int height){cameraID = id;cameraFacing = facing;cameraWidth = width;cameraHeight = height;}public CameraData(int id, int facing) {cameraID = id;cameraFacing = facing;}
}

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

4. 摄像头开启预览

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

4.1 设置预览回调

public static void setPreviewFormat(Camera camera, Camera.Parameters parameters) {//设置预览回调的图片格式try {parameters.setPreviewFormat(ImageFormat.NV21);camera.setParameters(parameters);} catch (Exception e) {e.printStackTrace();}
}

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

Camera.PreviewCallback myCallback = new Camera.PreviewCallback() {@Overridepublic void onPreviewFrame(byte[] data, Camera camera) {//得到相应的图片数据//Do something}
};
public static void setPreviewCallback(Camera camera, Camera.PreviewCallback callback) {camera.setPreviewCallback(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 的代码如下所示:

public static Camera.Size getOptimalPreviewSize(Camera camera, int width, int height) {Camera.Size optimalSize = null;double minHeightDiff = Double.MAX_VALUE;double minWidthDiff = Double.MAX_VALUE;List<Camera.Size> sizes = camera.getParameters().getSupportedPreviewSizes();if (sizes == null) return null;//找到宽度差距最小的for(Camera.Size size:sizes){if (Math.abs(size.width - width) < minWidthDiff) {minWidthDiff = Math.abs(size.width - width);}}//在宽度差距最小的里面,找到高度差距最小的for(Camera.Size size:sizes){if(Math.abs(size.width - width) == minWidthDiff) {if(Math.abs(size.height - height) < minHeightDiff) {optimalSize = size;minHeightDiff = Math.abs(size.height - height);}}}return optimalSize;
}public static void setPreviewSize(Camera camera, Camera.Size size, Camera.Parameters parameters) {try {    parameters.setPreviewSize(size.width, size.height);           camera.setParameters(parameters);} catch (Exception e) {    e.printStackTrace();}
}

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

4.3 图像旋转

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

public static int getDisplayRotation(Activity activity) {int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();switch (rotation) {case Surface.ROTATION_0: return 0;case Surface.ROTATION_90: return 90;case Surface.ROTATION_180: return 180;case Surface.ROTATION_270: return 270;}return 0;
}public static void setCameraDisplayOrientation(Activity activity, int cameraId, Camera camera) {// See android.hardware.Camera.setCameraDisplayOrientation for// documentation.Camera.CameraInfo info = new Camera.CameraInfo();Camera.getCameraInfo(cameraId, info);int degrees = getDisplayRotation(activity);int result;if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {result = (info.orientation + degrees) % 360;result = (360 - result) % 360; // compensate the mirror} else { // back-facingresult = (info.orientation - degrees + 360) % 360;}camera.setDisplayOrientation(result);
}

4.4 设置预览帧率

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

public static void setCameraFps(Camera camera, int fps) {Camera.Parameters params = camera.getParameters();int[] range = adaptPreviewFps(fps, params.getSupportedPreviewFpsRange());params.setPreviewFpsRange(range[0], range[1]);camera.setParameters(params);
}private static int[] adaptPreviewFps(int expectedFps, List<int[]> fpsRanges) {expectedFps *= 1000;int[] closestRange = fpsRanges.get(0);int measure = Math.abs(closestRange[0] - expectedFps) + Math.abs(closestRange[1] - expectedFps);for (int[] range : fpsRanges) {if (range[0] <= expectedFps && range[1] >= expectedFps) {int curMeasure = Math.abs(range[0] - expectedFps) + Math.abs(range[1] - expectedFps);if (curMeasure < measure) {closestRange = range;measure = curMeasure;}}}return closestRange;
}

4.5 设置相机对焦

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

public static void setAutoFocusMode(Camera camera) {try {Camera.Parameters parameters = camera.getParameters();List<String> focusModes = parameters.getSupportedFocusModes();if (focusModes.size() > 0 && focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);camera.setParameters(parameters);} else if (focusModes.size() > 0) {parameters.setFocusMode(focusModes.get(0));camera.setParameters(parameters);}} catch (Exception e) {e.printStackTrace();}
}public static void setTouchFocusMode(Camera camera) {try {Camera.Parameters parameters = camera.getParameters();List<String> focusModes = parameters.getSupportedFocusModes();if (focusModes.size() > 0 && focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);camera.setParameters(parameters);} else if (focusModes.size() > 0) {parameters.setFocusMode(focusModes.get(0));camera.setParameters(parameters);}} catch (Exception e) {e.printStackTrace();}
}

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

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

4.6 设置缩放

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

/*** Handles the pinch-to-zoom gesture*/
private class ZoomGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {@Overridepublic boolean onScale(ScaleGestureDetector detector) {if (!mIsFocusing) {float progress = 0;if (detector.getScaleFactor() > 1.0f) {progress = CameraHolder.instance().cameraZoom(true);} else if (detector.getScaleFactor() < 1.0f) {progress = CameraHolder.instance().cameraZoom(false);} else {return false;}if(mZoomListener != null) {mZoomListener.onZoomProgress(progress);}}return true;}
}public float cameraZoom(boolean isBig) {if(mState != State.PREVIEW || mCameraDevice == null || mCameraData == null) {return -1;}Camera.Parameters params = mCameraDevice.getParameters();if(isBig) {params.setZoom(Math.min(params.getZoom() + 1, params.getMaxZoom()));} else {params.setZoom(Math.max(params.getZoom() - 1, 0));}mCameraDevice.setParameters(params);return (float) params.getZoom()/params.getMaxZoom();
}

4.7 闪光灯操作

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

public static boolean supportFlash(Camera camera){Camera.Parameters params = camera.getParameters();List<String> flashModes = params.getSupportedFlashModes();if(flashModes == null) {return false;}for(String flashMode : flashModes) {if(Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode)) {return true;}}return false;
}

切换闪光灯的代码如下:

public static void switchLight(Camera camera, Camera.Parameters cameraParameters) {if (cameraParameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_OFF)) {cameraParameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);} else {cameraParameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);}try {camera.setParameters(cameraParameters);}catch (Exception e) {e.printStackTrace();}
}

4.8 开始预览

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

private SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {Log.d(SopCastConstant.TAG, "SurfaceView destroy");CameraHolder.instance().stopPreview();CameraHolder.instance().releaseCamera();}@TargetApi(Build.VERSION_CODES.GINGERBREAD)@Overridepublic void surfaceCreated(SurfaceHolder holder) {Log.d(SopCastConstant.TAG, "SurfaceView created");}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {Log.d(SopCastConstant.TAG, "SurfaceView width:" + width + " height:" + height);CameraHolder.instance().openCamera();CameraHolder.instance().startPreview();}
};

5. 停止预览

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

    public synchronized void releaseCamera() {if (mState == State.PREVIEW) {stopPreview();}if (mState != State.OPENED) {return;}if (mCameraDevice == null) {return;}mCameraDevice.release();mCameraDevice = null;mCameraData = null;mState = State.INIT;}

音频编码

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

软编

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

1. 编译 libfaac

1.1 下载 libfaac
wget https://sourceforge.net/projects/faac/files/faac-src/faac-1.29/faac-1.29.9.2.tar.gz
1.2 编写交叉编译脚本
#!/bin/bash#打包地址
PREFIX=`pwd`/android/armeabi-v7a
#配置NDK 环境变量
NDK_ROOT=$NDK_HOME
#指定 CPU
CPU=arm-linux-androideabi
#指定 Android API
ANDROID_API=17
#编译工具链目录
TOOLCHAIN=$NDK_ROOT/toolchains/$CPU-4.9/prebuilt/linux-x86_64FLAGS="-isysroot $NDK_ROOT/sysroot -isystem $NDK_ROOT/sysroot/usr/include/arm-linux-androideabi -D__ANDROID_API__=$ANDROID_API -U_FILE_OFFSET_BITS  -DANDROID -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb -Wa,--noexecstack -Wformat -Werror=format-security  -O0 -fPIC"CROSS_COMPILE=$TOOLCHAIN/bin/arm-linux-androideabi
export CC="$CROSS_COMPILE-gcc --sysroot=$NDK_ROOT/platforms/android-17/arch-arm"
export CFLAGS="$FLAGS"./configure \
--prefix=$PREFIX \
--host=arm-linux \
--with-pic \
--enable-shared=nomake clean
make install

2. CMakeLists.txt 配置

cmake_minimum_required(VERSION 3.4.1)
#语音编码器
set(faac ${CMAKE_SOURCE_DIR}/faac)
#加载 faac 头文件目录
include_directories(${faac}/include)
#指定 faac 静态库文件目录
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${faac}/libs/${CMAKE_ANDROID_ARCH_ABI}")
#批量添加自己编写的 cpp 文件,不要把 *.h 加入进来了
file(GLOB Push_CPP ${ykpusher}/*.cpp)
#添加自己编写 cpp 源文件生成动态库
add_library(ykpusher SHARED ${Push_CPP})
#找系统中 NDK log库
find_library(log_liblog)
#推流 so
target_link_libraries(#播放 soykpusher
#        # 写了此命令不用在乎添加 ffmpeg lib 顺序问题导致应用崩溃
#        -Wl,--start-group
#        avcodec avfilter avformat avutil swresample swscale
#        -Wl,--end-group
#        z#推流库rtmp#视频编码x264#语音编码faac#本地库android${log_lib})

3. 配置 faac 编码参数

//设置语音软编码参数
void AudioEncoderChannel::setAudioEncoderInfo(int samplesHZ, int channel) {//如果已经初始化,需要释放release();//通道 默认单声道mChannels = channel;//打开编码器//3、一次最大能输入编码器的样本数量 也编码的数据的个数 (一个样本是16位 2字节)//4、最大可能的输出数据  编码后的最大字节数mAudioCodec = faacEncOpen(samplesHZ, channel, &mInputSamples, &mMaxOutputBytes);if (!mAudioCodec) {if (mIPushCallback) {mIPushCallback->onError(THREAD_MAIN, FAAC_ENC_OPEN_ERROR);}return;}//设置编码器参数faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(mAudioCodec);//指定为 mpeg4 标准config->mpegVersion = MPEG4;//lc 标准config->aacObjectType = LOW;//16位config->inputFormat = FAAC_INPUT_16BIT;// 编码出原始数据 既不是adts也不是adifconfig->outputFormat = 0;faacEncSetConfiguration(mAudioCodec, config);//输出缓冲区 编码后的数据 用这个缓冲区来保存mBuffer = new u_char[mMaxOutputBytes];//设置一个标志,用于开启编码isStart = true;
}

4. 配置 AAC 包头

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

/*** 音频头包数据* @return*/
RTMPPacket *AudioEncoderChannel::getAudioTag() {if (!mAudioCodec) {setAudioEncoderInfo(FAAC_DEFAUTE_SAMPLE_RATE, FAAC_DEFAUTE_SAMPLE_CHANNEL);if (!mAudioCodec)return 0;}u_char *buf;u_long len;faacEncGetDecoderSpecificInfo(mAudioCodec, &buf, &len);int bodySize = 2 + len;RTMPPacket *packet = new RTMPPacket;RTMPPacket_Alloc(packet, bodySize);//双声道packet->m_body[0] = 0xAF;if (mChannels == 1) { //单身道packet->m_body[0] = 0xAE;}packet->m_body[1] = 0x00;//将包头数据 copy 到RTMPPacket 中memcpy(&packet->m_body[2], buf, len);//是否使用绝对时间戳packet->m_hasAbsTimestamp = FALSE;//包大小packet->m_nBodySize = bodySize;//包类型packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;//语音通道packet->m_nChannel = 0x11;packet->m_headerType = RTMP_PACKET_SIZE_LARGE;return packet;
}

5. 开始实时编码

void AudioEncoderChannel::encodeData(int8_t *data) {if (!mAudioCodec || !isStart)//不符合编码要求,退出return;//返回编码后的数据字节长度int bytelen = faacEncEncode(mAudioCodec, reinterpret_cast<int32_t *>(data), mInputSamples,mBuffer, mMaxOutputBytes);if (bytelen > 0) {//开始打包 rtmpint bodySize = 2 + bytelen;RTMPPacket *packet = new RTMPPacket;RTMPPacket_Alloc(packet, bodySize);//双声道packet->m_body[0] = 0xAF;if (mChannels == 1) {packet->m_body[0] = 0xAE;}//编码出的音频 都是 0x01packet->m_body[1] = 0x01;memcpy(&packet->m_body[2], mBuffer, bytelen);packet->m_hasAbsTimestamp = FALSE;packet->m_nBodySize = bodySize;packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;packet->m_nChannel = 0x11;packet->m_headerType = RTMP_PACKET_SIZE_LARGE;//发送 rtmp packet,回调给 RTMP send 模块mAudioCallback(packet);}
}

6. 释放编码器

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

void AudioEncoderChannel::release() {//退出编码的标志isStart = false;//释放编码器if (mAudioCodec) {//关闭编码器faacEncClose(mAudioCodec);//释放缓冲区DELETE(mBuffer);mAudioCodec = 0;}
}

硬编

软编码介绍完了下面利用 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" 类型的硬编码器

 mediaCodec = MediaCodec.createEncoderByType(configuration.mime);    

2. 配置音频硬编码器

    public static MediaCodec getAudioMediaCodec(AudioConfiguration configuration){MediaFormat format = MediaFormat.createAudioFormat(configuration.mime, configuration.frequency, configuration.channelCount);if(configuration.mime.equals(AudioConfiguration.DEFAULT_MIME)) {format.setInteger(MediaFormat.KEY_AAC_PROFILE, configuration.aacProfile);}//语音码率format.setInteger(MediaFormat.KEY_BIT_RATE, configuration.maxBps * 1024);//语音采样率 44100format.setInteger(MediaFormat.KEY_SAMPLE_RATE, configuration.frequency);int maxInputSize = AudioUtils.getRecordBufferSize(configuration);format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, configuration.channelCount);MediaCodec mediaCodec = null;try {mediaCodec = MediaCodec.createEncoderByType(configuration.mime);//MediaCodec.CONFIGURE_FLAG_ENCODE 代表编码器,解码传 0 即可mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);} catch (Exception e) {e.printStackTrace();if (mediaCodec != null) {mediaCodec.stop();mediaCodec.release();mediaCodec = null;}}return mediaCodec;}

3. 开启音频硬编码器

void prepareEncoder() {mMediaCodec = AudioMediaCodec.getAudioMediaCodec(mAudioConfiguration);mMediaCodec.start();
}

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

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

//存放的是 PCM 数据
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
//存放的是编码之后的 AAC 数据
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();

5. 开始 PCM 硬编码为 AAC

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

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

  1. Android音视频学习系列(八) — 基于Nginx搭建(rtmp、http)直播服务器

    系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...

  2. Android音视频学习系列(七) — 从0~1开发一款Android端播放器(支持多协议网络拉流本地文件)

    系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...

  3. Android音视频学习系列(五) — 掌握音频基础知识并使用AudioTrack、OpenSL ES渲染PCM数据

    系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...

  4. Android音视频学习系列(六) — 掌握视频基础知识并使用OpenGL ES 2.0渲染YUV数据

    系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...

  5. Android音视频学习系列(十) — 基于FFmpeg + OpenSL ES实现音频万能播放器

    系列文章 Android音视频学习系列(一) - JNI从入门到精通 Android音视频学习系列(二) - 交叉编译动态库.静态库的入门 Android音视频学习系列(三) - Shell脚本入门 ...

  6. android 键编译,Android 音视频学习系列 (四) 一键编译 32/64 位 FFmpeg 4.2.2

    前言 2020/5/20 增加了硬件解码编译脚本 编译环境 Centos + NDK20b + FFmpeg4.2.2 + Android-21/16 2020/4/26 更新了编译 64 位脚本 编 ...

  7. Android音视频 - 学习路线概览

    PS 我们上一个系列 - OpenGL ES 暂告一段落,如果你对相机滤镜感兴趣,可以参看之前的文章. 从本篇开始呢,开始记录Android音视频的相关知识. 学习路线概览 Android音视频的基础 ...

  8. 《Android 音视频开发》《 Android 进阶解密》 书籍赠送

    今年听了一年的 "Android不行了..." 之类的谣言,这不都年底了嘛,也没看见哪个公司停止了安卓开发需求. 最近又出现了很多关于"互联网寒冬..."之类的 ...

  9. Android-音视频学习系列-(九)Android-端实现-rtmp-推流,零基础如何成为高级Android开发

    camera.setDisplayOrientation(result); } 4.4 设置预览帧率 通过 Camera.Parameters 中 getSupportedPreviewFpsRang ...

最新文章

  1. Hibernate中的核心接口query接口用法
  2. python语句中ord_浅谈Python中chr、unichr、ord字符函数之间的对比
  3. 网络设备配置与管理--使用VTP实现扩展VLAN配置
  4. Oracle配置管理
  5. Tab栏切换布局分析
  6. 使用循环链表实现一个通讯录的管理程序_【LeetCode链表题型总结】
  7. jQuery 页面载入进度条 (必有一款适合你----综合搜集版)
  8. DXP 内电层分割
  9. Python中的字符串方法
  10. 排序小记【1】基本排序算法
  11. mysql角色管理权限管理_mysql权限角色管理
  12. 构建一个基于UIView的类别
  13. Flutter29,毕向东java基础全套视频教程百度网盘
  14. cad lisp 管道截面_CAD计算材料截面特性插件-cad计算截面特性插件稳定免费版-东坡下载...
  15. 论高碳艺术与低碳艺术
  16. html火狐里面背景音乐不播放,各位大神有知道怎样在火狐浏览器上添加背景音乐的吗?...
  17. Windows中Redis的下载安装与修改密码并启动
  18. html倒计时的原理,JS实现活动精确倒计时 - 轩枫阁
  19. 七牛云被攻击偷跑流量
  20. java毕设项目车牌号码识别系统开源了,很好玩,建议尝试

热门文章

  1. CDH6.2.0安装phoenix5.0.0
  2. 下列字符是c语言的保留字是,下列字符序列中,是C语言保留字的是().
  3. 将excel表格数据导入到Matlab并生成折线图
  4. xp系统扫描仪服务器,安装XP系统后扫描仪不能用了如何解决
  5. Java爬虫学习--爬取漫画
  6. 小程序毕业设计 基于微信家政服务预约小程序毕业设计开题报告功能参考
  7. ubuntu一个命令强制删除无法删除的文件
  8. 在一架天车中,透视5G时代的钢铁智变
  9. Whitted-Style 光线追踪
  10. Lightroom Classic 教程:如何在 Lightroom 中修复曝光过度的照片?