## 音視頻學習 (十一) Android 端實現 rtmp 推流
## 前言
咱們回顧前面 2 篇文章,主要講解了如何搭建 rtmp[直播服務器](https://juejin.im/post/6844904068964450318),和如何開發一款具有拉流功能的 Android[播放器](https://juejin.im/post/6844904066171011085)。那么現在有了播放端和直播服務器還缺少推流端。該篇文章我們就一起來實現 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的id
public 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() {
@Override
public 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-facing
result = (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 {
@Override
public 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() {
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(SopCastConstant.TAG, "SurfaceView destroy");
CameraHolder.instance().stopPreview();
CameraHolder.instance().releaseCamera();
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(SopCastConstant.TAG, "SurfaceView created");
}
@Override
public 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](https://sourceforge.net/projects/faac/files/faac-src/)通過 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_64
FLAGS="-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=no
make 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_lib
log)
#推流 so
target_link_libraries(
#播放 so
ykpusher
# # 寫了此命令不用在乎添加 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也不是adif
config->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) {
//開始打包 rtmp
int bodySize = 2 + bytelen;
RTMPPacket *packet = new RTMPPacket;
RTMPPacket_Alloc(packet, bodySize);
//雙聲道
packet->m_body[0] = 0xAF;
if (mChannels == 1) {
packet->m_body[0] = 0xAE;
}
//編碼出的音頻 都是 0x01
packet->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);
//語音采樣率 44100
format.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 中取出來,?? 左邊的小方塊代表各個 inputBuffer 元素,右邊的小方塊則代表各個 outputBuffer 元素。詳細介紹可以看[MediaCodec 類介紹](https://developer.android.com/reference/android/media/MediaCodec)。

代碼具體實現如下:
~~~
//input:PCM
synchronized void offerEncoder(byte[] input) {
if(mMediaCodec == null) {
return;
}
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(12000);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(input);
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, 0, 0);
}
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 12000);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
if(mListener != null) {
//將 AAC 數據回調出去
mListener.onAudioEncode(outputBuffer, mBufferInfo);
}
//釋放當前內部編碼內存
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 0);
}
}
復制代碼
~~~
#### 6\. AAC 打包為 flv
~~~
@Override
public void onAudioData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
if (packetListener == null || !isHeaderWrite || !isKeyFrameWrite) {
return;
}
bb.position(bi.offset);
bb.limit(bi.offset + bi.size);
byte[] audio = new byte[bi.size];
bb.get(audio);
int size = AUDIO_HEADER_SIZE + audio.length;
ByteBuffer buffer = ByteBuffer.allocate(size);
FlvPackerHelper.writeAudioTag(buffer, audio, false, mAudioSampleSize);
packetListener.onPacket(buffer.array(), AUDIO);
}
public static void writeAudioTag(ByteBuffer buffer, byte[] audioInfo, boolean isFirst, int audioSize) {
//寫入音頻頭信息
writeAudioHeader(buffer, isFirst, audioSize);
//寫入音頻信息
buffer.put(audioInfo);
}
復制代碼
~~~
#### 7\. 釋放編碼器
在使用完 MediaCodec 編碼器之后,就需要停止運行并釋放編碼器,代碼如下:
~~~
synchronized public void stop() {
if (mMediaCodec != null) {
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
}
}
復制代碼
~~~
## 視頻編碼
Camera 采集完之后需要對 YUV 數據進行實時的編碼 (軟編利用[x264](https://www.videolan.org/developers/x264.html)通過 NDK 交叉編譯靜態庫、硬編使用 Android SDK MediaCodec 進行編碼)。
### 軟編
視頻軟編這里們用主流的編碼庫 x264 進行編碼 H264 視頻格式數據。
#### 1\. 交叉編譯 x264
##### 1.1 下載 x264
~~~
//方式 一
git clone https://code.videolan.org/videolan/x264.git
//方式 二
wget ftp://ftp.videolan.org/pub/x264/snapshots/last_x264.tar.bz2
復制代碼
~~~
##### 1.2 編寫編譯腳本
在編寫腳本之前需要在 configure 中添加一處代碼`-Werror=implicit-function-declaration`,如下所示:

交叉編譯腳本如下:
~~~
#!/bin/bash
#打包地址
PREFIX=./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_64
FLAGS="-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"
#--disable-cli 不需要命令行工具
#--enable-static 靜態庫
./configure \
--prefix=$PREFIX \
--disable-cli \
--enable-static \
--enable-pic \
--host=arm-linux \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--sysroot=$NDK_ROOT/platforms/android-17/arch-arm \
--extra-cflags="$FLAGS"
make clean
make install
復制代碼
~~~
#### 2\. CMakeList.txt 配置
~~~
cmake_minimum_required(VERSION 3.4.1)
#視頻編碼器
set(x264 ${CMAKE_SOURCE_DIR}/x264)
#加載 x264 頭文件目錄
include_directories(${x264}/include)
#指定 x264 靜態庫文件目錄
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${x264}/libs/${CMAKE_ANDROID_ARCH_ABI}")
#批量添加自己編寫的 cpp 文件,不要把 *.h 加入進來了
file(GLOB Player_CPP ${ykplayer}/*.cpp)
file(GLOB Push_CPP ${ykpusher}/*.cpp)
#添加自己編寫 cpp 源文件生成動態庫
add_library(ykpusher SHARED ${Push_CPP})
#找系統中 NDK log庫
find_library(log_lib
log)
#推流 so
target_link_libraries(
#播放 so
ykpusher
# # 寫了此命令不用在乎添加 ffmpeg lib 順序問題導致應用崩潰
# -Wl,--start-group
# avcodec avfilter avformat avutil swresample swscale
# -Wl,--end-group
# z
#推流庫
rtmp
#視頻編碼
x264
#語音編碼
faac
#本地庫
android
${log_lib}
)
復制代碼
~~~
#### 3\. 配置并打開 x264 編碼器
~~~
void VideoEncoderChannel::setVideoEncoderInfo(int width, int height, int fps, int bit) {
pthread_mutex_lock(&mMutex);
this->mWidth = width;
this->mHeight = height;
this->mFps = fps;
this->mBit = bit;
this->mY_Size = width * height;
this->mUV_Size = mY_Size / 4;
//如果編碼器已經存在,需要釋放
if (mVideoCodec || pic_in) {
release();
}
//打開x264編碼器
//x264編碼器的屬性
x264_param_t param;
//2: 最快
//3: 無延遲編碼
x264_param_default_preset(¶m, x264_preset_names[0], x264_tune_names[7]);
//base_line 3.2 編碼規格
param.i_level_idc = 32;
//輸入數據格式
param.i_csp = X264_CSP_I420;
param.i_width = width;
param.i_height = height;
//無b幀
param.i_bframe = 0;
//參數i_rc_method表示碼率控制,CQP(恒定質量),CRF(恒定碼率),ABR(平均碼率)
param.rc.i_rc_method = X264_RC_ABR;
//碼率(比特率,單位Kbps)
param.rc.i_bitrate = mBit;
//瞬時最大碼率
param.rc.i_vbv_max_bitrate = mBit * 1.2;
//設置了i_vbv_max_bitrate必須設置此參數,碼率控制區大小,單位kbps
param.rc.i_vbv_buffer_size = mBit;
//幀率
param.i_fps_num = fps;
param.i_fps_den = 1;
param.i_timebase_den = param.i_fps_num;
param.i_timebase_num = param.i_fps_den;
// param.pf_log = x264_log_default2;
//用fps而不是時間戳來計算幀間距離
param.b_vfr_input = 0;
//幀距離(關鍵幀) 2s一個關鍵幀
param.i_keyint_max = fps * 2;
// 是否復制sps和pps放在每個關鍵幀的前面 該參數設置是讓每個關鍵幀(I幀)都附帶sps/pps。
param.b_repeat_headers = 1;
//多線程
param.i_threads = 1;
x264_param_apply_profile(¶m, "baseline");
//打開編碼器
mVideoCodec = x264_encoder_open(¶m);
pic_in = new x264_picture_t;
x264_picture_alloc(pic_in, X264_CSP_I420, width, height);
//相當于重啟編碼器
isStart = true;
pthread_mutex_unlock(&mMutex);
}
復制代碼
~~~
#### 4\. 開始編碼
~~~
void VideoEncoderChannel::onEncoder() {
while (isStart) {
if (!mVideoCodec) {
continue;
}
int8_t *data = 0;
mVideoPackets.pop(data);
if (!data) {
LOGE("獲取 YUV 數據錯誤");
continue;
}
//copy Y 數據
memcpy(this->pic_in->img.plane[0], data, mY_Size);
//拿到 UV 數據
for (int i = 0; i < mUV_Size; ++i) {
//拿到 u 數據
*(pic_in->img.plane[1] + i) = *(data + mY_Size + i * 2 + 1);
//拿到 v 數據
*(pic_in->img.plane[2] + i) = *(data + mY_Size + i * 2);
}
//編碼出來的數據
x264_nal_t *pp_nal;
//編碼出來的幀數量
int pi_nal = 0;
x264_picture_t pic_out;
//開始編碼
int ret = x264_encoder_encode(mVideoCodec, &pp_nal, &pi_nal, pic_in, &pic_out);
if (!ret) {
LOGE("編碼失敗");
continue;
}
//如果是關鍵幀
int sps_len = 0;
int pps_len = 0;
uint8_t sps[100];
uint8_t pps[100];
for (int i = 0; i < pi_nal; ++i) {
if (pp_nal[i].i_type == NAL_SPS) {
//排除掉 h264的間隔 00 00 00 01
sps_len = pp_nal[i].i_payload - 4;
memcpy(sps, pp_nal[i].p_payload + 4, sps_len);
} else if (pp_nal[i].i_type == NAL_PPS) {
pps_len = pp_nal[i].i_payload - 4;
memcpy(pps, pp_nal[i].p_payload + 4, pps_len);
//pps肯定是跟著sps的
sendSpsPps(sps, pps, sps_len, pps_len);
} else {
//編碼之后的 H264 數據
sendFrame(pp_nal[i].i_type, pp_nal[i].p_payload, pp_nal[i].i_payload, 0);
}
}
}
}
/**
* 發送 sps pps
* @param sps 編碼第一幀數據
* @param pps 編碼第二幀數據
* @param sps_len 編碼第一幀數據的長度
* @param pps_len 編碼第二幀數據的長度
*/
void VideoEncoderChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) {
int bodySize = 13 + sps_len + 3 + pps_len;
RTMPPacket *packet = new RTMPPacket;
//
RTMPPacket_Alloc(packet, bodySize);
int i = 0;
//固定頭
packet->m_body[i++] = 0x17;
//類型
packet->m_body[i++] = 0x00;
//composition time 0x000000
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
//版本
packet->m_body[i++] = 0x01;
//編碼規格
packet->m_body[i++] = sps[1];
packet->m_body[i++] = sps[2];
packet->m_body[i++] = sps[3];
packet->m_body[i++] = 0xFF;
//整個sps
packet->m_body[i++] = 0xE1;
//sps長度
packet->m_body[i++] = (sps_len >> 8) & 0xff;
packet->m_body[i++] = sps_len & 0xff;
memcpy(&packet->m_body[i], sps, sps_len);
i += sps_len;
//pps
packet->m_body[i++] = 0x01;
packet->m_body[i++] = (pps_len >> 8) & 0xff;
packet->m_body[i++] = (pps_len) & 0xff;
memcpy(&packet->m_body[i], pps, pps_len);
//視頻
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = bodySize;
//隨意分配一個管道(盡量避開rtmp.c中使用的)
packet->m_nChannel = 0x10;
//sps pps沒有時間戳
packet->m_nTimeStamp = 0;
//不使用絕對時間
packet->m_hasAbsTimestamp = 0;
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
if (mVideoCallback && isStart)
mVideoCallback(packet);
}
/**
* 發送視頻幀 -- 關鍵幀
* @param type
* @param payload
* @param i_playload
*/
void VideoEncoderChannel::sendFrame(int type, uint8_t *payload, int i_payload, long timestamp) {
if (payload[2] == 0x00) {
i_payload -= 4;
payload += 4;
} else {
i_payload -= 3;
payload += 3;
}
//看表
int bodySize = 9 + i_payload;
RTMPPacket *packet = new RTMPPacket;
//
RTMPPacket_Alloc(packet, bodySize);
packet->m_body[0] = 0x27;
if (type == NAL_SLICE_IDR) {
packet->m_body[0] = 0x17;
LOGE("關鍵幀");
}
//類型
packet->m_body[1] = 0x01;
//時間戳
packet->m_body[2] = 0x00;
packet->m_body[3] = 0x00;
packet->m_body[4] = 0x00;
//數據長度 int 4個字節
packet->m_body[5] = (i_payload >> 24) & 0xff;
packet->m_body[6] = (i_payload >> 16) & 0xff;
packet->m_body[7] = (i_payload >> 8) & 0xff;
packet->m_body[8] = (i_payload) & 0xff;
//圖片數據
memcpy(&packet->m_body[9], payload, i_payload);
packet->m_hasAbsTimestamp = 0;
packet->m_nBodySize = bodySize;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nChannel = 0x10;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
if (mVideoCallback && isStart)
mVideoCallback(packet);//回調給 RTMP 模塊
}
復制代碼
~~~
#### 5\. 釋放編碼器
當我們不需要編碼的時候需要釋放編碼器,代碼如下:
~~~
x264_encoder_close(mVideoCodec);
復制代碼
~~~
### 硬編
在 Android 4.3 系統以后,用 MediaCodec 編碼視頻成為了主流的使用場景,盡管 Android 的碎片化很嚴重,會導致一些兼容性問題,但是硬件編碼器的性能以及速度是非常可觀的,并且在 4.3 系統之后可以通過 Surface 來配置編碼器的輸入,大大降低了顯存到內存的交換過程所使用的時間,從而使得整個應用的體驗得到大大提升。由于輸入和輸出已經確定,因此接下來將直接編寫 MediaCodec 編碼視頻幀的過程。
#### 1\. 創建`video/avc`類型的硬編碼器
~~~
mediaCodec = MediaCodec.createEncoderByType(videoConfiguration.mime);
復制代碼
~~~
#### 2\. 配置視頻編碼器
~~~
public static MediaCodec getVideoMediaCodec(VideoConfiguration videoConfiguration) {
int videoWidth = getVideoSize(videoConfiguration.width);
int videoHeight = getVideoSize(videoConfiguration.height);
MediaFormat format = MediaFormat.createVideoFormat(videoConfiguration.mime, videoWidth, videoHeight);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, videoConfiguration.maxBps* 1024);
int fps = videoConfiguration.fps;
//設置攝像頭預覽幀率
if(BlackListHelper.deviceInFpsBlacklisted()) {
SopCastLog.d(SopCastConstant.TAG, "Device in fps setting black list, so set mediacodec fps 15");
fps = 15;
}
format.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, videoConfiguration.ifi);
format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
format.setInteger(MediaFormat.KEY_COMPLEXITY, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
MediaCodec mediaCodec = null;
try {
mediaCodec = MediaCodec.createEncoderByType(videoConfiguration.mime);
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\. 開啟視頻編碼器
~~~
mMediaCodec.start();
復制代碼
~~~
#### 4\. 拿到編碼之后的數據
~~~
private void drainEncoder() {
ByteBuffer[] outBuffers = mMediaCodec.getOutputBuffers();
while (isStarted) {
encodeLock.lock();
if(mMediaCodec != null) {
int outBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 12000);
if (outBufferIndex >= 0) {
ByteBuffer bb = outBuffers[outBufferIndex];
if (mListener != null) { //將編碼好的 H264 數據回調出去
mListener.onVideoEncode(bb, mBufferInfo);
}
mMediaCodec.releaseOutputBuffer(outBufferIndex, false);
} else {
try {
// wait 10ms
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
encodeLock.unlock();
} else {
encodeLock.unlock();
break;
}
}
}
復制代碼
~~~
#### 5\. H264 打包為 flv
~~~
//接收 H264 數據
@Override
public void onVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
mAnnexbHelper.analyseVideoData(bb, bi);
}
/**
* 將硬編得到的視頻數據進行處理生成每一幀視頻數據,然后傳給flv打包器
* @param bb 硬編后的數據buffer
* @param bi 硬編的BufferInfo
*/
public void analyseVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
bb.position(bi.offset);
bb.limit(bi.offset + bi.size);
ArrayList<byte[]> frames = new ArrayList<>();
boolean isKeyFrame = false;
while(bb.position() < bi.offset + bi.size) {
byte[] frame = annexbDemux(bb, bi);
if(frame == null) {
LogUtils.e("annexb not match.");
break;
}
// ignore the nalu type aud(9)
if (isAccessUnitDelimiter(frame)) {
continue;
}
// for pps
if(isPps(frame)) {
mPps = frame;
continue;
}
// for sps
if(isSps(frame)) {
mSps = frame;
continue;
}
// for IDR frame
if(isKeyFrame(frame)) {
isKeyFrame = true;
} else {
isKeyFrame = false;
}
byte[] naluHeader = buildNaluHeader(frame.length);
frames.add(naluHeader);
frames.add(frame);
}
if (mPps != null && mSps != null && mListener != null && mUploadPpsSps) {
if(mListener != null) {
mListener.onSpsPps(mSps, mPps);
}
mUploadPpsSps = false;
}
if(frames.size() == 0 || mListener == null) {
return;
}
int size = 0;
for (int i = 0; i < frames.size(); i++) {
byte[] frame = frames.get(i);
size += frame.length;
}
byte[] data = new byte[size];
int currentSize = 0;
for (int i = 0; i < frames.size(); i++) {
byte[] frame = frames.get(i);
System.arraycopy(frame, 0, data, currentSize, frame.length);
currentSize += frame.length;
}
if(mListener != null) {
mListener.onVideo(data, isKeyFrame);
}
}
復制代碼
~~~
這個方法主要是從編碼后的數據中解析得到NALU,然后判斷NALU的類型,最后再把數據回調給 FlvPacker 去處理。
處理 spsPps:
~~~
@Override
public void onSpsPps(byte[] sps, byte[] pps) {
if (packetListener == null) {
return;
}
//寫入第一個視頻信息
writeFirstVideoTag(sps, pps);
//寫入第一個音頻信息
writeFirstAudioTag();
isHeaderWrite = true;
}
復制代碼
~~~
處理視頻幀:
~~~
@Override
public void onVideo(byte[] video, boolean isKeyFrame) {
if (packetListener == null || !isHeaderWrite) {
return;
}
int packetType = INTER_FRAME;
if (isKeyFrame) {
isKeyFrameWrite = true;
packetType = KEY_FRAME;
}
//確保第一幀是關鍵幀,避免一開始出現灰色模糊界面
if (!isKeyFrameWrite) {
return;
}
int size = VIDEO_HEADER_SIZE + video.length;
ByteBuffer buffer = ByteBuffer.allocate(size);
FlvPackerHelper.writeH264Packet(buffer, video, isKeyFrame);
packetListener.onPacket(buffer.array(), packetType);
}
復制代碼
~~~
#### 6\. 釋放編碼器,并釋放 Surface
~~~
//釋放編碼器
private void releaseEncoder() {
if (mMediaCodec != null) {
mMediaCodec.signalEndOfInputStream();
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
}
if (mInputSurface != null) {
mInputSurface.release();
mInputSurface = null;
}
}
//釋放 OpenGL ES 渲染,Surface
public void release() {
EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
EGL14.eglReleaseThread();
EGL14.eglTerminate(mEGLDisplay);
mSurface.release();
mSurface = null;
mEGLDisplay = null;
mEGLContext = null;
mEGLSurface = null;
}
復制代碼
~~~
## rtmp 推流
注: 實際項目 rtmp 需要先連接上才有后續操作。
rtmp 模塊我們已在開發[播放器](https://juejin.im/post/6844904066171011085)的時候,將它和 ffmpeg 一并編譯了。所以我們直接使用上次的靜態庫和頭文件就可以了,如果對 rtmp 協議不了解的可以參考上一篇文章,里面也有介紹[搭建 RTMP 直播服務器](https://juejin.im/post/6844904068964450318)。
到這里軟編碼和硬編碼數據都已準備好了現在,需要發送給 rtmp 模塊,也就是在 native 中,先看 java 發送出口:
~~~
/**
* 打包之后的數據,和裸流數據
*
* @param data
* @param type
*/
@Override
public void onData(byte[] data, int type) {
if (type == RtmpPacker.FIRST_AUDIO || type == RtmpPacker.AUDIO) {//音頻 AAC 數據,已打包
mPusherManager.pushAACData(data, data.length, type);
} else if (type == RtmpPacker.FIRST_VIDEO ||
type == RtmpPacker.INTER_FRAME || type == RtmpPacker.KEY_FRAME) {//H264 視頻數據,已打包
mPusherManager.pushH264(data, type, 0);
} else if (type == RtmpPacker.PCM) { //PCM 裸流數據
mPusherManager.pushPCM(data);
} else if (type == RtmpPacker.YUV) { //YUV 裸流數據
mPusherManager.pushYUV(data);
}
}
/**
* 發送 H264 數據
*
* @param h264
*/
public native void pushH264(byte[] h264, int type, long timeStamp);
/**
* @param audio 直接推編碼完成之后的音頻流
* @param length
* @param timestamp
*/
public native void pushAACData(byte[] audio, int length, int timestamp);
/**
* 發送 PCM 原始數據
*
* @param audioData
*/
public native void native_pushAudio(byte[] audioData);
/**
* push 視頻原始 nv21
*
* @param data
*/
public native void native_push_video(byte[] data);
復制代碼
~~~
### 1\. Rtmp 鏈接
Rtmp 底層是 TCP 協議,所以你可以使用 Java Socket 進行連接,也可以使用 c++ librtmp 庫來進行連接,咱們這里就使用 librtmp 來進行連接。
~~~
/**
* 真正 rtmp 連接的函數
*/
void RTMPModel::onConnect() {
...
//1. 初始化
RTMP_Init(rtmp);
//2. 設置rtmp地址
int ret = RTMP_SetupURL(rtmp, this->url)
//3. 確認寫入 rtmp
RTMP_EnableWrite(rtmp);
//4. 開始鏈接
ret = RTMP_Connect(rtmp, 0);
//5. 連接成功之后需要連接一個流
ret = RTMP_ConnectStream(rtmp, 0);
...
}
復制代碼
~~~
### 2\. Native 音頻模塊接收 AAC Flv 打包數據
~~~
/**
* 直接推送 AAC 硬編碼
* @param data
*/
void AudioEncoderChannel::pushAAC(u_char *data, int dataLen, long timestamp) {
RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));
RTMPPacket_Alloc(packet, dataLen);
RTMPPacket_Reset(packet);
packet->m_nChannel = 0x05; //音頻
memcpy(packet->m_body, data, dataLen);
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
packet->m_hasAbsTimestamp = FALSE;
packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet->m_nBodySize = dataLen;
if (mAudioCallback)
mAudioCallback(packet); //發送給 rtmp 模塊
}
復制代碼
~~~
### 3\. Native 視頻模塊接收 H264 Flv 打包數據
~~~
/**
*
* @param type 視頻幀類型
* @param buf H264
* @param len H264 長度
*/
void VideoEncoderChannel::sendH264(int type, uint8_t *data, int dataLen, int timeStamp) {
RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));
RTMPPacket_Alloc(packet, dataLen);
RTMPPacket_Reset(packet);
packet->m_nChannel = 0x04; //視頻
if (type == RTMP_PACKET_KEY_FRAME) {
LOGE("視頻關鍵幀");
}
memcpy(packet->m_body, data, dataLen);
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
packet->m_hasAbsTimestamp = FALSE;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = dataLen;
mVideoCallback(packet);//發送給 rtmp 模塊
}
復制代碼
~~~
### 4\. RTMP 發送數據
#### 4.1 將接收到的數據入發送隊列
~~~
//不管是軟編碼還是硬編碼所有發送數據都需要入隊列
void callback(RTMPPacket *packet) {
if (packet) {
if (rtmpModel) {
//設置時間戳
packet->m_nTimeStamp = RTMP_GetTime() - rtmpModel->mStartTime;
rtmpModel->mPackets.push(packet);
}
}
}
復制代碼
~~~
#### 4.2 發送
~~~
/**
* 真正推流的地方
*/
void RTMPModel::onPush() {
RTMPPacket *packet = 0;
while (isStart) {
//從隊列中獲取發送的音視頻數據
mPackets.pop(packet);
if (!readyPushing) {
releasePackets(packet);
return;
}
if (!packet) {
LOGE("獲取失敗");
continue;
}
packet->m_nInfoField2 = rtmp->m_stream_id;
int ret = RTMP_SendPacket(rtmp, packet, 1);
if (!ret) {
LOGE("發送失敗")
if (pushCallback) {
pushCallback->onError(THREAD_CHILD, RTMP_PUSHER_ERROR);
}
return;
}
}
releasePackets(packet);
release();//釋放
}
復制代碼
~~~
### 5\. 關閉 RTMP
當不需要發送音視頻數據的時候需要關閉 rtmp 連接
~~~
void RTMPModel::release() {
isStart = false;
readyPushing = false;
if (rtmp) {
RTMP_DeleteStream(rtmp);
RTMP_Close(rtmp);
RTMP_Free(rtmp);
rtmp = 0;
LOGE("釋放 native 資源");
}
mPackets.clearQueue();
}
復制代碼
~~~
## 簡單談談軟硬編解碼
### 1\. 區別
**軟編碼:**使用 CPU 進行編碼。**硬編碼:**使用 GPU 進行編碼。
### 2\. 比較
**軟編碼:**實現直接、簡單,參數調整方便,升級容易,但 CPU 負載重,性能較硬編碼低,低碼率下質量通常比硬編碼要好一點。**硬編碼:**性能高,低碼率下通常質量低于軟編碼器,但部分產品在 GPU 硬件平臺移植了優秀的軟編碼算法(如X264)的,質量基本等同于軟編碼。
### 3\. 使用場景
**軟編碼:**適用短時間操作,如錄制短視頻等。
**硬編碼:**長時間編碼或者對視頻質量要求高(VOIP 實時通話),可以推薦硬件編碼 (前提是手機性能好)。
## 總結
到這里 Android 端軟編推流,硬編推流都分別實現了。在項目上可以根據實際情況來選擇到底是硬編還是軟編。
硬編我是基于來瘋開源項目進行二次開發:
[Android 推流項目地址](https://github.com/yangkun19921001/NDK_AV_SAMPLE/tree/master/ykav_common)
[Android 拉流項目地址](https://github.com/yangkun19921001/NDK_AV_SAMPLE/blob/master/ykav_sample/src/main/java/com/devyk/ykav_sample/PlayerActivity.java)
## 參考
* [來瘋直播項目](https://github.com/LaiFeng-Android/SopCastComponent)
- 前言
- JNI基礎知識
- C語言知識點總結
- ①基本語法
- ②數據類型
- 枚舉類型
- 自定義類型(類型定義)
- ③格式化輸入輸出
- printf函數
- scanf函數
- 編程規范
- ④變量和常量
- 局部變量和外部變量
- ⑤類型轉換
- ⑥運算符
- ⑦結構語句
- 1、分支結構(選擇語句)
- 2、循環結構
- 退出循環
- break語句
- continue語句
- goto語句
- ⑧函數
- 函數的定義和調用
- 參數
- 函數的返回值
- 遞歸函數
- 零起點學通C語言摘要
- 內部函數和外部函數
- 變量存儲類別
- ⑨數組
- 指針
- 結構體
- 聯合體(共用體)
- 預處理器
- 預處理器的工作原理
- 預處理指令
- 宏定義
- 簡單的宏
- 帶參數的宏
- 預定義宏
- 文件包含
- 條件編譯
- 內存中的數據
- C語言讀文件和寫文件
- JNI知識點總結
- 前情回顧
- JNI規范
- jni開發
- jni開發中常見的錯誤
- JNI實戰演練
- C++(CPP)在Android開發中的應用
- 掘金網友總結的音視頻開發知識
- 音視頻學習一、C 語言入門
- 1.程序結構
- 2. 基本語法
- 3. 數據類型
- 4. 變量
- 5. 常量
- 6. 存儲類型關鍵字
- 7. 運算符
- 8. 判斷
- 9. 循環
- 10. 函數
- 11. 作用域規則
- 12. 數組
- 13. 枚舉
- 14. 指針
- 15. 函數指針與回調函數
- 16. 字符串
- 17. 結構體
- 18. 共用體
- 19. typedef
- 20. 輸入 & 輸出
- 21.文件讀寫
- 22. 預處理器
- 23.頭文件
- 24. 強制類型轉換
- 25. 錯誤處理
- 26. 遞歸
- 27. 可變參數
- 28. 內存管理
- 29. 命令行參數
- 總結
- 音視頻學習二 、C++ 語言入門
- 1. 基本語法
- 2. C++ 關鍵字
- 3. 數據類型
- 4. 變量類型
- 5. 變量作用域
- 6. 常量
- 7. 修飾符類型
- 8. 存儲類
- 9. 運算符
- 10. 循環
- 11. 判斷
- 12. 函數
- 13. 數學運算
- 14. 數組
- 15. 字符串
- 16. 指針
- 17. 引用
- 18. 日期 & 時間
- 19. 輸入輸出
- 20. 數據結構
- 21. 類 & 對象
- 22. 繼承
- 23. 重載運算符和重載函數
- 24. 多態
- 25. 數據封裝
- 26. 接口(抽象類)
- 27. 文件和流
- 28. 異常處理
- 29. 動態內存
- 30. 命名空間
- 31. 預處理器
- 32. 多線程
- 總結
- 音視頻學習 (三) JNI 從入門到掌握
- 音視頻學習 (四) 交叉編譯動態庫、靜態庫的入門學習
- 音視頻學習 (五) Shell 腳本入門
- 音視頻學習 (六) 一鍵編譯 32/64 位 FFmpeg 4.2.2
- 音視頻學習 (七) 掌握音頻基礎知識并使用 AudioTrack、OpenSL ES 渲染 PCM 數據
- 音視頻學習 (八) 掌握視頻基礎知識并使用 OpenGL ES 2.0 渲染 YUV 數據
- 音視頻學習 (九) 從 0 ~ 1 開發一款 Android 端播放器(支持多協議網絡拉流/本地文件)
- 音視頻學習 (十) 基于 Nginx 搭建(rtmp、http)直播服務器
- 音視頻學習 (十一) Android 端實現 rtmp 推流
- 音視頻學習 (十二) 基于 FFmpeg + OpenSLES 實現音頻萬能播放器
- 音視頻學習 (十三) Android 中通過 FFmpeg 命令對音視頻編輯處理(已開源)