系列文章
Android音視頻學(xué)習(xí)系列(一) — JNI從入門(mén)到精通
Android音視頻學(xué)習(xí)系列(二) — 交叉編譯動(dòng)態(tài)庫(kù)、靜態(tài)庫(kù)的入門(mén)
Android音視頻學(xué)習(xí)系列(三) — Shell腳本入門(mén)
Android音視頻學(xué)習(xí)系列(四) — 一鍵編譯32/64位FFmpeg4.2.2
Android音視頻學(xué)習(xí)系列(五) — 掌握音頻基礎(chǔ)知識(shí)并使用AudioTrack、OpenSL ES渲染PCM數(shù)據(jù)
Android音視頻學(xué)習(xí)系列(六) — 掌握視頻基礎(chǔ)知識(shí)并使用OpenGL ES 2.0渲染YUV數(shù)據(jù)
Android音視頻學(xué)習(xí)系列(七) — 從0~1開(kāi)發(fā)一款A(yù)ndroid端播放器(支持多協(xié)議網(wǎng)絡(luò)拉流本地文件)
Android音視頻學(xué)習(xí)系列(八) — 基于Nginx搭建(rtmp、http)直播服務(wù)器
Android音視頻學(xué)習(xí)系列(九) — Android端實(shí)現(xiàn)rtmp推流
Android音視頻學(xué)習(xí)系列(十) — 基于FFmpeg + OpenSL ES實(shí)現(xiàn)音頻萬(wàn)能播放器
前言
前面講解了如何搭建 rtmp 直播服務(wù)器,和如何開(kāi)發(fā)一款具有拉流功能的 Android 播放器。那么現(xiàn)在有了播放端和直播服務(wù)器還缺少推流端。該篇文章我們就一起來(lái)實(shí)現(xiàn) Android 端的 rtmp 推流,想要實(shí)現(xiàn) Android 端推流必須要經(jīng)過(guò)如下幾個(gè)階段,見(jiàn)下圖:

該篇文章主要完成上圖黃顏色功能部分,下面就開(kāi)始進(jìn)入正題,代碼編寫(xiě)了。
項(xiàng)目效果
推流監(jiān)控

軟編碼

硬編碼

