简析Android集成FFMPEG推流

如今,视频直播行业在中国算是比较热门。在刚刚过去的2015年,视频直播成为互联网行业最抢眼的领域之一。从游戏到秀场,从传统的网页端到移动互联网,各大直播平台包括斗鱼、熊猫tv、虎牙战旗还有纯移动端的映客、易直播等,群雄割据。言归正转,今天我们讲解的是Android 端简要集成FFMPEG进行推流。

推流基本流程

推流基本流程

一般情况下推流的基本流程如上。

FFmpeg

说到推流就不得不提大名鼎鼎的FFMPEG了,FFmpeg是一个开源免费跨平台的视频和音频流方案,属于自由软件,采用LGPL或GPL许可证(依据你选择的组件)。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多codec都是从头开发的。诸如微信的小视频,映客、斗鱼等均是在FFmpeg基础上开发而来,可见FFmpeg之强大。一下是解压微信的apk的截图。

微信使用的动态链接库

FFmpeg 在Android中的使用

FFmpeg要想在Android就必须要编译成Android上能使用so动态链接库(下称so库)。具体编译方法可以参考github官方提供源码。编译过程大约在30分钟左右。以下是笔者编译好的so库。


编译好的so库
FFmpeg主要分为8大模块:
  • libavcodec:用于各种类型声音/图像编解码;
  • libavdevice:用于视频采集访问摄像头等
  • libavfilter:用做滤镜处理
  • libavformat:用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能;
  • libswresample:用于重采样;
  • libavutil:包含一些公共的工具函数;
  • libswscale:用于视频场景比例缩放、色彩映射转换;
  • libpostproc:用于后期效果处理;

JNI开发调用FFmpeg

我们先看看我们使用到的FFmpegUtils工具类,主要用于加载native方法:

public class FFmpegUtils {

//加载so库
static{
      System.loadLibrary("avutil-54");
      System.loadLibrary("swresample-1");
      System.loadLibrary("avcodec-56");
      System.loadLibrary("avformat-56");
      System.loadLibrary("swscale-3");
      System.loadLibrary("postproc-53");
      System.loadLibrary("avfilter-5");
      System.loadLibrary("avdevice-56");
      System.loadLibrary("sffstreamer");
}
     /*
      * 把本地的音视频数据通过rtmp协议发送到流媒体服务器上
      *@param input 
      *@param output
      */
     public native int stream(String input, String output);

}

接下来我们就是我们的重头戏JNI调用FFmpeg

#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/log.h"

