国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Android音視頻學(xué)習(xí)系列(九) — Android端實(shí)現(xiàn)rtmp推流

這篇具有很好參考價(jià)值的文章主要介紹了Android音視頻學(xué)習(xí)系列(九) — Android端實(shí)現(xiàn)rtmp推流。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問(wèn)。

系列文章

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)下圖:

安卓推流拉流,Android,android,ffmpeg,rtmp,直播服務(wù)器,推流拉流,Powered by 金山文檔

該篇文章主要完成上圖黃顏色功能部分,下面就開(kāi)始進(jìn)入正題,代碼編寫(xiě)了。

項(xiàng)目效果

推流監(jiān)控

安卓推流拉流,Android,android,ffmpeg,rtmp,直播服務(wù)器,推流拉流,Powered by 金山文檔

軟編碼

安卓推流拉流,Android,android,ffmpeg,rtmp,直播服務(wù)器,推流拉流,Powered by 金山文檔

硬編碼

安卓推流拉流,Android,android,ffmpeg,rtmp,直播服務(wù)器,推流拉流,Powered by 金山文檔

文章末尾會(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)中。

安卓推流拉流,Android,android,ffmpeg,rtmp,直播服務(wù)器,推流拉流,Powered by 金山文檔

如果攝像機(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 類介紹。

安卓推流拉流,Android,android,ffmpeg,rtmp,直播服務(wù)器,推流拉流,Powered by 金山文檔

代碼具體實(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,如下所示:

安卓推流拉流,Android,android,ffmpeg,rtmp,直播服務(wù)器,推流拉流,Powered by 金山文檔

交叉編譯腳本如下:

#!/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(&param, 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(&param, "baseline");
    //打開(kāi)編碼器
    mVideoCodec = x264_encoder_open(&param);
    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)選擇到底是硬編還是軟編。

如有幫助到你,可以點(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)!

本文來(lái)自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • Android-音視頻學(xué)習(xí)系列-(八)基于-Nginx-搭建(rtmp、http)直播服務(wù)器

    Android-音視頻學(xué)習(xí)系列-(八)基于-Nginx-搭建(rtmp、http)直播服務(wù)器

    #!/bin/sh HTTP_FLV_MODULE_PATH=…/nginx-http-flv-module-1.2.7 OpenSSL_PATH=…/openssl-1.1.1d #–prefix=./bin 代表編譯完成之后輸出的路徑地址 #–add-module 將拓展模塊添加到當(dāng)前一起編譯 ./configure --prefix=./bin –add-module= H T T P F L V M O D U L E P A T H ? ? ? w i t h ? o p e n s s l = HTTP_FLV_MODULE_PATH --with

    2024年04月15日
    瀏覽(32)
  • 音視頻開(kāi)發(fā)---ffmpeg rtmp推流

    音視頻開(kāi)發(fā)---ffmpeg rtmp推流

    推流是將輸入視頻數(shù)據(jù)推送至流媒體服務(wù)器, 輸入視頻數(shù)據(jù)可以是本地視頻文件(avi,mp4,flv......),也可以是內(nèi)存視頻數(shù)據(jù),或者攝像頭等系統(tǒng)設(shè)備,也可以是網(wǎng)絡(luò)流URL。本篇介紹將本地視頻文件通過(guò)FFmpeg編程以RTMP直播流的形式推送至RTMP流媒體服務(wù)器的方法。 推流的網(wǎng)絡(luò)拓?fù)?/p>

    2024年02月16日
    瀏覽(33)
  • JavaCV音視頻開(kāi)發(fā)寶典:使用javacv讀取GB28181、??荡笕A平臺(tái)和網(wǎng)絡(luò)攝像頭sdk回調(diào)視頻碼流并轉(zhuǎn)碼推流rtmp流媒體服務(wù)

    JavaCV音視頻開(kāi)發(fā)寶典:使用javacv讀取GB28181、??荡笕A平臺(tái)和網(wǎng)絡(luò)攝像頭sdk回調(diào)視頻碼流并轉(zhuǎn)碼推流rtmp流媒體服務(wù)

    《JavaCV音視頻開(kāi)發(fā)寶典》專欄目錄導(dǎo)航 《JavaCV音視頻開(kāi)發(fā)寶典》專欄介紹和目錄 本篇文章用于解決javacv接入h264/hevc裸流或者接入ps/ts流等字節(jié)流的非流媒體協(xié)議視頻源接入并推流到rtmp流媒體服務(wù)。 本篇文章適用于gb28181/海康大華網(wǎng)絡(luò)攝像機(jī)設(shè)備sdk對(duì)接以及??荡笕A等視頻平

    2023年04月09日
    瀏覽(36)
  • 音視頻開(kāi)發(fā)系列(10):基于qt的音頻推流

    音視頻開(kāi)發(fā)系列(10):基于qt的音頻推流

    今天分享一下利用qt錄制音頻,然后再利用ffmpeg推流到nginx服務(wù)器,最后再利用vlc進(jìn)行拉流的demo。 首先介紹一下如何利用qt來(lái)進(jìn)行音頻的錄制,qt的音頻錄制主要利用qt的QAudioFormat先進(jìn)行音頻信息的配置。主要需要配置以下的信息: 然后使用QAudioDeviceInfo來(lái)獲取是否支持改設(shè)置

    2024年02月02日
    瀏覽(25)
  • 音視頻開(kāi)發(fā)系列(7):完成本地?cái)z像頭直播推流

    音視頻開(kāi)發(fā)系列(7):完成本地?cái)z像頭直播推流

    今天把讀取本地?cái)z像頭將視頻流推流到nginx服務(wù)器的直播代碼學(xué)習(xí)完了,這里對(duì)代碼的流程做一下記錄,以便以后進(jìn)行復(fù)習(xí)。 這邊用到了opencv和ffmpeg的開(kāi)源庫(kù)(PS:在前面有進(jìn)行分享),配置環(huán)境在之前也有進(jìn)行分享。 第一步:先用到了opencv的VideoCapture類的open函數(shù)打開(kāi)攝像頭,

    2024年02月02日
    瀏覽(26)
  • Android平臺(tái)音視頻推送選RTMP還是GB28181?

    Android平臺(tái)音視頻推送選RTMP還是GB28181?

    早在2015年,我們發(fā)布了RTMP直播推送模塊,那時(shí)候音視頻直播這塊場(chǎng)景需求,還不像現(xiàn)在這么普遍,我們做這塊的初衷,主要是為了實(shí)現(xiàn)移動(dòng)單兵應(yīng)急指揮系統(tǒng)的低延遲音視頻數(shù)據(jù)傳輸。好多開(kāi)發(fā)者可能會(huì)疑惑,走RTMP怎么可能低延遲?網(wǎng)上看到的RTMP推拉流延遲,總歸要2-3秒起

    2024年02月10日
    瀏覽(21)
  • 音視頻學(xué)習(xí)(二十一)——rtmp收流(tcp方式)

    音視頻學(xué)習(xí)(二十一)——rtmp收流(tcp方式)

    本文主要介紹rtmp協(xié)議收流流程,在linux上搭建rtmp服務(wù)器,通過(guò)自研的rtmp收流庫(kù)發(fā)起取流請(qǐng)求,使用ffmpeg+qt實(shí)現(xiàn)視頻流的解碼與播放。 關(guān)于rtmp協(xié)議基礎(chǔ)介紹可查看:https://blog.csdn.net/www_dong/article/details/131026072 下載nginx 解壓,將nginx-rtmp-module拷貝至nginx-1.24.0目錄,如下所示:

    2024年02月03日
    瀏覽(43)
  • Android-音視頻學(xué)習(xí)系列-(二)-交叉編譯動(dòng)態(tài)庫(kù)、靜態(tài)庫(kù)的入門(mén)學(xué)習(xí)

    Android-音視頻學(xué)習(xí)系列-(二)-交叉編譯動(dòng)態(tài)庫(kù)、靜態(tài)庫(kù)的入門(mén)學(xué)習(xí)

    gcc -S test.i -o test.s//-S 的作用是編譯結(jié)束生成匯編文件。 匯編階段 匯編階段把 .S 文件翻譯成二進(jìn)制機(jī)器指令文件 .o ,這個(gè)階段接收.c ,.i ,.s 的文件都沒(méi)有問(wèn)題。 下面我們通過(guò)以下命令生成二進(jìn)制機(jī)器指令文件 .o 文件: gcc -c test.s -o test.o 鏈接階段 鏈接階段,鏈接的是函數(shù)庫(kù)。

    2024年04月09日
    瀏覽(26)
  • 音視頻開(kāi)發(fā) RTMP協(xié)議發(fā)送H.264編碼及AAC編碼的音視頻(C++實(shí)現(xiàn))

    音視頻開(kāi)發(fā) RTMP協(xié)議發(fā)送H.264編碼及AAC編碼的音視頻(C++實(shí)現(xiàn))

    RTMP(Real Time Messaging Protocol)是專門(mén)用來(lái)傳輸音視頻數(shù)據(jù)的流媒體協(xié)議,最初由Macromedia 公司創(chuàng)建,后來(lái)歸Adobe公司所有,是一種私有協(xié)議,主要用來(lái)聯(lián)系Flash Player和RtmpServer,如 FMS , Red5 , crtmpserver 等。RTMP協(xié)議可用于實(shí)現(xiàn)直播、點(diǎn)播應(yīng)用,通過(guò) FMLE(Flash Media Live Encoder) 推送音

    2023年04月08日
    瀏覽(28)
  • Android平臺(tái)一對(duì)一音視頻通話方案對(duì)比:WebRTC VS RTMP VS RTSP

    Android平臺(tái)一對(duì)一音視頻通話方案對(duì)比:WebRTC VS RTMP VS RTSP

    一對(duì)一音視頻通話使用場(chǎng)景 一對(duì)一音視頻通話都需要穩(wěn)定、清晰和流暢,以確保良好的用戶體驗(yàn),常用的使用場(chǎng)景如下: 社交應(yīng)用 :社交應(yīng)用是一種常見(jiàn)的使用場(chǎng)景,用戶可以通過(guò)音視頻通話進(jìn)行面對(duì)面的交流; 在線教育: 老師和學(xué)生可以通過(guò)音視頻通話功能進(jìn)行實(shí)時(shí)互

    2024年02月13日
    瀏覽(24)

覺(jué)得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包