文章末尾會(huì)介紹軟硬編解碼。
音頻采集
Android SDK 提供了兩套音頻采集的 API ,分別是 MediaRecorder 、AudioRecord 。前者是一個(gè)上層 API ,它可以直接對(duì)手機(jī)麥克風(fēng)錄入的音頻數(shù)據(jù)進(jìn)行編碼壓縮(如 AMR/MP3) 等,并存儲(chǔ)為文件;后者則更接近底層,能夠更加自由靈活地控制,其可以讓開(kāi)發(fā)者得到內(nèi)存中的 PCM 原始音頻數(shù)據(jù)流。如果想做一個(gè)簡(jiǎn)單的錄音機(jī),輸出音頻文件則推薦使用 MediaRecorder ; 如果需要對(duì)音頻做進(jìn)一步的算法處理,或者需要采用第三方的編碼庫(kù)進(jìn)行編碼,又或者需要用到網(wǎng)絡(luò)傳輸?shù)葓?chǎng)景中,那么只能使用 AudioRecord 或者 OpenSL ES ,其實(shí) MediaRecorder 底層也是調(diào)用了 AudioRecord 與 Android Framework 層的 AudioFlinger 進(jìn)行交互的。而我們?cè)撈膱?chǎng)景更傾向于第二種實(shí)現(xiàn)方式,即使用 AudioRecord 來(lái)采集音頻。
如果想要使用 AudioRecord 這個(gè) API ,則需要在應(yīng)用 AndroidManifest.xml 的配置文件中進(jìn)行如下配置:
<uses-permission android:name="android.permission.RECORD_AUDIO"></uses-permission>
當(dāng)然,如果你想把采集到的 PCM 原始數(shù)據(jù),存儲(chǔ) sdcard 中,還需要額外添加寫(xiě)入權(quán)限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
接下來(lái)了解一下 AudioRecord 的工作流程。
1. 初始化 AudioRecord
首先來(lái)看一下 AudioRecord 的配置參數(shù),AudioRecord 是通過(guò)構(gòu)造函數(shù)來(lái)配置參數(shù)的,其函數(shù)原型如下:
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes)
上述參數(shù)所代表的函數(shù)及其在各種場(chǎng)景下應(yīng)該傳遞的值的含義參考如下說(shuō)明:
audioSource: 該參數(shù)指的是音頻采集的輸入源,可選值以常量的形式定義在類 AudioSource (MediaRecorder 中的一個(gè)內(nèi)部類)中,常用的值包過(guò):
DEFAULT(默認(rèn))
VOICE_RECOGNITION (用于語(yǔ)音識(shí)別,等同于默認(rèn))
MIC (由手機(jī)麥克風(fēng)輸入)
VOICE_COMMUNICATION (用于 VOIP 應(yīng)用場(chǎng)景)
sampleRateInHz: 用于指定以多大的采樣頻率來(lái)采集音頻,現(xiàn)在用的最多的兼容最好是 44100 (44.1KHZ)采樣頻率。
channelConfig: 該參數(shù)用于指定錄音器采集幾個(gè)聲道的聲音,可選值以常量的形式定義在 AudioFormat 類中,常用的值包括:
CHANNEL_IN_MONO 單聲道 (移動(dòng)設(shè)備上目前推薦使用)
CHANNEL_IN_STEREO 立體聲
audioFormat: 采樣格式,以常量的形式定義在 AudioFormat 類中,常用的值包括:
ENCODING_PCM_16BIT (16bit 兼容大部分 Android 手機(jī))
ENCODING_PCM_8BIT (8bit)
bufferSizeInBytes: 配置內(nèi)部音頻緩沖區(qū)的大小(配置的緩存值越小,延時(shí)就越低),而具體的大小,有可能在不同的手機(jī)上會(huì)有不同的值,那么可以使用如下 API 進(jìn)行確定緩沖大小:
AudioRecord.getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat);
配置好之后,檢查一下 AudioRecord 當(dāng)前的狀態(tài)是否可以進(jìn)行錄制,可以通過(guò) AudioRecord##getState 來(lái)獲取當(dāng)前的狀態(tài):
STATE_UNINITIALIZED 還沒(méi)有初始化,或者初始化失敗了
STATE_INITIALIZED 已經(jīng)初始化成功了。
2. 開(kāi)啟采集
創(chuàng)建好 AudioRecord 之后,就可以開(kāi)啟音頻數(shù)據(jù)的采集了,可以通過(guò)調(diào)用下面的函數(shù)進(jìn)行控制麥克風(fēng)的采集:
mAudioRecord.startRecording();
3. 提取數(shù)據(jù)
執(zhí)行完上一步之后,需要開(kāi)啟一個(gè)子線程用于不斷的從 AudioRecord 緩沖區(qū)讀取 PCM 數(shù)據(jù),調(diào)用如下函數(shù)進(jìn)行讀取數(shù)據(jù):
int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes);
4. 停止采集
如果想要停止采集,那么只需要調(diào)用 AudioRecord 的 stop 方法來(lái)實(shí)現(xiàn),最后可以通過(guò)一個(gè)變量先控制子線程停止讀取數(shù)據(jù),然后在調(diào)用 stop 停止最后釋放 AudioRecord 實(shí)例。
public void stopEncode() {
//停止的變量標(biāo)記
mStopFlag = true;
if(mAudioEncoder != null) {
//停止采集
mAudioEncoder.stop();
//釋放內(nèi)存
mAudioEncoder = null;
}
}
視頻采集
視頻畫(huà)面的采集主要是使用各個(gè)平臺(tái)提供的攝像頭 API 來(lái)實(shí)現(xiàn)的,在為攝像頭設(shè)置了合適的參數(shù)之后,將攝像頭實(shí)時(shí)采集的視頻幀渲染到屏幕上提供給用戶預(yù)覽,然后將該視頻幀傳遞給編碼通道,進(jìn)行編碼。
1. 權(quán)限配置
<uses-permission android:name="android.permission.CAMERA"></uses-permission>
2. 打開(kāi)攝像頭
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 檢查攝像頭的個(gè)數(shù)
檢查完攝像頭服務(wù)后,還需要檢查手機(jī)上攝像頭的個(gè)數(shù),如果個(gè)數(shù)為 0,則說(shuō)明手機(jī)上沒(méi)有攝像頭,這樣的話也是不能進(jìn)行后續(xù)操作的。
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;
}
在上面的方法中,需要傳入一個(gè)是否先開(kāi)啟背面攝像頭的 boolean 變量,如果變量為 true,則把背面攝像頭放在列表第一個(gè),之后打開(kāi)攝像頭的時(shí)候,直接獲取列表中第一個(gè)攝像頭相關(guān)參數(shù),然后進(jìn)行打開(kāi)。這樣的設(shè)計(jì)使得切換攝像頭也變得十分簡(jiǎn)單,切換攝像頭時(shí),先關(guān)閉當(dāng)前攝像頭,然后變化攝像頭列表中的順序,然后再打開(kāi)攝像頭即可,也就是每次打開(kāi)攝像頭都打開(kāi)攝像頭列表中第一個(gè)攝像頭參數(shù)所指向的攝像頭。
2.3 打開(kāi)攝像頭
打開(kāi)攝像頭之前,先從攝像頭列表中獲取第一個(gè)攝像頭參數(shù),之后根據(jù)參數(shù)中的 CameraId 來(lái)打開(kāi)攝像頭,打開(kāi)成功后改變相關(guān)狀態(tài)。相關(guān)代碼如下:
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) 拋出異常則說(shuō)明Camera 不可用,否則說(shuō)明 Camera 可用,但是在一些手機(jī)上 Camera.open(cameraData.cameraID) 不是拋出異常,而是返回 null。
3. 配置攝像頭參數(shù)
在給攝像頭設(shè)置參數(shù)后,需要記錄這些參數(shù),以方便其他地方使用。比如記錄當(dāng)前攝像頭是否有閃光點(diǎn),從而可以決定 UI 界面上是否顯示打開(kāi)閃光燈按鈕。在直播項(xiàng)目中使用 CameraData 來(lái)記錄這些參數(shù),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; //區(qū)分前后攝像頭
public int cameraWidth; //camera的采集寬度
public int cameraHeight; //camera的采集高度
public boolean hasLight; //camera是否有閃光燈
public int orientation; //camera旋轉(zhuǎn)角度
public boolean supportTouchFocus; //camera是否支持手動(dòng)對(duì)焦
public boolean touchFocusMode; //camera是否處在自動(dòng)對(duì)焦模式
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;
}
}
給攝像頭設(shè)置參數(shù)的時(shí)候,有一點(diǎn)需要注意:設(shè)置的參數(shù)不生效會(huì)拋出異常,因此需要每個(gè)參數(shù)單獨(dú)設(shè)置,這樣就避免一個(gè)參數(shù)不生效后拋出異常,導(dǎo)致之后所有的參數(shù)都沒(méi)有設(shè)置。
4. 攝像頭開(kāi)啟預(yù)覽
設(shè)置預(yù)覽界面有兩種方式:1、通過(guò) SurfaceView 顯示;2、通過(guò) GLSurfaceView 顯示。當(dāng)為 SurfaceView 顯示時(shí),需要傳給 Camera 這個(gè) SurfaceView 的 SurfaceHolder。當(dāng)使用 GLSurfaceView 顯示時(shí),需要使用Renderer 進(jìn)行渲染,先通過(guò) OpenGL 生成紋理,通過(guò)生成紋理的紋理 id 生成 SurfaceTexture ,將SurfaceTexture 交給 Camera ,那么在 Render 中便可以使用這個(gè)紋理進(jìn)行相應(yīng)的渲染,最后通過(guò)GLSurfaceView 顯示。
4.1 設(shè)置預(yù)覽回調(diào)
public static void setPreviewFormat(Camera camera, Camera.Parameters parameters) {
//設(shè)置預(yù)覽回調(diào)的圖片格式
try {
parameters.setPreviewFormat(ImageFormat.NV21);
camera.setParameters(parameters);
} catch (Exception e) {
e.printStackTrace();
}
}
當(dāng)設(shè)置預(yù)覽好預(yù)覽回調(diào)的圖片格式后,需要設(shè)置預(yù)覽回調(diào)的 Callback。
Camera.PreviewCallback myCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//得到相應(yīng)的圖片數(shù)據(jù)
//Do something
}
};
public static void setPreviewCallback(Camera camera, Camera.PreviewCallback callback) {
camera.setPreviewCallback(callback);
}
Android 推薦的 PreViewFormat 是 NV21,在 PreviewCallback 中會(huì)返回 Preview 的 N21 圖片。如果是軟編的話,由于 H264 支持 I420 的圖片格式,因此需要將 N21格式轉(zhuǎn)為 I420 格式,然后交給 x264 編碼庫(kù)。如果是硬編的話,由于 Android 硬編編碼器支持 I420(COLOR_FormatYUV420Planar) 和NV12(COLOR_FormatYUV420SemiPlanar),因此可以將 N21 的圖片轉(zhuǎn)為 I420 或者 NV12 ,然后交給硬編編碼器。
4.2 設(shè)置預(yù)覽圖像大小
在攝像頭相關(guān)處理中,一個(gè)比較重要的是 屏幕顯示大小和攝像頭預(yù)覽大小比例不一致 的處理。在 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();
}
}
在設(shè)置好最適合的 PreviewSize 之后,將 size 信息存儲(chǔ)在 CameraData 中。當(dāng)選擇了 SurfaceView 顯示的方式,可以將 SurfaceView 放置在一個(gè) LinearLayout 中,然后根據(jù)攝像頭 PreviewSize 的比例改變 SurfaceView 的大小,從而使得兩者比例一致,確保圖像正常。當(dāng)選擇了GLSurfaceView 顯示的時(shí)候,可以通過(guò)裁剪紋理,使得紋理的大小比例和 GLSurfaceView 的大小比例保持一致,從而確保圖像顯示正常。
4.3 圖像旋轉(zhuǎn)
在 Android 中攝像頭出來(lái)的圖像需要進(jìn)行一定的旋轉(zhuǎn),然后才能交給屏幕顯示,而且如果應(yīng)用支持屏幕旋轉(zhuǎn)的話,也需要根據(jù)旋轉(zhuǎn)的狀況實(shí)時(shí)調(diào)整攝像頭的角度。在 Android 中旋轉(zhuǎn)攝像頭圖像同樣有兩種方法,一是通過(guò)攝像頭的 setDisplayOrientation(result) 方法,一是通過(guò) OpenGL 的矩陣進(jìn)行旋轉(zhuǎn)。下面是通過(guò)setDisplayOrientation(result) 方法進(jìn)行旋轉(zhuǎn)的代碼:
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 設(shè)置預(yù)覽幀率
通過(guò) Camera.Parameters 中 getSupportedPreviewFpsRange() 可以獲得攝像頭支持的幀率變化范圍,從中選取合適的設(shè)置給攝像頭即可。相關(guān)的代碼如下:
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 設(shè)置相機(jī)對(duì)焦
一般攝像頭對(duì)焦的方式有兩種:手動(dòng)對(duì)焦和觸摸對(duì)焦。下面的代碼分別是設(shè)置自動(dòng)對(duì)焦和觸摸對(duì)焦的模式:
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();
}
}
對(duì)于自動(dòng)對(duì)焦這樣設(shè)置后就完成了工作,但是對(duì)于觸摸對(duì)焦則需要設(shè)置對(duì)應(yīng)的對(duì)焦區(qū)域。要準(zhǔn)確地設(shè)置對(duì)焦區(qū)域,有三個(gè)步驟:一、得到當(dāng)前點(diǎn)擊的坐標(biāo)位置;二、通過(guò)點(diǎn)擊的坐標(biāo)位置轉(zhuǎn)換到攝像頭預(yù)覽界面坐標(biāo)系統(tǒng)上的坐標(biāo);三、根據(jù)坐標(biāo)生成對(duì)焦區(qū)域并且設(shè)置給攝像頭。整個(gè)攝像頭預(yù)覽界面定義了如下的坐標(biāo)系統(tǒng),對(duì)焦區(qū)域也需要對(duì)應(yīng)到這個(gè)坐標(biāo)系統(tǒng)中。

