最純粹的直播技術實戰02-Camera的處理以及推流

最純粹的直播技術實戰02-Camera的處理以及推流



最新實戰教程,Android自動化刷量、作弊與防作弊,案例:刷友盟統計、批量註冊蘋果帳號



這個系列的文章將會研究最純粹的Android直播的實現,而且不是用現在的集成SDK來達到直播的技術實現,而是從一個比較底層的直播實現來探討這個技術,這樣子對於直播技術的實現,現成的一些直播框架等都有一個比較好的理解。

上一篇文章裏面,我們完成了FFmpeg的編譯,然後也把編譯出來的庫運行在了Android上,那接下來就要處理Android的Camera以及推流的實現了。如果沒有看過上一篇文章的可以戳這裏

我們會使用上一篇文章那工程項目來繼續後續的功能編寫,產生,我們在MainActivity裏面添加兩個Button,一個是直播的,一個是看直播的

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="com.xiaoxiao.live.MainActivity">

    <TextView
        android:id="@+id/main_tv_info"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:text="Hello World!" />

    <Button
        android:id="@+id/main_bt_live"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="我要直播" />

    <Button
        android:id="@+id/main_bt_watch"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="看直播" />

</LinearLayout>

layout完成之後呢,就去MainActivity裏面處理一下Button的點擊操作了

click

我們把上一次的測試TextView註釋掉了,然後新建了一個LiveActivity來處理Camera以及推流,主要就是展示直播。
後續還會有看直播的處理,就要就是拉流以及視頻的播放了
接下來就要在LiveActivity裏面處理一下Camera的東西了,首先要在LiveActivity的layout裏面添加一個SurfaceView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/live_sv_live"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

因爲不是拍照,所以Camera的處理就會顯得比較的簡單了,

package com.xiaoxiao.live;

import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * Created by Administrator on 2017/2/20.
 */

public class LiveActivity extends AppCompatActivity implements SurfaceHolder.Callback, Camera.PreviewCallback {

