Android平台FFmpeg实现rtmp推流-C++的实现

视频编码有几种方式:
1.硬编码,使用MediaCodec实现
2.软编码,使用FFmpeg或者libx264库来实现。

本文分享在Android平台视频编码-软编码的实现,也就是用FFmpeg来实现视频的编码,rtmp推流到服务器上,相机采集视频将在下一篇文章分享。

流媒体服务器使用nginx-rtmp-module来进行搭建。

本文所使用FFmpeg的版本是4.1,关于FFmpeg编译成Android平台so库如果有需要,我将在下一篇文章分享说明。

视频编码比较耗cpu,上传视频数据的会耗网络io,所以需要开启新线程去处理,这里我用HandlerThread来处理视频的编码上传。

HandlerThread videoEncodeThread = new HandlerThread("video encode thread");
videoEncodeThread.start();
Handler mHandler = new Handler(videoEncodeThread.getLooper());

初始化编码相关操作
这里我们使用的是FFmpeg,所以在编码前我们会先做一些初始化以及参数设置工作。
FFmpeg初始化
av_register_all()
创建输出格式上下文
avformat_alloc_output_context2()
获取编码器
avcodec_find_encoder(AV_CODEC_ID_H264) 获取H264的编码器
设置编码器参数

  • pix_fmt 像素的格式这里我们使用的AV_PIX_FMT_YUV420P,也就是YUV平面格式,三个平面分别存放Y、U、V数据。
  • codec_type 编码器编码的数据类型
  • framerate 帧率
  • time_base 帧率的基本单位
  • gop_size GOP的大小

使用给定的编码器和参数初始化编码上下文
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)

/**
资源初始化
rtmp_width:视频的宽度
rtmp_height:视频的高度
*/
JNIEXPORT void JNICALL init(JNIEnv *env, jobject obj, jint rtmp_width, jint rtmp_height)
    {
        // 流媒体服务器地址
        const char* out_file = "rtmp://10.1.2.23:1935/hls/test";

        // 初始化
        av_register_all();

        // 初始化网络
        avformat_network_init();
        
        // 初始化Format上下文        
        avformat_alloc_output_context2(&av_format_context, NULL, "flv", out_file);
        av_output_format = av_format_context->oformat;

        //  打开连接服务器
        if (avio_open(&av_format_context->pb, out_file, AVIO_FLAG_WRITE) < 0) {
            LOGE("Failed to open output file! ");
            return ;
        }

        // 获取视频编码器
        video_codec = avcodec_find_encoder(AV_CODEC_ID_H264);
        if(video_codec == NULL) {
            LOGE("video codec is NULL");
            return ;
        }

        video_stream = avformat_new_stream(av_format_context, video_codec);
        video_stream->time_base.num = 1;
        video_stream->time_base.den = 20;   // 帧数

        /* Some formats want stream headers to be separate. */
        if (av_format_context->oformat->flags & AVFMT_GLOBALHEADER) {
            video_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
        }

        if (video_stream == NULL) {
            LOGE("video_stream is null");
            return ;
        }

        // 视频编码器的参数设置
        video_av_codec_context = video_stream->codec;
        video_av_codec_context->codec_id = video_codec->id;
        video_av_codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
        video_av_codec_context->pix_fmt = AV_PIX_FMT_YUV420P;    // yuv420  yyyy  uu  vv  4个y对应一个uv
        video_av_codec_context->width = rtmp_width;
        video_av_codec_context->height = rtmp_height;
        video_av_codec_context->bit_rate = 20 * 1000;               // 码率 kbps
        video_av_codec_context->gop_size = 60;                        // GOP图像组的宽度
        video_av_codec_context->time_base.num = 1;
        video_av_codec_context->time_base.den = 20;

        //最大和最小量化系数
        video_av_codec_context->qmin = 26;
        video_av_codec_context->qmax = 52;
        video_av_codec_context->max_b_frames = 3;

        AVDictionary *param = 0;
        if(video_av_codec_context->codec_id == AV_CODEC_ID_H264) {
            /**
            * ultrafast,superfast, veryfast, faster, fast, medium
            * slow, slower, veryslow, placebo. 这是x264编码速度的选项
            */
            av_dict_set(&param, "preset", "slow", 0);
            av_dict_set(&param, "tune", "zerolatency", 0);
            av_dict_set(&param, "profile", "baseline", 0);
        }

        // 打开视频编码器
        int ret = avcodec_open2(video_av_codec_context, video_codec, &param);
        if (ret < 0) {
            LOGE("Failed to open encoder! %d", ret);
            return ;
        }

        // 分配YUV帧
        video_frame_yuv = av_frame_alloc();
        // 计算YUV帧所需的数据量
        int picture_size = avpicture_get_size(video_av_codec_context->pix_fmt, video_av_codec_context->width, video_av_codec_context->height);
        LOGE("picture_size:%d", picture_size);

        picture_buf = (uint8_t *)av_malloc(picture_size * sizeof(uint8_t));
        avpicture_fill((AVPicture *)video_frame_yuv, picture_buf, video_av_codec_context->pix_fmt, video_av_codec_context->width, video_av_codec_context->height);

        // 写文件头

        avformat_write_header(av_format_context, NULL);
        av_new_packet(&enc_pkt, picture_size);
    }

