音視頻學習 (十一) Android 端實現 rtmp 推流

前言

我們回顧前面 2 篇文章,主要講解了如何搭建 rtmp 直播服務器,和如何開發一款具備拉流功能的 Android 播放器。那麼如今有了播放端和直播服務器還缺乏推流端。該篇文章咱們就一塊兒來實現 Android 端的 rtmp 推流,想要實現 Android 端推流必需要通過以下幾個階段,見下圖:html

該篇文章主要完成上圖黃顏色功能部分,下面就開始進入正題,代碼編寫了。java

項目效果

推流監控

軟編碼

硬編碼

文章末尾會介紹軟硬編解碼。linux

音頻採集

Android SDK 提供了兩套音頻採集的 API ,分別是 MediaRecorder 、AudioRecord 。前者是一個上層 API ,它能夠直接對手機麥克風錄入的音頻數據進行編碼壓縮(如 AMR/MP3) 等,並存儲爲文件;後者則更接近底層,可以更加自由靈活地控制,其可讓開發者獲得內存中的 PCM 原始音頻數據流。若是想作一個簡單的錄音機,輸出音頻文件則推薦使用 MediaRecorder ; 若是須要對音頻作進一步的算法處理,或者須要採用第三方的編碼庫進行編碼,又或者須要用到網絡傳輸等場景中,那麼只能使用 AudioRecord 或者 OpenSL ES ,其實 MediaRecorder 底層也是調用了 AudioRecord 與 Android Framework 層的 AudioFlinger 進行交互的。而咱們該篇的場景更傾向於第二種實現方式,即便用 AudioRecord 來採集音頻。android

若是想要使用 AudioRecord 這個 API ,則須要在應用 AndroidManifest.xml 的配置文件中進行以下配置:c++

<uses-permission android:name="android.permission.RECORD_AUDIO"></uses-permission>
複製代碼

固然,若是你想把採集到的 PCM 原始數據,存儲 sdcard 中,還須要額外添加寫入權限:git

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
複製代碼

接下來了解一下 AudioRecord 的工做流程。github

1. 初始化 AudioRecord

首先來看一下 AudioRecord 的配置參數,AudioRecord 是經過構造函數來配置參數的,其函數原型以下:算法

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes) 複製代碼

上述參數所表明的函數及其在各類場景下應該傳遞的值的含義參考以下說明:shell

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. 攝像頭開啓預覽

設置預覽界面有兩種方式:一、經過 SurfaceView 顯示;二、經過 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();
    }
}
複製代碼

對於自動對焦這樣設置後就完成了工做,可是對於觸摸對焦則須要設置對應的對焦區域。要準確地設置對焦區域,有三個步驟:1、獲得當前點擊的座標位置;2、經過點擊的座標位置轉換到攝像頭預覽界面座標系統上的座標;3、根據座標生成對焦區域而且設置給攝像頭。整個攝像頭預覽界面定義了以下的座標系統,對焦區域也須要對應到這個座標系統中。

若是攝像機預覽界面是經過 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 經過 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;
    //打開編碼器
    //三、一次最大能輸入編碼器的樣本數量 也編碼的數據的個數 (一個樣本是16位 2字節)
    //四、最大可能的輸出數據 編碼後的最大字節數
    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 類介紹

代碼具體實現以下:

//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 經過 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(&param, 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(&param, "baseline");
    //打開編碼器
    mVideoCodec = x264_encoder_open(&param);
    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 模塊咱們已在開發 播放器 的時候,將它和 ffmpeg 一併編譯了。因此咱們直接使用上次的靜態庫和頭文件就能夠了,若是對 rtmp 協議不瞭解的能夠參考上一篇文章,裏面也有介紹 搭建 RTMP 直播服務器

到這裏軟編碼和硬編碼數據都已準備好了如今,須要發送給 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 推流項目地址

Android 拉流項目地址

參考

相關文章
相關標籤/搜索