    private Camera mCamera;
    private SurfaceView mSurfaceView;
    private SurfaceHolder mSurfaceHolder;
    private int mCameraId = 0;
    private int width = 720;
    private int height = 480;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_live);

        mSurfaceView = (SurfaceView) findViewById(R.id.live_sv_live);
        mSurfaceHolder = mSurfaceView.getHolder();
        mSurfaceHolder.setFixedSize(width, height);
        mSurfaceHolder.addCallback(this);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_live, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if(item.getItemId() == R.id.checkable_menu) {
            boolean isChecked = item.isChecked();
            Log.e("LiveActivity", "checked: " + isChecked);
            item.setChecked(!isChecked);

            mCameraId = 1 - mCameraId;
            destroyCamera();
            initCamera();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {

    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        initCamera();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        destroyCamera();
    }

    private void initCamera() {
        try {
            mCamera = Camera.open(mCameraId);
            mCamera.setPreviewDisplay(mSurfaceHolder);
            Camera.Parameters params = mCamera.getParameters();
            //設置預覽大小
            params.setPreviewSize(width, height);
            //設置生成的照片大小
            params.setPictureSize(width, height);
            params.setPreviewFormat(ImageFormat.NV21);
            mCamera.setDisplayOrientation(90);
            //params.setRotation(90);

            /*List<Camera.Size> sizes = params.getSupportedPreviewSizes();
            for(Camera.Size s : sizes) {
                Log.e("LiveActivity", s.width + " X " + s.height);
            }*/

            mCamera.setParameters(params);
            mCamera.setPreviewCallback(this);
            mCamera.startPreview();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void destroyCamera() {
        if(mCamera == null) {
            return;
        }

        mCamera.setPreviewCallback(null);
        mCamera.stopPreview();
        mCamera.release();
        mCamera = null;
    }
}

我們通過menu來做了一個攝像頭的切換功能,這樣子就可以前攝像頭直播或者後攝像頭直播了。
到時會在onPreviewFrame裏面獲取到數據,然後交給jni進行一個編碼的處理,然後就推流

那麼這裏就會有一個非常重要的知識點了:
我們通過setPreviewFormat方法把預覽的數據(onPreviewFrame方法參數裏面的data)的格式設置成了ImageFormat.NV21,一般來說,常用的格式是NV21或者YV12,因爲這兩種格式被所有的攝像頭支持的,Android默認是會設置NV21的。

那麼什麼是NV21或YV12呢,其實這也是一種yuv格式的數據來的

上一篇文章我們已經說過了,就是通過把yuv通過編碼,然後再封裝就可以得到一個視頻文件了,但我們還需要對這種yuv進行一定的處理,因爲yuv也是有不同的各類的。

yuv通常分成兩大格式,一種是planar:把所有像素點的Y值全部存放在數組的最前面,然後再存放所有像素點的U值,最後再存放所有像素點的V值
還有一種就是packed:它是依次存放每一個像素點的YUV值的

同時yuv還有不同的採樣方式,一般主流的有三種:

  • YUV4:4:4 每一個Y對應一組UV分量
  • YUV4:2:2 每兩個Y共用一組UV分量
  • YUV4:2:0 每四個Y共用一組UV分量

假設一張720 X 480的圖片存儲成yuv格式:

  • YUV4:4:4 Y = 720 * 480 U = V = 720 * 480 所以整個數組的大小就是720 * 480 * 3
  • YUV4:2:2 Y = 720 * 480 U = V = 720 * 480 / 2 所以整個數組的大小就是720 * 480 * 2
  • YUV4:2:0 Y = 720 * 480 U = V = 720 * 480 / 4 所以整個數組的大小就是720 * 480 * 1.5

NV21和YV12就是YUV4:2:0這種採樣格式的,而且我們到時用FFmpeg編碼採用的格式一般是AV_PIX_FMT_YUV420P,都是YUV4:2:0這種採樣格式的

但還是有一些差別的

  • AV_PIX_FMT_YUV420P 格式是planar,就是先存全部的Y再存全部的U再存全部的V,採樣格式4:2:0。存儲格式類似 yyyyyyyy uu vv 這樣
  • NV21 格式也是planar,採樣格式也是4:2:0。存儲格式類似 yyyyyyyy vu vu
  • YV12 格式也是planar,採樣格式也是4:2:0。存儲格式類似 yyyyyyyy vv uu

從上面可以看到,我們需要用的格式和預覽的格式還是有些差別的,所以我們到時要處理一下。

那麼現在我們可以先把我們的Camera的功能給測試一下先的,看看能不能預覽成功,但在運行前,還需要去AndroidManifest裏面配置一下

manifest

如果Camera模塊測試沒有問題的話,我們就可以來寫native方法了,首先在LiveActivity裏面定義好幾個native方法

    /**
     * 初始化編碼的一些東西,比如編碼器等
     * @param width  編碼視頻的寬
     * @param height 編碼視頻的高
     * @return 0 成功  小於0失敗
     */
    private native int streamerInit(int width, int height);

    /**
     * 對每一次預覽的數據進行編碼推流
     * @param data NV21格式的數據
     * @return 0成功,小於0失敗
     */
    private native int streamerHandle(byte[] data);

    /**
     * 把緩衝幀的數據清空
     * @return 0成功,小於0失敗
     */
    private native int streamerFlush();

    /**
     * 釋放資源,比如編碼器這些
     * @return 0成功,小於0失敗
     */
    private native int streamerRelease();

native

定義完成native方法後,我們先把LiveActivity裏面的邏輯給處理一下先。爲了不影響UI線程(以後可能數據處理會有點多),我就使用了HandlerThread這個類來進行異步操作,先把類初始化

        mHandlerThread = new HandlerThread("liveHandlerThread");
        mHandlerThread.start();
        mHandler = new LiveHandler(this, mHandlerThread.getLooper());

LiveHandler是我定義在LiveActivity的靜態內部類,用來進行異步操作的

    private static class LiveHandler extends Handler {
        private WeakReference<LiveActivity> mActivity;

        public LiveHandler(LiveActivity activity, Looper looper) {
            super(looper);
            mActivity = new WeakReference<LiveActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            LiveActivity activity = mActivity.get();
            if(activity == null) {
                return;
            }

            switch (msg.what) {
                case STREAMER_INIT:
                    break;

                case STREAMER_HANDLE:
                    Bundle bundle = msg.getData();
                    if(bundle != null) {
                        byte[] data = bundle.getByteArray("frame_data");
                        if(data != null && data.length > 0) {
                            activity.streamerHandle(data);
                        } else {
                            Log.e("LiveActivity", "byte data null");
                        }
                    } else {
                        Log.e("LiveActivity", "bundle null");
                    }
                    break;

                case STREAMER_FLUSH:
                    activity.streamerFlush();
                    break;

                case STREAMER_RELEASE:
                    activity.streamerRelease();
                    break;
            }
        }
    }

LiveActivity裏面的邏輯主要是一些細節的處理,完整的代碼就下面那樣:

package com.xiaoxiao.live;

import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.lang.ref.WeakReference;

/**
 * Created by Administrator on 2017/2/20.
 */

public class LiveActivity extends AppCompatActivity implements SurfaceHolder.Callback, Camera.PreviewCallback {

    private static final int STREAMER_INIT = 0;
    private static final int STREAMER_HANDLE = 1;
    private static final int STREAMER_RELEASE = 2;
    private static final int STREAMER_FLUSH = 3;


    private Camera mCamera;
    private SurfaceView mSurfaceView;
    private SurfaceHolder mSurfaceHolder;
    private int mCameraId = 0;
    private int width = 720;
    private int height = 480;

    /**
     * 判斷有沒有初始化成功,不成功不不進行後續的編碼處理
     */
    private int liveInitResult = -1;

    /**
     * 異步操作
     */
    private HandlerThread mHandlerThread;
    private LiveHandler mHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_live);

        mSurfaceView = (SurfaceView) findViewById(R.id.live_sv_live);
        mSurfaceHolder = mSurfaceView.getHolder();
        mSurfaceHolder.setFixedSize(width, height);
        mSurfaceHolder.addCallback(this);

        mHandlerThread = new HandlerThread("liveHandlerThread");
        mHandlerThread.start();
        mHandler = new LiveHandler(this, mHandlerThread.getLooper());
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_live, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if(item.getItemId() == R.id.checkable_menu) {
            boolean isChecked = item.isChecked();
            Log.e("LiveActivity", "checked: " + isChecked);
            item.setChecked(!isChecked);

            mCameraId = 1 - mCameraId;
            destroyCamera();
            initCamera();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        /**
         * 如果初始化成功,那就把數據發送到Handler,然後再調用native方法
         */
        if(liveInitResult == 0 && data != null && data.length > 0) {
            Message msg = Message.obtain();
            Bundle bundle = new Bundle();
            bundle.putByteArray("frame_data", data);
            msg.what = STREAMER_HANDLE;
            msg.setData(bundle);
            mHandler.sendMessage(msg);
        }
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        /**
         * 在surface創建的時候進行初始化,如果失敗了,也是需要釋放已經開闢了的資源
         */
        liveInitResult = streamerInit(width, height);
        if(liveInitResult == -1) {
            mHandler.sendEmptyMessage(STREAMER_RELEASE);
        } else {
            Log.e("LiveActivity", "streamer init result: " + liveInitResult);
        }
        initCamera();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        /**
         * 在surface銷燬的時候清空緩衝幀(在直播成功開啓的情況下)
         * 清空後就進行資源的釋放
         * 並且把HandlerThread退出
         */
        if(liveInitResult == 0) {
            mHandler.sendEmptyMessage(STREAMER_FLUSH);
        }
        mHandler.sendEmptyMessage(STREAMER_RELEASE);
        mHandlerThread.quitSafely();
        destroyCamera();
    }

    private void initCamera() {
        try {
            mCamera = Camera.open(mCameraId);
            mCamera.setPreviewDisplay(mSurfaceHolder);
            Camera.Parameters params = mCamera.getParameters();
            //設置預覽大小
            params.setPreviewSize(width, height);
            //設置生成的照片大小
            params.setPictureSize(width, height);
            params.setPreviewFormat(ImageFormat.NV21);
            mCamera.setDisplayOrientation(90);
            //params.setRotation(90);

            /*List<Camera.Size> sizes = params.getSupportedPreviewSizes();
            for(Camera.Size s : sizes) {
                Log.e("LiveActivity", s.width + " X " + s.height);
            }*/

            mCamera.setParameters(params);
            mCamera.setPreviewCallback(this);
            mCamera.startPreview();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void destroyCamera() {
        if(mCamera == null) {
            return;
        }

        mCamera.setPreviewCallback(null);
        mCamera.stopPreview();
        mCamera.release();
        mCamera = null;
    }

    /**
     * 初始化編碼的一些東西,比如編碼器等
     * @param width  編碼視頻的寬
     * @param height 編碼視頻的高
     * @return 0 成功  小於0失敗
     */
    private native int streamerInit(int width, int height);

    /**
     * 對每一次預覽的數據進行編碼推流
     * @param data NV21格式的數據
     * @return 0成功,小於0失敗
     */
    private native int streamerHandle(byte[] data);

    /**
     * 把緩衝幀的數據清空
     * @return 0成功,小於0失敗
     */
    private native int streamerFlush();

    /**
     * 釋放資源,比如編碼器這些
     * @return 0成功,小於0失敗
     */
    private native int streamerRelease();



    //------------------------------------------------------------------------

    private static class LiveHandler extends Handler {
        private WeakReference<LiveActivity> mActivity;

        public LiveHandler(LiveActivity activity, Looper looper) {
            super(looper);
            mActivity = new WeakReference<LiveActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            LiveActivity activity = mActivity.get();
            if(activity == null) {
                return;
            }

            switch (msg.what) {
                case STREAMER_INIT:
                    break;

                case STREAMER_HANDLE:
                    Bundle bundle = msg.getData();
                    if(bundle != null) {
                        byte[] data = bundle.getByteArray("frame_data");
                        if(data != null && data.length > 0) {
                            activity.streamerHandle(data);
                        } else {
                            Log.e("LiveActivity", "byte data null");
                        }
                    } else {
                        Log.e("LiveActivity", "bundle null");
                    }
                    break;

                case STREAMER_FLUSH:
                    activity.streamerFlush();
                    break;

                case STREAMER_RELEASE:
                    activity.streamerRelease();
                    break;
            }
        }
    }
}

那麼,寫完LiveActivity的邏輯後,就要進入重要的內容了,就是在c裏面完成編碼以及推流的操作

//
// Created by Administrator on 2017/2/19.
//

#include <jni.h>
#include <stdio.h>
#include <android/log.h>

#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/time.h"
#include "libavutil/imgutils.h"

#define LOG_TAG "FFmpeg"

#define LOGE(format, ...)  __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, format, ##__VA_ARGS__)
#define LOGI(format, ...)  __android_log_print(ANDROID_LOG_INFO,  LOG_TAG, format, ##__VA_ARGS__)


AVFormatContext *ofmt_ctx = NULL;
AVStream *out_stream = NULL;
AVPacket pkt;
AVCodecContext *pCodecCtx = NULL;
AVCodec *pCodec = NULL;
AVFrame *yuv_frame;

int frame_count;
int src_width;
int src_height;
int y_length;
int uv_length;
int64_t start_time;


/**
 * 回調函數,用來把FFmpeg的log寫到sdcard裏面
 */
void live_log(void *ptr, int level, const char* fmt, va_list vl) {
    FILE *fp = fopen("/sdcard/123/live_log.txt", "a+");
    if(fp) {
        vfprintf(fp, fmt, vl);
        fflush(fp);
        fclose(fp);
    }
}

/**
 * 編碼函數
 * avcodec_encode_video2被deprecated後,自己封裝的
 */
int encode(AVCodecContext *pCodecCtx, AVPacket* pPkt, AVFrame *pFrame, int *got_packet) {
    int ret;

    *got_packet = 0;

    ret = avcodec_send_frame(pCodecCtx, pFrame);
    if(ret <0 && ret != AVERROR_EOF) {
        return ret;
    }

    ret = avcodec_receive_packet(pCodecCtx, pPkt);
    if(ret < 0 && ret != AVERROR(EAGAIN)) {
        return ret;
    }

    if(ret >= 0) {
        *got_packet = 1;
    }

    return 0;
}


JNIEXPORT jstring JNICALL
Java_com_xiaoxiao_live_MainActivity_helloFromFFmpeg(JNIEnv *env, jobject instance) {

    // TODO
    char info[10000] = {0};
    sprintf(info, "%s\n", avcodec_configuration());

    return (*env)->NewStringUTF(env, info);
}

JNIEXPORT jint JNICALL
Java_com_xiaoxiao_live_LiveActivity_streamerRelease(JNIEnv *env, jobject instance) {

    // TODO
    if(pCodecCtx) {
        avcodec_close(pCodecCtx);
        pCodecCtx = NULL;
    }

    if(ofmt_ctx) {
        avio_close(ofmt_ctx->pb);
    }
    if(ofmt_ctx) {
        avformat_free_context(ofmt_ctx);
        ofmt_ctx = NULL;
    }

    if(yuv_frame) {
        av_frame_free(&yuv_frame);
        yuv_frame = NULL;
    }

}

JNIEXPORT jint JNICALL
Java_com_xiaoxiao_live_LiveActivity_streamerFlush(JNIEnv *env, jobject instance) {

    // TODO
    int ret;
    int got_packet;
    AVPacket packet;
    if(!(pCodec->capabilities & CODEC_CAP_DELAY)) {
        return 0;
    }

    while(1) {
        packet.data = NULL;
        packet.size = 0;
        av_init_packet(&packet);
        ret = encode(pCodecCtx, &packet, NULL, &got_packet);
        if(ret < 0) {
            break;
        }
        if(!got_packet) {
            ret = 0;
            break;
        }

        LOGI("Encode 1 frame size:%d\n", packet.size);

        AVRational time_base = ofmt_ctx->streams[0]->time_base;
        AVRational r_frame_rate1 = {60, 2};
        AVRational time_base_q = {1, AV_TIME_BASE};

        int64_t calc_duration = (double)(AV_TIME_BASE) * (1 / av_q2d(r_frame_rate1));

        packet.pts = av_rescale_q(frame_count * calc_duration, time_base_q, time_base);
        packet.dts = packet.pts;
        packet.duration = av_rescale_q(calc_duration, time_base_q, time_base);

        packet.pos = -1;
        frame_count++;
        ofmt_ctx->duration = packet.duration * frame_count;

        ret = av_interleaved_write_frame(ofmt_ctx, &packet);
        if(ret < 0) {
            break;
        }
    }

    //寫文件尾
    av_write_trailer(ofmt_ctx);
    return 0;

}

JNIEXPORT jint JNICALL
Java_com_xiaoxiao_live_LiveActivity_streamerHandle(JNIEnv *env, jobject instance,
                                                   jbyteArray data_) {
    jbyte *data = (*env)->GetByteArrayElements(env, data_, NULL);

    // TODO
    int ret, i, resultCode;
    int got_packet = 0;
    resultCode = 0;

    /**
     * 這裏就是之前說的NV21轉爲AV_PIX_FMT_YUV420P這種格式的操作了
     */
    memcpy(yuv_frame->data[0], data, y_length);
    for (i = 0; i < uv_length; i++) {
        *(yuv_frame->data[2] + i) = *(data + y_length + i * 2);
        *(yuv_frame->data[1] + i) = *(data + y_length + i * 2 + 1);
    }

    yuv_frame->format = pCodecCtx->pix_fmt;
    yuv_frame->width = src_width;
    yuv_frame->height = src_height;
    //yuv_frame->pts = frame_count;
    yuv_frame->pts = (1.0 / 30) * 90 * frame_count;


    pkt.data = NULL;
    pkt.size = 0;
    av_init_packet(&pkt);

    //進行編碼
    ret = encode(pCodecCtx, &pkt, yuv_frame, &got_packet);
    if(ret < 0) {
        resultCode = -1;
        LOGE("Encode error\n");
        goto end;
    }
    if(got_packet) {
        LOGI("Encode frame: %d\tsize:%d\n", frame_count, pkt.size);
        frame_count++;
        pkt.stream_index = out_stream->index;

        //寫PTS/DTS
        AVRational time_base1 = ofmt_ctx->streams[0]->time_base;
        AVRational r_frame_rate1 = {60, 2};
        AVRational time_base_q = {1, AV_TIME_BASE};
        int64_t calc_duration = (double)(AV_TIME_BASE) * (1 / av_q2d(r_frame_rate1));

        pkt.pts = av_rescale_q(frame_count * calc_duration, time_base_q, time_base1);
        pkt.dts = pkt.pts;
        pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base1);
        pkt.pos = -1;

        //處理延遲
        int64_t pts_time = av_rescale_q(pkt.dts, time_base1, time_base_q);
        int64_t now_time = av_gettime() - start_time;
        if(pts_time > now_time) {
            av_usleep(pts_time - now_time);
        }

        ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
        if(ret < 0) {
            LOGE("Error muxing packet");
            resultCode = -1;
            goto end;
        }
        av_packet_unref(&pkt);
    }


end:
    (*env)->ReleaseByteArrayElements(env, data_, data, 0);
    return resultCode;
}

JNIEXPORT jint JNICALL
Java_com_xiaoxiao_live_LiveActivity_streamerInit(JNIEnv *env, jobject instance, jint width,
                                                 jint height) {

    // TODO
    int ret = 0;
    const char *address = "rtmp://192.168.1.102/oflaDemo/test";

    src_width = width;
    src_height = height;
    //yuv數據格式裏面的  y的大小(佔用的空間)
    y_length = width * height;
    //u/v佔用的空間大小
    uv_length = y_length / 4;

    //設置回調函數,寫log
    av_log_set_callback(live_log);

    //激活所有的功能
    av_register_all();

    //推流就需要初始化網絡協議
    avformat_network_init();

    //初始化AVFormatContext
    avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", address);
    if(!ofmt_ctx) {
        LOGE("Could not create output context\n");
        return -1;
    }

    //尋找編碼器,這裏用的就是x264的那個編碼器了
    pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if(!pCodec) {
        LOGE("Can not find encoder!\n");
        return -1;
    }

    //初始化編碼器的context
    pCodecCtx = avcodec_alloc_context3(pCodec);
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;  //指定編碼格式
    pCodecCtx->width = width;
    pCodecCtx->height = height;
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 30;
    pCodecCtx->bit_rate = 800000;
    pCodecCtx->gop_size = 300;

    if(ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
        pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
    }

    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;

    pCodecCtx->max_b_frames = 3;

    AVDictionary *dicParams = NULL;
    av_dict_set(&dicParams, "preset", "ultrafast", 0);
    av_dict_set(&dicParams, "tune", "zerolatency", 0);

    //打開編碼器
    if(avcodec_open2(pCodecCtx, pCodec, &dicParams) < 0) {
        LOGE("Failed to open encoder!\n");
        return -1;
    }

    //新建輸出流
    out_stream = avformat_new_stream(ofmt_ctx, pCodec);
    if(!out_stream) {
        LOGE("Failed allocation output stream\n");
        return -1;
    }
    out_stream->time_base.num = 1;
    out_stream->time_base.den = 30;
    //複製一份編碼器的配置給輸出流
    avcodec_parameters_from_context(out_stream->codecpar, pCodecCtx);

    //打開輸出流
    ret = avio_open(&ofmt_ctx->pb, address, AVIO_FLAG_WRITE);
    if(ret < 0) {
        LOGE("Could not open output URL %s", address);
        return -1;
    }

    ret = avformat_write_header(ofmt_ctx, NULL);
    if(ret < 0) {
        LOGE("Error occurred when open output URL\n");
        return -1;
    }

    //初始化一個幀的數據結構,用於編碼用
    //指定AV_PIX_FMT_YUV420P這種格式的
    yuv_frame = av_frame_alloc();
    uint8_t *out_buffer = (uint8_t *) av_malloc(av_image_get_buffer_size(pCodecCtx->pix_fmt, src_width, src_height, 1));
    av_image_fill_arrays(yuv_frame->data, yuv_frame->linesize, out_buffer, pCodecCtx->pix_fmt, src_width, src_height, 1);

    start_time = av_gettime();

    return 0;

}

這裏面有一個值得注意的就是NV21的數據處理那裏

    /**
     * 這裏就是之前說的NV21轉爲AV_PIX_FMT_YUV420P這種格式的操作了
     */
    memcpy(yuv_frame->data[0], data, y_length);
    for (i = 0; i < uv_length; i++) {
        *(yuv_frame->data[2] + i) = *(data + y_length + i * 2);
        *(yuv_frame->data[1] + i) = *(data + y_length + i * 2 + 1);
    }

可以對照着前面說的yuv的數據存儲來看,這樣子就會明白爲什麼要這樣處理一下了,明白了這個,那YV12的處理也很容易了

那麼寫完這個c代碼後,我們就可以把服務器給配置一下了,這樣子就可以調試我們的直播代碼有沒有問題了

上一篇文章裏面說了,直播需要一個流媒體服務器,現在可以用nginx 然後裝個RTMP的模塊就可以了(戰鬥民族寫的),還有其他的就是FMS,red5.

我這裏使用的就是red5,java寫的,開源的。我們把它下載下來,然後解壓就行了

red5

運行起來後,就可以在瀏覽器裏面輸入http://localhost:5080/ 如果能打開red5的頁面就說明已經運行起來了

red5

打開demos

red5

如果ofla這個demo存在的話,打開就可以看到下面的頁面了

red5

在這裏面有兩個直接協議的實現了,一個是RTMP,一個是RTMPT(是RTMP的變種,相當於RTMP用http包裝後的協議)。
點擊那個播放的圖標就可以播放流媒體了,但是要直播我們app的流還需要配置一點東西,在red5的根目錄下打開webapps/oflaDemo這個目錄

red5

用編輯器打開index.html,把rtmp那個播放器的腳本修改成下面的

<center>
<b>RTMP</b>
<div id='mediaspace'>This text will be replaced</div>
<script type='text/javascript'>
  jwplayer('mediaspace').setup({
    'flashplayer': 'player.swf',
    'file': 'test',
    'streamer': 'rtmp://192.168.1.102/oflaDemo',
    'controlbar': 'bottom',
    'width': '720',
    'height': '480'
  });
</script>
<br />

<b>RTMPT</b>
<div id='mediaspace2'>This text will be replaced</div>
<script type='text/javascript'>
  jwplayer('mediaspace2').setup({
    'flashplayer': 'player.swf',
    'file': 'BladeRunner2049.flv',
    'streamer': 'rtmpt://localhost:5080/oflaDemo',
    'controlbar': 'bottom',
    'width': '720',
    'height': '480'
  });
</script>
</center>

rtmp://192.168.1.102/oflaDemo這個地址和我們在c裏面寫的那個address是不是一樣,然後我們再指定了它的file是test
完整的就是我們在c裏面寫的那個address了const char *address = "rtmp://192.168.1.102/oflaDemo/test";,所以這個配置一定要正確,不然就無法直播了

192.168.1.102是我電腦的ip,完成這個調試要求手機和電腦在同一局域網下,除非自己有外網的流媒體服務器就另說了
手機那個地址千萬不要寫localhost,都不是同一個機器

好了,配置完這個之後,我們再重新刷新一下我們的網頁。然後就可以調試我們的直播了

點擊我要直播,然後就可以點擊網頁的那個播放圖標了,這樣子就可以調試我們的直播了。由於手機電腦互調,弄不了圖片,所以就要各位自己運行看結果了

總結

那麼到這裏,我們就已經完成了camera的處理,以及推流成功了,通過red5服務器,也可以看到了我們的直播,但現在這個直播還有幾個問題要處理先的:

  • 看到的直播和手機上的有一個旋轉的差別(這個原因是因爲手機攝像頭的預覽我們設置了旋轉,以方便豎屏直播,但是這個設置是不會影響原始數據的旋轉的,而且沒法設置,所以就會產生這個bug)
  • 有延遲,這個應該是PTS/DTS的問題
  • 沒有聲音

上面那幾個問題都是需要處理好的,那麼下一篇我們就會先把前面的兩個問題給處理一下



資源下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章