前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android平台下使用FFmpeg进行RTMP推流(摄像头推流)

Android平台下使用FFmpeg进行RTMP推流(摄像头推流)

作者头像
用户2929716
发布2018-08-23 13:33:39
5.6K0
发布2018-08-23 13:33:39
举报
文章被收录于专栏:流媒体流媒体

简介

前面讲到了在Android平台下使用FFmpeg进行RTMP推流(视频文件推流),里面主要是介绍如何解析视频文件并进行推流,今天要给大家介绍如何在Android平台下获取采集的图像,并进行编码推流。同时项目工程也是在之前的代码基础上新增功能。源码仓库地址FFmpegSample,这一节对应的代码版本是v1.2。大家注意不要下载错了版本。主要涉及的代码。

QQ截图20171124114855.png

建议:这套代码和讲解中,有些地方我也还没研究透彻,但这个不影响我们要实现的功能,我之前也特别纠结一些细节,花了很多的时间。其实学习一门技术和框架是一个慢慢深入的过程,刚开始我们先跑起来,再深入,否则如果你还没入门,就开始纠结一些细节参数,然后又发现网上很难找到答案,那你的自信心就会受到打击,这也是我自己的体验,和大家分享一下。等到我们越来越熟悉FFmpeg和一些技术,那么之前的问题都会迎刃而解

这套代码我在4.4.2上运行时没问题的。所以如果有同学在5.0以上,如果涉及动态权限问题,大家加上即可。学习本章之前最好先看之前的文章,这里是一套连贯的教程

打开摄像头并设置参数

具体代码查看CameraActivity.java

代码语言:javascript
复制
    private Camera getCamera() {
        Camera camera;
        try {
            //打开相机,默认为后置,可以根据摄像头ID来指定打开前置还是后置
            camera = Camera.open(1);
            if (camera != null && !isPreview) {
                try {
                    Camera.Parameters parameters = camera.getParameters();
                    //对拍照参数进行设置
                    for (Camera.Size size : parameters.getSupportedPictureSizes()) {
                        LogUtils.d(size.width + "  " + size.height);
                    }
                    LogUtils.d("============");
                    for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
                        LogUtils.d(size.width + "  " + size.height);
                    }
                    parameters.setPreviewSize(screenWidth, screenHeight); // 设置预览照片的大小
                    parameters.setPreviewFpsRange(30000, 30000);
                    parameters.setPictureFormat(ImageFormat.NV21); // 设置图片格式
                    parameters.setPictureSize(screenWidth, screenHeight); // 设置照片的大小
                    camera.setParameters(parameters);
                    //指定使用哪个SurfaceView来显示预览图片
                    camera.setPreviewDisplay(sv.getHolder()); // 通过SurfaceView显示取景画面
                    camera.setPreviewCallback(new StreamIt()); // 设置回调的类
                    camera.startPreview(); // 开始预览
                    //Camera.takePicture()方法进行拍照
                    camera.autoFocus(null); // 自动对焦
                } catch (Exception e) {
                    e.printStackTrace();
                }
                isPreview = true;
            }
        } catch (Exception e) {
            camera = null;
            e.printStackTrace();
            Toast.makeText(this, "无法获取前置摄像头", Toast.LENGTH_LONG);
        }
        return camera;
    }

Camera.open(int cameraId)

这里是创建一个Camera对象对应具体的硬件摄像头,如果摄像头已经被其他app打开,就会抛出RuntimeException异常。

cameraId是camera的Id。我们可以通过getNumberOfCameras()

获取摄像头的数量,那id的范围就是0~(getNumberOfCameras()-1)。一般情况下传0就直接获取到后置摄像头,1就获取到前置摄像头。当然有些设备可能有些不同。

Camera.Parameters

这个类用于存储和设置摄像头的参数信息,当然Camera有很多默认参数,所以我们只需要通过camera.getParameters()获取该对象,然后并设置我们需要修改的属性即可。我们看一些常见的属性设置

  • setPreviewSize 设置预览图像的大小
  • setPictureSize 设置照片的大小
  • setPreviewFpsRange 设置Fps,帧率。但我发现并没有什么卵用。每次修改后采集的频率还是没变,擦!
  • setPictureFormat 设置采集到图像的像素格式,Android推荐NV21。那我们就用这个,这个参数很重要,后面编码我们会详细讲解。

最后不要忘了调用setParameters进行设置。否则你就白忙活了。

预览和获取采集图像数据

预览

