最纯粹的直播技术实战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的点击操作了

我们把上一次的测试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里面配置一下

如果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方法后,我们先把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写的,开源的。我们把它下载下来,然后解压就行了

运行起来后,就可以在浏览器里面输入http://localhost:5080/ 如果能打开red5的页面就说明已经运行起来了

打开demos

如果ofla这个demo存在的话,打开就可以看到下面的页面了

在这里面有两个直接协议的实现了,一个是RTMP,一个是RTMPT(是RTMP的变种,相当于RTMP用http包装后的协议)。

点击那个播放的图标就可以播放流媒体了,但是要直播我们app的流还需要配置一点东西,在red5的根目录下打开webapps/oflaDemo这个目录

用编辑器打开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的问题
  • 没有声音

上面那几个问题都是需要处理好的,那么下一篇我们就会先把前面的两个问题给处理一下



资源下载

时间: 2024-10-27 06:44:13

最纯粹的直播技术实战02-Camera的处理以及推流的相关文章

最纯粹的直播技术实战03-通过filter进行旋转及卡顿修复

最纯粹的直播技术实战03-通过filter进行旋转及卡顿修复 最新实战教程,Android自动化刷量.作弊与防作弊,案例:刷友盟统计.批量注册苹果帐号 这个系列的文章将会研究最纯粹的Android直播的实现,而且不是用现在的集成SDK来达到直播的技术实现,而是从一个比较底层的直播实现来探讨这个技术,这样子对于直播技术的实现,现成的一些直播框架等都有一个比较好的理解. 上一篇文章把Camera的处理以及推流给实现了,但还留下了几个bug,这一篇文章就把一些bug处理一下,主要处理两个bug 直播画

「视频直播技术详解」系列之四:推流和传输

关于直播的技术文章不少,成体系的不多.我们将用七篇文章,更系统化地介绍当下大热的视频直播各环节的关键技术,帮助视频直播创业者们更全面.深入地了解视频直播技术,更好地技术选型. 在上一期中,我们介绍了讲解编码和封装. 本篇是<解密视频直播技术>系列之四:推流和传输.推流是直播的第一公里,直播的推流对这个直播链路影响非常大,如果推流的网络不稳定,无论我们如何做优化,观众的体验都会很糟糕.所以也是我们排查问题的第一步,如何系统地解决这类问题需要我们对相关理论有基础的认识. 本系列文章大纲如下: (一

移动直播技术秒开优化经验

现今移动直播技术上的挑战要远远难于传统设备或电脑直播,其完整的处理环节包括但不限于:音视频采集.美颜/滤镜/特效处理.编码.封包.推流.转码.分发.解码/渲染/播放等. 直播常见的问题包括 主播在不稳定的网络环境下如何稳定推流? 偏远地区的观众如何高清流畅观看直播? 直播卡顿时如何智能切换线路? 如何精确度量直播质量指标并实时调整? 移动设备上不同的芯片平台如何高性能编码和渲染视频? 美颜等滤镜特效处理怎么做? 如何实现播放秒开? 如何保障直播持续播放流畅不卡顿? 视频.直播等基础知识什么是视频

技术实战:基于 MHA 方式实现 MySQL 的高可用(转)

转自:http://os.51cto.com/art/201307/401702_all.htm MHA故障转移可以很好的帮我们解决从库数据的一致性问题,同时最大化挽回故障发生后的数据.本文分享了基于 MHA 方式实现 Mysql 的高可用的技术实战,希望对您有所帮助. AD:51CTO网+ 首届中国APP创新评选大赛火热招募中…… 数据的重要性对于人们来说重要程度不说自明,在信息时代,数据有着比人们更大的力量,我们也知道最近的斯诺登事件,军事专家对于他掌握的数据给出的评价是,相当于美军十个重装

转: 移动直播技术秒开优化经验

移动直播技术秒开优化经验(含PPT) 2016-04-28 09:27 徐立,七牛创始合伙人兼产品副总裁,负责七牛直播云的整体研发,是国内 Go / Docker / Container 技术早期布道者,Go / Containers / Distributed Systems 技术的忠实爱好者和实践者.曾合著国内第一本 Go 语言图书<Go 语言编程>,翻译<Go 语言程序设计>. 现今移动直播技术上的挑战要远远难于传统设备或电脑直播,其完整的处理环节包括但不限于:音视频采集.美

直播技术简单介绍(非原创)

文章大纲 一.视频直播原理介绍二.视频直播代码演示(Android)三.项目源码下载四.参考文章 一.视频直播原理介绍 1. 视频直播技术流程 视频直播的流程可以分为如下几步:采集 —>处理—>编码和封装—>推流到服务器—>服务器流分发—>播放器流播放 2. 采集 采集是整个视频推流过程中的第一个环节,它从系统的采集设备中获取原始视频数据,将其输出到下一个环节.视频的采集涉及两方面数据的采集:音频采集和图像采集,它们分别对应两种完全不同的输入源和数据格式. 2.1 音频采集音

爬虫技术实战 | WooYun知识库

爬虫技术实战 | WooYun知识库 爬虫技术实战 大数据分析与机器学习领域Python兵器谱-大数据邦-微头条(wtoutiao.com) 大数据分析与机器学习领域Python兵器谱

Apache Spark技术实战之1 -- KafkaWordCount

欢迎转载,转载请注明出处,徽沪一郎. 概要 Spark应用开发实践性非常强,很多时候可能都会将时间花费在环境的搭建和运行上,如果有一个比较好的指导将会大大的缩短应用开发流程.Spark Streaming中涉及到和许多第三方程序的整合,源码中的例子如何真正跑起来,文档不是很多也不详细. 本篇主要讲述如何运行KafkaWordCount,这个需要涉及Kafka集群的搭建,还是说的越仔细越好. 搭建Kafka集群 步骤1:下载kafka 0.8.1及解压 wget https://www.apach

HTTP Live Streaming直播(iOS直播)技术分析与实现

http://www.cnblogs.com/haibindev/archive/2013/01/30/2880764.html 不经意间发现,大半年没写博客了,自觉汗颜.实则2012后半年,家中的事一样接着一样发生,实在是没有时间.快过年了,总算忙里偷闲,把最近的一些技术成果,总结成了文章,与大家分享. 前些日子,也是项目需要,花了一些时间研究了HTTP Live Streaming(HLS)技术,并实现了一个HLS编码器HLSLiveEncoder,当然,C++写的.其功能是采集摄像头与麦克