如果攝像機(jī)預(yù)覽界面是通過(guò) SurfaceView 顯示的則比較簡(jiǎn)單,由于要確保不變形,會(huì)將 SurfaceView 進(jìn)行拉伸,從而使得 SurfaceView 和預(yù)覽圖像大小比例一致,因此整個(gè) SurfaceView 相當(dāng)于預(yù)覽界面,只需要得到當(dāng)前點(diǎn)擊點(diǎn)在整個(gè) SurfaceView 上對(duì)應(yīng)的坐標(biāo),然后轉(zhuǎn)化為相應(yīng)的對(duì)焦區(qū)域即可。如果攝像機(jī)預(yù)覽界面是通過(guò)GLSurfaceView 顯示的則要復(fù)雜一些,由于紋理需要進(jìn)行裁剪,才能使得顯示不變形,這樣的話,我們要還原出整個(gè)預(yù)覽界面的大小,然后通過(guò)當(dāng)前點(diǎn)擊的位置換算成預(yù)覽界面坐標(biāo)系統(tǒng)上的坐標(biāo),然后得到相應(yīng)的對(duì)焦區(qū)域,然后設(shè)置給攝像機(jī)。當(dāng)設(shè)置好對(duì)焦區(qū)域后,通過(guò)調(diào)用 Camera 的 autoFocus() 方法即可完成觸摸對(duì)焦。 整個(gè)過(guò)程代碼量較多,請(qǐng)自行閱讀項(xiàng)目源碼。
4.6 設(shè)置縮放
當(dāng)檢測(cè)到手勢(shì)縮放的時(shí)候,我們往往希望攝像頭也能進(jìn)行相應(yīng)的縮放,其實(shí)這個(gè)實(shí)現(xiàn)還是比較簡(jiǎn)單的。首先需要加入縮放的手勢(shì)識(shí)別,當(dāng)識(shí)別到縮放的手勢(shì)的時(shí)候,根據(jù)縮放的大小來(lái)對(duì)攝像頭進(jìn)行縮放。代碼如下所示:
/**
* 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 閃光燈操作
一個(gè)攝像頭可能有相應(yīng)的閃光燈,也可能沒(méi)有,因此在使用閃光燈功能的時(shí)候先要確認(rèn)是否有相應(yīng)的閃光燈。檢測(cè)攝像頭是否有閃光燈的代碼如下:
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 開(kāi)始預(yù)覽
當(dāng)打開(kāi)了攝像頭,并且設(shè)置好了攝像頭相關(guān)的參數(shù)后,便可以通過(guò)調(diào)用 Camera 的 startPreview() 方法開(kāi)始預(yù)覽。有一個(gè)需要說(shuō)明,無(wú)論是 SurfaceView 還是 GLSurfaceView ,都可以設(shè)置 SurfaceHolder.Callback ,當(dāng)界面開(kāi)始顯示的時(shí)候打開(kāi)攝像頭并且開(kāi)始預(yù)覽,當(dāng)界面銷毀的時(shí)候停止預(yù)覽并且關(guān)閉攝像頭,這樣的話當(dāng)程序退到后臺(tái),其他應(yīng)用也能調(diào)用攝像頭。
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. 停止預(yù)覽
停止預(yù)覽只需要釋放掉相機(jī)資源即可:
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 采集完之后需要對(duì) PCM 數(shù)據(jù)進(jìn)行實(shí)時(shí)的編碼 (軟編利用 libfaac 通過(guò) NDK 交叉編譯靜態(tài)庫(kù)、硬編使用 Android SDK MediaCodec 進(jìn)行編碼)。
軟編
語(yǔ)音軟編這里們用主流的編碼庫(kù) libfaac 進(jìn)行編碼 AAC 語(yǔ)音格式數(shù)據(jù)。
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 編寫(xiě)交叉編譯腳本
#!/bin/bash
#打包地址
PREFIX=`pwd`/android/armeabi-v7a
#配置NDK 環(huán)境變量
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)
#語(yǔ)音編碼器
set(faac ${CMAKE_SOURCE_DIR}/faac)
#加載 faac 頭文件目錄
include_directories(${faac}/include)
#指定 faac 靜態(tài)庫(kù)文件目錄
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${faac}/libs/${CMAKE_ANDROID_ARCH_ABI}")
#批量添加自己編寫(xiě)的 cpp 文件,不要把 *.h 加入進(jìn)來(lái)了
file(GLOB Push_CPP ${ykpusher}/*.cpp)
#添加自己編寫(xiě) cpp 源文件生成動(dòng)態(tài)庫(kù)
add_library(ykpusher SHARED ${Push_CPP})
#找系統(tǒng)中 NDK log庫(kù)
find_library(log_lib
log)
#推流 so
target_link_libraries(
#播放 so
ykpusher
# # 寫(xiě)了此命令不用在乎添加 ffmpeg lib 順序問(wèn)題導(dǎo)致應(yīng)用崩潰
# -Wl,--start-group
# avcodec avfilter avformat avutil swresample swscale
# -Wl,--end-group
# z
#推流庫(kù)
rtmp
#視頻編碼
x264
#語(yǔ)音編碼
faac
#本地庫(kù)
android
${log_lib}
)
3. 配置 faac 編碼參數(shù)
//設(shè)置語(yǔ)音軟編碼參數(shù)
void AudioEncoderChannel::setAudioEncoderInfo(int samplesHZ, int channel) {
//如果已經(jīng)初始化,需要釋放
release();
//通道 默認(rèn)單聲道
mChannels = channel;
//打開(kāi)編碼器
//3、一次最大能輸入編碼器的樣本數(shù)量 也編碼的數(shù)據(jù)的個(gè)數(shù) (一個(gè)樣本是16位 2字節(jié))
//4、最大可能的輸出數(shù)據(jù) 編碼后的最大字節(jié)數(shù)
mAudioCodec = faacEncOpen(samplesHZ, channel, &mInputSamples, &mMaxOutputBytes);
if (!mAudioCodec) {
if (mIPushCallback) {
mIPushCallback->onError(THREAD_MAIN, FAAC_ENC_OPEN_ERROR);
}
return;
}
//設(shè)置編碼器參數(shù)
faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(mAudioCodec);
//指定為 mpeg4 標(biāo)準(zhǔn)
config->mpegVersion = MPEG4;
//lc 標(biāo)準(zhǔn)
config->aacObjectType = LOW;
//16位
config->inputFormat = FAAC_INPUT_16BIT;
// 編碼出原始數(shù)據(jù) 既不是adts也不是adif
config->outputFormat = 0;
faacEncSetConfiguration(mAudioCodec, config);
//輸出緩沖區(qū) 編碼后的數(shù)據(jù) 用這個(gè)緩沖區(qū)來(lái)保存
mBuffer = new u_char[mMaxOutputBytes];
//設(shè)置一個(gè)標(biāo)志,用于開(kāi)啟編碼
isStart = true;
}
4. 配置 AAC 包頭
在發(fā)送 rtmp 音視頻包的時(shí)候需要將語(yǔ)音包頭第一個(gè)發(fā)送
/**
* 音頻頭包數(shù)據(jù)
* @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;
//將包頭數(shù)據(jù) copy 到RTMPPacket 中
memcpy(&packet->m_body[2], buf, len);
//是否使用絕對(duì)時(shí)間戳
packet->m_hasAbsTimestamp = FALSE;
//包大小
packet->m_nBodySize = bodySize;
//包類型
packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
//語(yǔ)音通道
packet->m_nChannel = 0x11;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
return packet;
}
5. 開(kāi)始實(shí)時(shí)編碼
void AudioEncoderChannel::encodeData(int8_t *data) {
if (!mAudioCodec || !isStart)//不符合編碼要求,退出
return;
//返回編碼后的數(shù)據(jù)字節(jié)長(zhǎng)度
int bytelen = faacEncEncode(mAudioCodec, reinterpret_cast<int32_t *>(data), mInputSamples,mBuffer, mMaxOutputBytes);
if (bytelen > 0) {
//開(kāi)始打包 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;
//發(fā)送 rtmp packet,回調(diào)給 RTMP send 模塊
mAudioCallback(packet);
}
}
6. 釋放編碼器
在不需要編碼或者退出編碼的時(shí)候需要主動(dòng)釋放編碼器,釋放 native 內(nèi)存,可以通過(guò)如下函數(shù)來(lái)實(shí)現(xiàn)釋放編碼器的操作:
void AudioEncoderChannel::release() {
//退出編碼的標(biāo)志
isStart = false;
//釋放編碼器
if (mAudioCodec) {
//關(guān)閉編碼器
faacEncClose(mAudioCodec);
//釋放緩沖區(qū)
DELETE(mBuffer);
mAudioCodec = 0;
}
}
硬編
軟編碼介紹完了下面利用 Android SDK 自帶的 MediaCodec 函數(shù)進(jìn)行對(duì) PCM 編碼為 AAC 的格式音頻數(shù)據(jù)。使用 MediaCodec 編碼 AAC 對(duì) Android 系統(tǒng)是有要求的,必須是 4.1系統(tǒng)以上,即要求 Android 的版本代號(hào)在 Build.VERSION_CODES.JELLY_BEAN (16) 以上。MediaCodec 是 Android 系統(tǒng)提供的硬件編碼器,它可以利用設(shè)備的硬件來(lái)完成編碼,從而大大提高編碼的效率,還可以降低電量的使用,但是其在兼容性方面不如軟編號(hào),因?yàn)?Android 設(shè)備的鎖片化太嚴(yán)重,所以讀者可以自己衡量在應(yīng)用中是否使用 Android 平臺(tái)的硬件編碼特性。
1. 創(chuàng)建 "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);
}
//語(yǔ)音碼率
format.setInteger(MediaFormat.KEY_BIT_RATE, configuration.maxBps * 1024);
//語(yǔ)音采樣率 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. 開(kāi)啟音頻硬編碼器
void prepareEncoder() {
mMediaCodec = AudioMediaCodec.getAudioMediaCodec(mAudioConfiguration);
mMediaCodec.start();
}
4. 拿到硬編碼輸入(PCM)輸出(AAC) ByteBufferer
到了這一步說(shuō)明,音頻編碼器配置完成并且也成功開(kāi)啟了,現(xiàn)在就可以從 MediaCodec 實(shí)例中獲取兩個(gè) buffer ,一個(gè)是輸入 buffer 一個(gè)是輸出 buffer , 輸入 buffer 類似于 FFmpeg 中的 AVFrame 存放待編碼的 PCM 數(shù)據(jù),輸出 buffer 類似于 FFmpeg 的 AVPacket 編碼之后的 AAC 數(shù)據(jù), 其代碼如下:
//存放的是 PCM 數(shù)據(jù)
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
//存放的是編碼之后的 AAC 數(shù)據(jù)
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
5. 開(kāi)始 PCM 硬編碼為 AAC
到此,所有初始化方法已實(shí)現(xiàn)完畢,下面來(lái)看一下 MediaCodec 的工作原理如下圖所示,左邊 Client 元素代表要將 PCM 放到 inputBuffer 中的某個(gè)具體的 buffer 中去,右邊的 Client 元素代表將編碼之后的原始 AAC 數(shù)據(jù)從 outputBuffer 中的某個(gè)具體 buffer 中取出來(lái),?? 左邊的小方塊代表各個(gè) inputBuffer 元素,右邊的小方塊則代表各個(gè) outputBuffer 元素。詳細(xì)介紹可以看 MediaCodec 類介紹。

代碼具體實(shí)現(xiàn)如下:
//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 數(shù)據(jù)回調(diào)出去
mListener.onAudioEncode(outputBuffer, mBufferInfo);
}
//釋放當(dāng)前內(nèi)部編碼內(nèi)存
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) {
//寫(xiě)入音頻頭信息
writeAudioHeader(buffer, isFirst, audioSize);
//寫(xiě)入音頻信息
buffer.put(audioInfo);
}
7. 釋放編碼器
在使用完 MediaCodec 編碼器之后,就需要停止運(yùn)行并釋放編碼器,代碼如下:
synchronized public void stop() {
if (mMediaCodec != null) {
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
}
}
視頻編碼
Camera 采集完之后需要對(duì) YUV 數(shù)據(jù)進(jìn)行實(shí)時(shí)的編碼 (軟編利用 x264 通過(guò) NDK 交叉編譯靜態(tài)庫(kù)、硬編使用 Android SDK MediaCodec 進(jìn)行編碼)。
軟編
視頻軟編這里們用主流的編碼庫(kù) x264 進(jìn)行編碼 H264 視頻格式數(shù)據(jù)。
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 編寫(xiě)編譯腳本
在編寫(xiě)腳本之前需要在 configure 中添加一處代碼 -Werror=implicit-function-declaration,如下所示:

交叉編譯腳本如下:
#!/bin/bash
#打包地址
PREFIX=./android/armeabi-v7a
#配置NDK 環(huán)境變量
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 靜態(tài)庫(kù)
./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 靜態(tài)庫(kù)文件目錄
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${x264}/libs/${CMAKE_ANDROID_ARCH_ABI}")
#批量添加自己編寫(xiě)的 cpp 文件,不要把 *.h 加入進(jìn)來(lái)了
file(GLOB Player_CPP ${ykplayer}/*.cpp)
file(GLOB Push_CPP ${ykpusher}/*.cpp)
#添加自己編寫(xiě) cpp 源文件生成動(dòng)態(tài)庫(kù)
add_library(ykpusher SHARED ${Push_CPP})
#找系統(tǒng)中 NDK log庫(kù)
find_library(log_lib
log)
#推流 so
target_link_libraries(
#播放 so
ykpusher
# # 寫(xiě)了此命令不用在乎添加 ffmpeg lib 順序問(wèn)題導(dǎo)致應(yīng)用崩潰
# -Wl,--start-group
# avcodec avfilter avformat avutil swresample swscale
# -Wl,--end-group
# z
#推流庫(kù)
rtmp
#視頻編碼
x264
#語(yǔ)音編碼
faac
#本地庫(kù)
android
${log_lib}
)
3. 配置并打開(kāi) 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;
//如果編碼器已經(jīng)存在,需要釋放
if (mVideoCodec || pic_in) {
release();
}
//打開(kāi)x264編碼器
//x264編碼器的屬性
x264_param_t param;
//2: 最快
//3: 無(wú)延遲編碼
x264_param_default_preset(¶m, x264_preset_names[0], x264_tune_names[7]);
//base_line 3.2 編碼規(guī)格
param.i_level_idc = 32;
//輸入數(shù)據(jù)格式
param.i_csp = X264_CSP_I420;
param.i_width = width;
param.i_height = height;
//無(wú)b幀
param.i_bframe = 0;
//參數(shù)i_rc_method表示碼率控制,CQP(恒定質(zhì)量),CRF(恒定碼率),ABR(平均碼率)
param.rc.i_rc_method = X264_RC_ABR;
//碼率(比特率,單位Kbps)
param.rc.i_bitrate = mBit;
//瞬時(shí)最大碼率
param.rc.i_vbv_max_bitrate = mBit * 1.2;
//設(shè)置了i_vbv_max_bitrate必須設(shè)置此參數(shù),碼率控制區(qū)大小,單位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而不是時(shí)間戳來(lái)計(jì)算幀間距離
param.b_vfr_input = 0;
//幀距離(關(guān)鍵幀) 2s一個(gè)關(guān)鍵幀
param.i_keyint_max = fps * 2;
// 是否復(fù)制sps和pps放在每個(gè)關(guān)鍵幀的前面 該參數(shù)設(shè)置是讓每個(gè)關(guān)鍵幀(I幀)都附帶sps/pps。
param.b_repeat_headers = 1;
//多線程
param.i_threads = 1;
x264_param_apply_profile(¶m, "baseline");
//打開(kāi)編碼器
mVideoCodec = x264_encoder_open(¶m);
pic_in = new x264_picture_t;
x264_picture_alloc(pic_in, X264_CSP_I420, width, height);
//相當(dāng)于重啟編碼器
isStart = true;
pthread_mutex_unlock(&mMutex);
}
4. 開(kāi)始編碼
void VideoEncoderChannel::onEncoder() {
while (isStart) {
if (!mVideoCodec) {
continue;
}
int8_t *data = 0;
mVideoPackets.pop(data);
if (!data) {
LOGE("獲取 YUV 數(shù)據(jù)錯(cuò)誤");
continue;
}
//copy Y 數(shù)據(jù)
memcpy(this->pic_in->img.plane[0], data, mY_Size);
//拿到 UV 數(shù)據(jù)
for (int i = 0; i < mUV_Size; ++i) {
//拿到 u 數(shù)據(jù)
*(pic_in->img.plane[1] + i) = *(data + mY_Size + i * 2 + 1);
//拿到 v 數(shù)據(jù)
*(pic_in->img.plane[2] + i) = *(data + mY_Size + i * 2);
}
//編碼出來(lái)的數(shù)據(jù)
x264_nal_t *pp_nal;
//編碼出來(lái)的幀數(shù)量
int pi_nal = 0;
x264_picture_t pic_out;
//開(kāi)始編碼
int ret = x264_encoder_encode(mVideoCodec, &pp_nal, &pi_nal, pic_in, &pic_out);
if (!ret) {
LOGE("編碼失敗");
continue;
}
//如果是關(guān)鍵幀
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 數(shù)據(jù)
sendFrame(pp_nal[i].i_type, pp_nal[i].p_payload, pp_nal[i].i_payload, 0);
}
}
}
}
/**
* 發(fā)送 sps pps
* @param sps 編碼第一幀數(shù)據(jù)
* @param pps 編碼第二幀數(shù)據(jù)
* @param sps_len 編碼第一幀數(shù)據(jù)的長(zhǎng)度
* @param pps_len 編碼第二幀數(shù)據(jù)的長(zhǎng)度
*/
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;
//編碼規(guī)格
packet->m_body[i++] = sps[1];
packet->m_body[i++] = sps[2];
packet->m_body[i++] = sps[3];
packet->m_body[i++] = 0xFF;
//整個(gè)sps
packet->m_body[i++] = 0xE1;
//sps長(zhǎng)度
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;
//隨意分配一個(gè)管道(盡量避開(kāi)rtmp.c中使用的)
packet->m_nChannel = 0x10;
//sps pps沒(méi)有時(shí)間戳
packet->m_nTimeStamp = 0;
//不使用絕對(duì)時(shí)間
packet->m_hasAbsTimestamp = 0;
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
if (mVideoCallback && isStart)
mVideoCallback(packet);
}
/**
* 發(fā)送視頻幀 -- 關(guān)鍵幀
* @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("關(guān)鍵幀");
}
//類型
packet->m_body[1] = 0x01;
//時(shí)間戳
packet->m_body[2] = 0x00;
packet->m_body[3] = 0x00;
packet->m_body[4] = 0x00;
//數(shù)據(jù)長(zhǎng)度 int 4個(gè)字節(jié)
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;
//圖片數(shù)據(jù)
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);//回調(diào)給 RTMP 模塊
}
5. 釋放編碼器
當(dāng)我們不需要編碼的時(shí)候需要釋放編碼器,代碼如下:
x264_encoder_close(mVideoCodec);
硬編
在 Android 4.3 系統(tǒng)以后,用 MediaCodec 編碼視頻成為了主流的使用場(chǎng)景,盡管 Android 的碎片化很嚴(yán)重,會(huì)導(dǎo)致一些兼容性問(wèn)題,但是硬件編碼器的性能以及速度是非常可觀的,并且在 4.3 系統(tǒng)之后可以通過(guò) Surface 來(lái)配置編碼器的輸入,大大降低了顯存到內(nèi)存的交換過(guò)程所使用的時(shí)間,從而使得整個(gè)應(yīng)用的體驗(yàn)得到大大提升。由于輸入和輸出已經(jīng)確定,因此接下來(lái)將直接編寫(xiě) MediaCodec 編碼視頻幀的過(guò)程。
1. 創(chuàng)建 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;
//設(shè)置攝像頭預(yù)覽幀率
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. 開(kāi)啟視頻編碼器
mMediaCodec.start();
4. 拿到編碼之后的數(shù)據(jù)
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 數(shù)據(jù)回調(diào)出去
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 數(shù)據(jù)
@Override
public void onVideoData(ByteBuffer bb, MediaCodec.BufferInfo bi) {
mAnnexbHelper.analyseVideoData(bb, bi);
}
/**
* 將硬編得到的視頻數(shù)據(jù)進(jìn)行處理生成每一幀視頻數(shù)據(jù),然后傳給flv打包器
* @param bb 硬編后的數(shù)據(jù)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);
}
}
這個(gè)方法主要是從編碼后的數(shù)據(jù)中解析得到NALU,然后判斷NALU的類型,最后再把數(shù)據(jù)回調(diào)給 FlvPacker 去處理。
處理 spsPps:
@Override
public void onSpsPps(byte[] sps, byte[] pps) {
if (packetListener == null) {
return;
}
//寫(xiě)入第一個(gè)視頻信息
writeFirstVideoTag(sps, pps);
//寫(xiě)入第一個(gè)音頻信息
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;
}
//確保第一幀是關(guān)鍵幀,避免一開(kāi)始出現(xiàn)灰色模糊界面
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 推流
注: 實(shí)際項(xiàng)目 rtmp 需要先連接上才有后續(xù)操作。
rtmp 模塊我們已在開(kāi)發(fā) 播放器 的時(shí)候,將它和 ffmpeg 一并編譯了。所以我們直接使用上次的靜態(tài)庫(kù)和頭文件就可以了,如果對(duì) rtmp 協(xié)議不了解的可以參考上一篇文章,里面也有介紹 搭建 RTMP 直播服務(wù)器。
到這里軟編碼和硬編碼數(shù)據(jù)都已準(zhǔn)備好了現(xiàn)在,需要發(fā)送給 rtmp 模塊,也就是在 native 中,先看 java 發(fā)送出口:
/**
* 打包之后的數(shù)據(jù),和裸流數(shù)據(jù)
*
* @param data
* @param type
*/
@Override
public void onData(byte[] data, int type) {
if (type == RtmpPacker.FIRST_AUDIO || type == RtmpPacker.AUDIO) {//音頻 AAC 數(shù)據(jù),已打包
mPusherManager.pushAACData(data, data.length, type);
} else if (type == RtmpPacker.FIRST_VIDEO ||
type == RtmpPacker.INTER_FRAME || type == RtmpPacker.KEY_FRAME) {//H264 視頻數(shù)據(jù),已打包
mPusherManager.pushH264(data, type, 0);
} else if (type == RtmpPacker.PCM) { //PCM 裸流數(shù)據(jù)
mPusherManager.pushPCM(data);
} else if (type == RtmpPacker.YUV) { //YUV 裸流數(shù)據(jù)
mPusherManager.pushYUV(data);
}
}
/**
* 發(fā)送 H264 數(shù)據(jù)
*
* @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);
/**
* 發(fā)送 PCM 原始數(shù)據(jù)
*
* @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 協(xié)議,所以你可以使用 Java Socket 進(jìn)行連接,也可以使用 c++ librtmp 庫(kù)來(lái)進(jìn)行連接,咱們這里就使用 librtmp 來(lái)進(jìn)行連接。
/**
* 真正 rtmp 連接的函數(shù)
*/
void RTMPModel::onConnect() {
...
//1\. 初始化
RTMP_Init(rtmp);
//2\. 設(shè)置rtmp地址
int ret = RTMP_SetupURL(rtmp, this->url)
//3\. 確認(rèn)寫(xiě)入 rtmp
RTMP_EnableWrite(rtmp);
//4\. 開(kāi)始鏈接
ret = RTMP_Connect(rtmp, 0);
//5\. 連接成功之后需要連接一個(gè)流
ret = RTMP_ConnectStream(rtmp, 0);
...
}
2. Native 音頻模塊接收 AAC Flv 打包數(shù)據(jù)
/**
* 直接推送 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); //發(fā)送給 rtmp 模塊
}
3. Native 視頻模塊接收 H264 Flv 打包數(shù)據(jù)
/**
*
* @param type 視頻幀類型
* @param buf H264
* @param len H264 長(zhǎng)度
*/
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("視頻關(guān)鍵幀");
}
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);//發(fā)送給 rtmp 模塊
}
4. RTMP 發(fā)送數(shù)據(jù)
4.1 將接收到的數(shù)據(jù)入發(fā)送隊(duì)列
//不管是軟編碼還是硬編碼所有發(fā)送數(shù)據(jù)都需要入隊(duì)列
void callback(RTMPPacket *packet) {
if (packet) {
if (rtmpModel) {
//設(shè)置時(shí)間戳
packet->m_nTimeStamp = RTMP_GetTime() - rtmpModel->mStartTime;
rtmpModel->mPackets.push(packet);
}
}
}
4.2 發(fā)送
/**
* 真正推流的地方
*/
void RTMPModel::onPush() {
RTMPPacket *packet = 0;
while (isStart) {
//從隊(duì)列中獲取發(fā)送的音視頻數(shù)據(jù)
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("發(fā)送失敗")
if (pushCallback) {
pushCallback->onError(THREAD_CHILD, RTMP_PUSHER_ERROR);
}
return;
}
}
releasePackets(packet);
release();//釋放
}
5. 關(guān)閉 RTMP
當(dāng)不需要發(fā)送音視頻數(shù)據(jù)的時(shí)候需要關(guān)閉 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();
}
簡(jiǎn)單談?wù)勡浻簿幗獯a
1. 區(qū)別
軟編碼: 使用 CPU 進(jìn)行編碼。 硬編碼: 使用 GPU 進(jìn)行編碼。
2. 比較
軟編碼: 實(shí)現(xiàn)直接、簡(jiǎn)單,參數(shù)調(diào)整方便,升級(jí)容易,但 CPU 負(fù)載重,性能較硬編碼低,低碼率下質(zhì)量通常比硬編碼要好一點(diǎn)。 硬編碼: 性能高,低碼率下通常質(zhì)量低于軟編碼器,但部分產(chǎn)品在 GPU 硬件平臺(tái)移植了優(yōu)秀的軟編碼算法(如X264)的,質(zhì)量基本等同于軟編碼。
3. 使用場(chǎng)景
軟編碼: 適用短時(shí)間操作,如錄制短視頻等。
硬編碼: 長(zhǎng)時(shí)間編碼或者對(duì)視頻質(zhì)量要求高(VOIP 實(shí)時(shí)通話),可以推薦硬件編碼 (前提是手機(jī)性能好)。
總結(jié)
到這里 Android 端軟編推流,硬編推流都分別實(shí)現(xiàn)了。在項(xiàng)目上可以根據(jù)實(shí)際情況來(lái)選擇到底是硬編還是軟編。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-705246.html
如有幫助到你,可以點(diǎn)擊一波關(guān)注、點(diǎn)贊嗎?感謝支持!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-705246.html
到了這里,關(guān)于Android音視頻學(xué)習(xí)系列(九) — Android端實(shí)現(xiàn)rtmp推流的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!