第一个问题,用什么来承载预览图像。Android提供了SurfaceView和GLSurfaceView。这里为了方便大家上手,我们先选择使用SurfaceView稍微简单一点,对SurfaceView大家不熟的可以查找相关资料。接下来就是使用SurfaceView

  • 布局中添加SurfaceView。这里我做了一个继承类MySurfaceView <com.wangheart.rtmpfile.MySurfaceView android:id="@+id/sv" android:layout_width="match_parent" android:layout_height="match_parent" />
  • 获取SurfaceHolder并设置回调 SurfaceView里有一个SurfaceHolder用来控制SurfaceView的相关操作。比如设置SurfaceView的Callback,用来监听SurfaceView的创建,变化和销毁。这里只需要实现SurfaceHolder.Callback的接口 @Override public void surfaceCreated(SurfaceHolder holder) { setStartPreview(mCamera, mHolder); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { setStartPreview(mCamera, mHolder); } @Override public void surfaceDestroyed(SurfaceHolder holder) { releaseCamera(); } 然后设置到SurfaceHolder中mHolder.addCallback(this)
  • SurfaceView与Camera关联 因为我们要讲图像预览到SurfaceView上,那么必定有地方存在关联。这里很简单,就是调用Camera的setPreviewDisplay,将SurfaceView的SurfaceHolder设置进去即可。
  • 开始预览 直接调用camera的startPreview开始进行预览。那么什么时候调用这个方法呢?
代码语言:txt
复制
1. 设置一个按钮,点击之后我们就调用这个方法进行预览
2. SurfaceView的创建回调方法中`surfaceCreated`中进行调用,因为图像要预览到SurfaceView中,所以必须得SurfaceView已成功创建。

获取采集数据

前面我们已经知道怎么预览图像了。接下来就是获取采集数据。这个也很容易就是调用Camera的setPreviewCallback设置预览回调。我们实现一下这个接口

代码语言:javascript
复制
    public class StreamIt implements Camera.PreviewCallback {
        @Override
        public void onPreviewFrame(final byte[] data, Camera camera) {
            long endTime = System.currentTimeMillis();
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    encodeTime = System.currentTimeMillis();
                    FFmpegHandle.getInstance().onFrameCallback(data);
                    LogUtils.w("编码第:" + (encodeCount++) + "帧,耗时:" + (System.currentTimeMillis() - encodeTime));
                }
            });
            LogUtils.d("采集第:" + (++count) + "帧,距上一帧间隔时间:"
                    + (endTime - previewTime) + "  " + Thread.currentThread().getName());
            previewTime = endTime;
        }
    }

很简单,这个接口就是讲原始数据进行回调。这里大家也看到了,我把采集的时间间隔和编码消耗的时间打印出来了。

编码

前面把基础的如何采集摄像头数据讲了一下,接下来就是进行视频数据编码。

开启线程编码

因为编码毕竟会比较耗时,所以我们需要放到线程中处理,这里我用了一个单线程池,避免每次开启和销毁线程产生的开销。为了保证图片按顺序编码,这里使用单线程池。

ExecutorService executor = Executors.newSingleThreadExecutor();

获取到采集的数据后就可以丢进去进行编码

代码语言:javascript
复制
executor.execute(new Runnable() {
    @Override
    public void run() {
        encodeTime = System.currentTimeMillis();
        FFmpegHandle.getInstance().onFrameCallback(data);
        LogUtils.w("编码第:" + (encodeCount++) + "帧,耗时:" + (System.currentTimeMillis() - encodeTime));
    }
});

这里大家也看出来了调用FFmpegHandle.getInstance().onFrameCallback(data);进行编码。

初始化编码相关操作

这里我们使用的是FFmpeg,所以在编码前我们会先做一些初始化以及参数设置工作,所以我们在FFmpegHandle中增加一个native方法public native int initVideo(String url);

对应到C++层,也就是ffmpeg_handle.cpp

代码语言:javascript
复制
AVFormatContext *ofmt_ctx;
AVStream *video_st;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
AVPacket enc_pkt;
AVFrame *pFrameYUV;
int count = 0;
int yuv_width;
int yuv_height;
int y_length;
int uv_length;
int width = 480;
int height = 320;
int fps = 15;
/**
 * 初始化
 */