像素格式转换
AV_PIX_FMT_YUV420P,它是纯平面存储。总共三个平面,分别存放,Y、U、V数据。
当图像宽是width,高是height时,Y分量的大小就是width×heitht,而U是width×heitht/4,V也是U是width×heitht/4。
H264编码
首先我们需要了解两个数据结构AVFrame、AVPacket
AVFrame存放的是原始数据、AVPacket存放的是编码后的数据。
创建AVPacket
av_new_packet(&enc_pkt, picture_size);
开始编码
ret = avcodec_encode_video2(pCodecCtx, pFrameYUV);
输出一帧编码后的视频数据
ret = av_write_frame(pCodecCtx, &enc_pkt);

/**
yBuffer,uBuffer,vBuffer 由ByteBuffer.allocateDirect分配内存数据,存储yuv分量的数据,传到编码器进行编码,封装成rtmp协议的数据上传到流媒体服务器。
*/
JNIEXPORT void JNICALL sendFrameData(JNIEnv *env, jobject obj,
            jobject yBuffer, jobject uBuffer, jobject vBuffer, jint row_stride)
    {

        if(video_stream == NULL) {
            LOGE("init fail");
            return ;
        }

        void* yByte = env->GetDirectBufferAddress(yBuffer);
        void* uByte = env->GetDirectBufferAddress(uBuffer);
        void* vByte = env->GetDirectBufferAddress(vBuffer);

        int width = video_av_codec_context->width;
        int height = video_av_codec_context->height;
      
        memcpy(video_frame_yuv->data[0], yByte, width * height);
        memcpy(video_frame_yuv->data[1], uByte, width * height / 4);
        memcpy(video_frame_yuv->data[2], vByte, width * height / 4);

        video_frame_yuv->pts = frameCount * (video_stream->time_base.den) / ((video_stream->time_base.num)*25);

        int got_picture = 0;
        // 视频编码
        int ret = avcodec_encode_video2(video_av_codec_context, &enc_pkt, video_frame_yuv, &got_picture);
        if(ret < 0) {
            LOGE("Failed to encode! ret:%d", ret);
            return ;
        }

        if (got_picture == 1)
        {
            enc_pkt.stream_index = video_stream->index;

            LOGE("encode package pts:%lld, dts:%lld, duration:%d", enc_pkt.pts, enc_pkt.dts, enc_pkt.duration);

            ret = av_write_frame(av_format_context, &enc_pkt);
            LOGE("av_write_frame ret:%d", ret);
            av_free_packet(&enc_pkt);
        }
        
        // 记录视频帧数
        frameCount++;
    }

释放资源

/**
结束rtmp推流,释放资源
*/
JNIEXPORT void JNICALL end(JNIEnv *env, jobject obj) {

        LOGE("rtmp end, frameCount:%d", frameCount);

        int ret = flush_encoder(av_format_context, video_stream->index);
        if (ret < 0) {
            LOGE("Flushing encoder failed\n");
            return ;
        }

        // Write file trailer
        av_write_trailer(av_format_context);

        // Clean
        if (video_stream != NULL) {
            avcodec_close(video_stream->codec);
            av_free(video_frame_yuv);
            av_free(picture_buf);
        }
        avio_close(av_format_context->pb);
        avformat_free_context(av_format_context);
    }

小伙伴们有疑问的可以在下方评论区评论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容