#define LOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR, "(>_<)", format, ##__VA_ARGS__)
#define LOGI(format, ...) __android_log_print(ANDROID_LOG_INFO, "(^_^)", format, ##__VA_ARGS__)
JNIEXPORT jint JNICALL Java_org_loofer_ffmpegstreamer_MainActivity_stream  (JNIEnv *env, jobject obj, jstring input_jstr, jstring output_jstr)
{


    //java string -> c char*
    //视频文件所在路径
    const char* input_cstr = (*env)->GetStringUTFChars(env,input_jstr, NULL));
    //推送的流媒体地址
    const char* output_cstr = (*env)->GetStringUTFChars(env,output_jstr, NULL));
    //封装格式(读入,写出)(解封装,得到frame)
    AVFormatContext *inFmtCtx = NULL, *outFmtCtx = NULL;
    AVOutputFormat *ofmt = NULL;
    
    AVPacket pkt;


    //注册组件
    av_register_all();
    //初始化网络
    avformat_network_init();

    //Input
    if ((ret = avformat_open_input(&inFmtCtx, input_str, 0, 0)) < 0) {
        LOGE( "Could not open input file.");
        goto end;
    }
    //获取文件信息
    if ((ret = avformat_find_stream_info(inFmtCtx, 0)) < 0) {
        LOGE( "Failed to retrieve input stream information");
        goto end;
    }
    //获取视频的索引位置
    int videoindex=-1;
    for(i=0; i<inFmtCtx->nb_streams; i++) 
        if(inFmtCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
            videoindex=i;
            break;
        }
    //输出封装格式,推送flv封装格式的视频流
    avformat_alloc_output_context2(&outFmtCtx, NULL, "flv",output_cstr); //RTMP
    //avformat_alloc_output_context2(&outFmtCtx, NULL, "mpegts", output_cstr);//UDP

    if (!outFmtCtx) {
        LOGE( "Could not create output context\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    
    
    for (i = 0; i < inFmtCtx->nb_streams; i++) {
        //解码器,解码上下文保持一致
        AVStream *in_stream = inFmtCtx->streams[i];
        AVStream *out_stream = avformat_new_stream(outFmtCtx, in_stream->codec->codec);
        if (!out_stream) {
            LOGE( "Failed allocating output stream\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }
        //复制解码器上下文的 设置
        ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
        if (ret < 0) {
            LOGE( "Failed to copy context from input to output stream codec context\n");
            goto end;
        }
        //全局的header
        out_stream->codec->codec_tag = 0;
        if (outFmtCtx->oformat->flags & AVFMT_GLOBALHEADER)
            out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
    }
    //打开输出的AVIOContext IO流上下文
    ofmt = outFmtCtx->oformat;
    //Open output URL
    if (!(ofmt->flags & AVFMT_NOFILE)) {
        ret = avio_open(&outFmtCtx->pb, output_cstr, AVIO_FLAG_WRITE);
        if (ret < 0) {
            LOGE( "Could not open output URL '%s'", output_cstr);
            goto end;
        }
    }
    //先写一个头
    ret = avformat_write_header(outFmtCtx, NULL);
    if (ret < 0) {
        LOGE( "Error occurred when opening output URL\n");
        goto end;
    }

    
    int frame_index=0;
    int64_t start_time=av_gettime();
    while (1) {
        AVStream *in_stream, *out_stream;
        //Get an AVPacket
        ret = av_read_frame(inFmtCtx, &pkt);
        if (ret < 0)
            break;
        //FIX:No PTS (Example: Raw H.264)
        //raw stream 裸流
        //PTS:Presentation Time Stamp 解码后视频帧要在什么时候取出来
        //DTS:送入解码器后什么时候标识进行解码
        if(pkt.pts==AV_NOPTS_VALUE){
            //Write PTS
            AVRational time_base1=inFmtCtx->streams[videoindex]->time_base;
            //Duration between 2 frames (us)
            int64_t calc_duration=(double)AV_TIME_BASE/av_q2d(inFmtCtx->streams[videoindex]->r_frame_rate);
            //Parameters
            pkt.pts=(double)(frame_index*calc_duration)/(double)(av_q2d(time_base1)*AV_TIME_BASE);
            pkt.dts=pkt.pts;
            pkt.duration=(double)calc_duration/(double)(av_q2d(time_base1)*AV_TIME_BASE);
        }
        //读入速度比较快,可以在这里调整读取速度减轻服务器压力
        if(pkt.stream_index==videoindex){
            AVRational time_base=inFmtCtx->streams[videoindex]->time_base;
            AVRational time_base_q={1,AV_TIME_BASE};
            int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
            int64_t now_time = av_gettime() - start_time;
            if (pts_time > now_time)
                av_usleep(pts_time - now_time);

        }

        in_stream  = inFmtCtx->streams[pkt.stream_index];
        out_stream = outFmtCtx->streams[pkt.stream_index];
        /* copy packet */
        //Convert PTS/DTS
        pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
        pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
        pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
        pkt.pos = -1;
        //Print to Screen
        if(pkt.stream_index==videoindex){
            LOGE("Send %8d video frames to output URL\n",frame_index);
            frame_index++;
        }
        //写数据
        //ret = av_write_frame(outFmtCtx, &pkt);
        ret = av_interleaved_write_frame(outFmtCtx, &pkt);

        if (ret < 0) {
            LOGE( "Error muxing packet\n");
            break;
        }
        av_free_packet(&pkt);
        
    }
    //写结尾
    av_write_trailer(outFmtCtx);
end:
    //释放自愿
    avformat_close_input(&inFmtCtx);
    /* 关闭输出流 */
    if (outFmtCtx && !(ofmt->flags & AVFMT_NOFILE))
        avio_close(outFmtCtx->pb);
    avformat_free_context(outFmtCtx);
    if (ret < 0 && ret != AVERROR_EOF) {
        LOGE( "Error occurred.\n");
        return -1;
    }
    return 0;
}

为了更好的理解整个推流过程,最后放几张图

FFmpeg播放一个文件的流程
解码流程图

函数简单介绍

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

推荐阅读更多精彩内容