extern "C"
JNIEXPORT jint JNICALL
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_initVideo(JNIEnv *env, jobject instance,
                                                          jstring url_) {
    const char *out_path = env->GetStringUTFChars(url_, 0);
    logd(out_path);

    //计算yuv数据的长度
    yuv_width = width;
    yuv_height = height;
    y_length = width * height;
    uv_length = width * height / 4;

    av_register_all();

    //output initialize
    avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_path);
    //output encoder initialize
    pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!pCodec) {
        loge("Can not find encoder!\n");
        return -1;
    }
    pCodecCtx = avcodec_alloc_context3(pCodec);
    //编码器的ID号,这里为264编码器,可以根据video_st里的codecID 参数赋值
    pCodecCtx->codec_id = pCodec->id;
    //像素的格式,也就是说采用什么样的色彩空间来表明一个像素点
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    //编码器编码的数据类型
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    //编码目标的视频帧大小,以像素为单位
    pCodecCtx->width = width;
    pCodecCtx->height = height;
    pCodecCtx->framerate = (AVRational) {fps, 1};
    //帧率的基本单位,我们用分数来表示,
    pCodecCtx->time_base = (AVRational) {1, fps};
    //目标的码率,即采样的码率;显然,采样码率越大,视频大小越大
    pCodecCtx->bit_rate = 400000;
    //固定允许的码率误差,数值越大,视频越小
//    pCodecCtx->bit_rate_tolerance = 4000000;
    pCodecCtx->gop_size = 50;
    /* Some formats want stream headers to be separate. */
    if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
        pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;

    //H264 codec param
//    pCodecCtx->me_range = 16;
    //pCodecCtx->max_qdiff = 4;
    pCodecCtx->qcompress = 0.6;
    //最大和最小量化系数
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    //Optional Param
    //两个非B帧之间允许出现多少个B帧数
    //设置0表示不使用B帧
    //b 帧越多,图片越小
    pCodecCtx->max_b_frames = 0;
    // Set H264 preset and tune
    AVDictionary *param = 0;
    //H.264
    if (pCodecCtx->codec_id == AV_CODEC_ID_H264) {
//        av_dict_set(&param, "preset", "slow", 0);
        /**
         * 这个非常重要,如果不设置延时非常的大
         * ultrafast,superfast, veryfast, faster, fast, medium
         * slow, slower, veryslow, placebo. 这是x264编码速度的选项
       */
        av_dict_set(&param, "preset", "superfast", 0);
        av_dict_set(&param, "tune", "zerolatency", 0);
    }

    if (avcodec_open2(pCodecCtx, pCodec, &param) < 0) {
        loge("Failed to open encoder!\n");
        return -1;
    }

    //Add a new stream to output,should be called by the user before avformat_write_header() for muxing
    video_st = avformat_new_stream(ofmt_ctx, pCodec);
    if (video_st == NULL) {
        return -1;
    }
    video_st->time_base.num = 1;
    video_st->time_base.den = fps;
//    video_st->codec = pCodecCtx;
    video_st->codecpar->codec_tag = 0;
    avcodec_parameters_from_context(video_st->codecpar, pCodecCtx);

    //Open output URL,set before avformat_write_header() for muxing
    if (avio_open(&ofmt_ctx->pb, out_path, AVIO_FLAG_READ_WRITE) < 0) {
        loge("Failed to open output file!\n");
        return -1;
    }

    //Write File Header
    avformat_write_header(ofmt_ctx, NULL);

    return 0;
}

首先需要声明一些全局的变量,方便后面编码使用AVFormatContext、AVStream等。

  • 进行FFmpeg初始化 这个和之前讲到的一样av_register_all()
  • 创建输出格式上下文 avformat_alloc_output_context2这些之前都讲到过,就不错累述
  • 获取编码器 pCodec = avcodec_find_encoder(AV_CODEC_ID_H264)获取编码器。这里我们使用H264进行视频编码。如果编码器获取失败就没有下文了
  • 创建编码器上下文 pCodecCtx = avcodec_alloc_context3(pCodec)
  • 设置编码器参数 这些参数我个参数的设置上都有加注释,大家查看即可。同时参考音视频编码相关名词详解。这讲几个主要的参数
代码语言:txt
复制
- pix\_fmt 像素的格式这里我们使用的AV\_PIX\_FMT\_YUV420P,也就是YUV平面格式,三个平面分别存放Y、U、V数据。
- codec\_type 编码器编码的数据类型
- framerate 帧率
- time\_base 帧率的基本单位
- gop\_size  GOP的大小AVDictionary设置
代码语言:txt
复制
-  前面讲了一些常规参数的设置,这里还有一些重要参数设置
 if (pCodecCtx->codec\_id == AV\_CODEC\_ID\_H264) { //        av\_dict\_set(&param, "preset", "slow", 0);         /\*\*          \* 这个非常重要,如果不设置延时非常的大          \* ultrafast,superfast, veryfast, faster, fast, medium          \* slow, slower, veryslow, placebo. 这是x264编码速度的选项        \*/         av\_dict\_set(&param, "preset", "superfast", 0);         av\_dict\_set(&param, "tune", "zerolatency", 0);     } 
  • 使用给定的编码器和参数初始化编码上下文 avcodec_open2(pCodecCtx, pCodec, &param)
  • 创建视频流 video_st = avformat_new_stream(ofmt_ctx, pCodec)这个就和之前的推文件流一样了。创建并设置相关的参数
  • 打开输出上下文 avio_open(&ofmt_ctx->pb, out_path, AVIO_FLAG_READ_WRITE)
  • 写入输出头信息 avformat_write_header(ofmt_ctx, NULL)

开始编码

在获取到采集的时候后我们通过线程池调用执行了FFmpegHandle.getInstance().onFrameCallback(mData);接下来我们重点看到onFrameCallback方法。当然这也是一个navive方法。我们看到c++层的实现。

代码语言:javascript
复制
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_onFrameCallback(JNIEnv *env, jobject instance,
                                                                jbyteArray buffer_) {
//    startTime = av_gettime();
    jbyte *in = env->GetByteArrayElements(buffer_, NULL);

    int ret = 0;

    pFrameYUV = av_frame_alloc();
    int picture_size = av_image_get_buffer_size(pCodecCtx->pix_fmt, pCodecCtx->width,
                                                pCodecCtx->height, 1);
    uint8_t *buffers = (uint8_t *) av_malloc(picture_size);


    //将buffers的地址赋给AVFrame中的图像数据,根据像素格式判断有几个数据指针
    av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, buffers, pCodecCtx->pix_fmt,
                         pCodecCtx->width, pCodecCtx->height, 1);

    //安卓摄像头数据为NV21格式,此处将其转换为YUV420P格式
    ////N21   0~width * height是Y分量,  width*height~ width*height*3/2是VU交替存储
    //复制Y分量的数据
    memcpy(pFrameYUV->data[0], in, y_length); //Y
    pFrameYUV->pts = count;
    for (int i = 0; i < uv_length; i++) {
        //将v数据存到第三个平面
        *(pFrameYUV->data[2] + i) = *(in + y_length + i * 2);
        //将U数据存到第二个平面
        *(pFrameYUV->data[1] + i) = *(in + y_length + i * 2 + 1);
    }

    pFrameYUV->format = AV_PIX_FMT_YUV420P;
    pFrameYUV->width = yuv_width;
    pFrameYUV->height = yuv_height;

    //例如对于H.264来说。1个AVPacket的data通常对应一个NAL
    //初始化AVPacket
    av_init_packet(&enc_pkt);
//    __android_log_print(ANDROID_LOG_WARN, "eric", "编码前时间:%lld",
//                        (long long) ((av_gettime() - startTime) / 1000));
    //开始编码YUV数据
    ret = avcodec_send_frame(pCodecCtx, pFrameYUV);
    if (ret != 0) {
        logw("avcodec_send_frame error");
        return -1;
    }
    //获取编码后的数据
    ret = avcodec_receive_packet(pCodecCtx, &enc_pkt);
//    __android_log_print(ANDROID_LOG_WARN, "eric", "编码时间:%lld",
//                        (long long) ((av_gettime() - startTime) / 1000));
    //是否编码前的YUV数据
    av_frame_free(&pFrameYUV);
    if (ret != 0 || enc_pkt.size <= 0) {
        loge("avcodec_receive_packet error");
        avError(ret);
        return -2;
    }
    enc_pkt.stream_index = video_st->index;
    AVRational time_base = ofmt_ctx->streams[0]->time_base;//{ 1, 1000 };
    enc_pkt.pts = count * (video_st->time_base.den) / ((video_st->time_base.num) * fps);
    enc_pkt.dts = enc_pkt.pts;
    enc_pkt.duration = (video_st->time_base.den) / ((video_st->time_base.num) * fps);
    __android_log_print(ANDROID_LOG_WARN, "eric",
                        "index:%d,pts:%lld,dts:%lld,duration:%lld,time_base:%d,%d",
                        count,
                        (long long) enc_pkt.pts,
                        (long long) enc_pkt.dts,
                        (long long) enc_pkt.duration,
                        time_base.num, time_base.den);
    enc_pkt.pos = -1;
//    AVRational time_base_q = {1, AV_TIME_BASE};
//    //计算视频播放时间
//    int64_t pts_time = av_rescale_q(enc_pkt.dts, time_base, time_base_q);
//    //计算实际视频的播放时间
//    if (count == 0) {
//        startTime = av_gettime();
//    }
//    int64_t now_time = av_gettime() - startTime;
//    __android_log_print(ANDROID_LOG_WARN, "eric", "delt time :%lld", (pts_time - now_time));
//    if (pts_time > now_time) {
//        //睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
//        av_usleep((unsigned int) (pts_time - now_time));
//    }

    ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
    if (ret != 0) {
        loge("av_interleaved_write_frame failed");
    }
    count++;
    env->ReleaseByteArrayElements(buffer_, in, 0);
    return 0;

}
像素格式转换

在设置摄像头采集的图像格式时候我们设置的是NV21。而我们编码需要的是AV_PIX_FMT_YUV420P。所以这需要进行转换。我们先看下两个像素格式的区别

  • NV21 是一个YUV 4:2:0数据,应该说是平面和打包混合存储。有两个平面,第一个平面存放Y数据第二个平面VU数据交替存储
  • AV_PIX_FMT_YUV420P 那不用说是我们熟悉的YUV 4:2:0的像素数据,它是纯平面存储。总共三个平面,分别存放,Y、U、V数据。

我们还需要了解,以为采集的数据YUV是4:2:0。所以Y:(U或V)的大小是4:1。而U:V是1:1。所以当图像宽是width,高是height时,Y分量的大小就是width×heitht,而U是width×heitht/4,V也是U是width×heitht/4。

知道上面的存储格式后我们就知道怎么转换了。

首先复制Y分量的数据

memcpy(pFrameYUV->data[0], in, y_length);

然后遍历VU数据并存放到data1和data2平面中

代码语言:javascript
复制
    for (int i = 0; i < uv_length; i++) {
        //将v数据存到第三个平面
        *(pFrameYUV->data[2] + i) = *(in + y_length + i * 2);
        //将U数据存到第二个平面
        *(pFrameYUV->data[1] + i) = *(in + y_length + i * 2 + 1);
    }

H264编码

首先我们需要了解两个数据结构AVFrame、AVPacket

AVFrame存放的是原始数据、AVPacket存放的是编码后的数据。所以前面格式转换也是将数据存放到pFrameYUV中。

  • 初始化AVPacket av_init_packet(&enc_pkt);
  • 开始编码 ret = avcodec_send_frame(pCodecCtx, pFrameYUV);
  • 接受编码后的数据 ret = avcodec_receive_packet(pCodecCtx, &enc_pkt);

是不是很简单,这样编码后的数据就存到了enc_pkt中。到这里只是完成的编码工作,接下来还有一些参数需要设置

PTS、DTS、duration

PTS是显示时间戳,DTS解码时间戳,duration是当当前帧和下一帧的时间间隔,。这个很重要,不然播放会出现问题。

首先我们要知道时间基数,也就是你按什么时间单位算。

AVRational time_base = ofmt_ctx->streams[0]->time_base;

这里的值是{1,1000},这应该就是毫秒。知道时间基础,同时根据fps我们就知道每一帧的时间间隔是1000/fps。

那第n帧的pts就是n×(1000/fps)。对应代码

enc_pkt.pts = count * (video_st->time_base.den) / ((video_st->time_base.num) * fps);

dts和pts设置成一样,标示解码时间和显示时间一致。至于为什么,其实我也没太明白,如果不一致该怎么计算?我们先不管,以后再研究。

duration那就容易了,就是(video_st->time_base.den) / ((video_st->time_base.num) * fps)

这里的三个参数这是可能不太准确,但我们先这样,想把功能跑起来再说,不然我们纠结这些就永无止境了。后面等我们深入了,也就会明白。

输出视频数据

ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);

释放资源

在结束编码推流后我们也需要释放相关的资源

代码语言:javascript
复制
    if (video_st)
        avcodec_close(video_st->codec);
    if (ofmt_ctx) {
        avio_close(ofmt_ctx->pb);
        avformat_free_context(ofmt_ctx);
        ofmt_ctx = NULL;
    }
    return 0;
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.11.24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 打开摄像头并设置参数
    • Camera.open(int cameraId)
      • Camera.Parameters
      • 预览和获取采集图像数据
        • 预览
          • 获取采集数据
          • 编码
            • 开启线程编码
              • 初始化编码相关操作
                • 开始编码
                  • 像素格式转换
                • H264编码
                  • PTS、DTS、duration
                • 输出视频数据
                • 释放资源
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档