基于FFmpeg进行RTMP推流(一)

简介

  • 开发环境
  • FFmpeg sdk下载
  • 项目配置
  • 代码流程

开发环境

vs 2017

FFmpeg sdk下载

下载地址
这里下载3.3.3 — 32bit — share和Dev

8.png

  • Shared包含运行时的动态库在bin目录下
  • Dev包含开发是编译需要的头文件(include目录下)和库文件(lib目录下)

项目配置

先看下项目的目录结构

9.png

这里的bin、include、lib就是我们刚才在FFmpeg下载的相关文件。
src是我们的项目源码目录。
新建Win32控制台应用程序、选择位置、项目名称。注意:去掉“为结局方案创建目录”的勾选
10.png

然后选择空项目、去掉预编译头。完成项目的创建
11.png

项目属性配置

右击项目属性

  • 【常规】=>【输出目录】 修改为..\..\bin
  • 【调试】=>【工作目录】修改为..\..\bin
  • 【C/C++】=>【常规】=>【附加包含目录】修改为..\..\include
    -【链接器】=>【常规】=>【附加库目录】修改为..\..\lib
    注意:这里所有的路径都是相对路径,相对于源码的路径
    这里设置输出目录到bin。是因为win下运行时会默认在当前运行的目录下寻找dll文件。而我们的dll文件放在bin目录下。

开发流程

Flow Chart.png

流程详解

av_register_all()

该方法初始化所有的封装和解封装。在使用FFmpeg的时候首先要调用这个方法。
找到这个方法的源码libavformat\allformats.c

void av_register_all(void)
{
    static AVOnce control = AV_ONCE_INIT;

    ff_thread_once(&control, register_all);
}

这里控制注册方法只被调用一次register_all是函数指针。看到实现部分。

static void register_all(void)
{
    avcodec_register_all();

    /* (de)muxers */
    REGISTER_MUXER   (A64,              a64);
    REGISTER_DEMUXER (AA,               aa);
    REGISTER_DEMUXER (AAC,              aac);
//...
}

这里面就是进行各种注册,而REGISTER_MUXER 、REGISTER_DEMUXER 是前面定义的宏。我们看到是静态方法,说明该方法只能在所在的文件中使用,这也防止被注册多次。

#define REGISTER_MUXER(X, x)                                            \
    {                                                                   \
        extern AVOutputFormat ff_##x##_muxer;                           \
        if (CONFIG_##X##_MUXER)                                         \
            av_register_output_format(&ff_##x##_muxer);                 \
    }

#define REGISTER_DEMUXER(X, x)                                          \
    {                                                                   \
        extern AVInputFormat ff_##x##_demuxer;                          \
        if (CONFIG_##X##_DEMUXER)                                       \
            av_register_input_format(&ff_##x##_demuxer);                \
    }

#define REGISTER_MUXDEMUX(X, x) REGISTER_MUXER(X, x); REGISTER_DEMUXER(X, x)

av_register_input_format和av_register_output_format我们也可以单独去初始化。这里内部细节就不做过多介绍。

avformat_network_init()

网络相关初始化。如果我们使用了网络拉流和推流等等,要先初始化。

avformat_open_input()

声明是

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

定义在libavformat\utils.c中。主要功能

  • 输入输出结构体AVIOContext的初始化;
  • 输入数据的协议URLProtocol,通过函数指针的方式,与FFMPEG关联,剩下的就是调用该URLProtocol的函数进行open,read等操作了
avformat_find_stream_info
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

可以读取视音频数据并且获得一些相关的信息。定义在libavformat\utils.c

avformat_alloc_output_context2
int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat,
                                   const char *format_name, const char *filename);

定义在libavformat\mux.c

  • ctx:函数调用成功之后创建的AVFormatContext结构体。
  • oformat:指定AVFormatContext中的AVOutputFormat,用于确定输出格式。如果指定为NULL,可以设定后两个参数(format_name或者filename)由FFmpeg猜测输出格式。
    PS:使用该参数需要自己手动获取AVOutputFormat,相对于使用后两个参数来说要麻烦一些。
  • format_name:指定输出格式的名称。根据格式名称,FFmpeg会推测输出格式。输出格式可以是“flv”,“mkv”等等。
  • filename:指定输出文件的名称。根据文件名称,FFmpeg会推测输出格式。文件名称可以是“xx.flv”,“yy.mkv”等等。
    函数执行成功的话,其返回值大于等于0。

内部流程

  • 调用avformat_alloc_context()初始化一个默认的AVFormatContext。
  • 如果指定了输入的AVOutputFormat,则直接将输入的AVOutputFormat赋值给AVOutputFormat的oformat。如果没有指定输入的AVOutputFormat,就需要根据文件格式名称或者文件名推测输出的AVOutputFormat。无论是通过文件格式名称还是文件名推测输出格式,都会调用一个函数av_guess_format()。
avio_open

打开FFmpeg的输入输出文件

int avio_open2(AVIOContext **s, const char *url, int flags,
               const AVIOInterruptCB *int_cb, AVDictionary **options);
  • s:函数调用成功之后创建的AVIOContext结构体。
  • url:输入输出协议的地址(文件也是一种“广义”的协议,对于文件来说就是文件的路径)。
  • flags:打开地址的方式。可以选择只读,只写,或者读写。取值如下。
    AVIO_FLAG_READ:只读。
    AVIO_FLAG_WRITE:只写。
    AVIO_FLAG_READ_WRITE:读写。
  • int_cb:不太清楚
  • options:不太清楚
avformat_write_header

写视频文件头,av_write_trailer()用于写视频文件尾

av_read_frame

定义在libavformat\utils.c
读取码流中的音频若干帧或者视频一帧。解码视频的时候,每解码一个视频帧,需要先调用 av_read_frame()获得一帧视频的压缩数据,然后才能对该数据进行解码(例如H.264中一帧压缩数据通常对应一个NAL)。
这里我贴上官方的注释,很详细:

/**
 * Return the next frame of a stream.
 * This function returns what is stored in the file, and does not validate
 * that what is there are valid frames for the decoder. It will split what is
 * stored in the file into frames and return one for each call. It will not
 * omit invalid data between valid frames so as to give the decoder the maximum
 * information possible for decoding.
 *
 * If pkt->buf is NULL, then the packet is valid until the next
 * av_read_frame() or until avformat_close_input(). Otherwise the packet
 * is valid indefinitely. In both cases the packet must be freed with
 * av_packet_unref when it is no longer needed. For video, the packet contains
 * exactly one frame. For audio, it contains an integer number of frames if each
 * frame has a known fixed size (e.g. PCM or ADPCM data). If the audio frames
 * have a variable size (e.g. MPEG audio), then it contains one frame.
 *
 * pkt->pts, pkt->dts and pkt->duration are always set to correct
 * values in AVStream.time_base units (and guessed if the format cannot
 * provide them). pkt->pts can be AV_NOPTS_VALUE if the video format
 * has B-frames, so it is better to rely on pkt->dts if you do not
 * decompress the payload.
 *
 * @return 0 if OK, < 0 on error or end of file
 */

总结起来每段的核心意思

  • 读取码流中的音频若干帧或者视频一帧
  • 如果pkt->buf是空,那么就要等待下一次av_read_frame调用。否则无法确定是否有效
  • pts dts duration通常被设置为正确的值。但如果视频帧包括Bzh帧,那么pts可以是AV_NOPTS_VALUE。所以最好依赖dts。
av_interleaved_write_frame

输出一帧视音频数据

核心类

AVFormatContext

AVFormatContext是一个贯穿始终的数据结构,很多函数都要用到它作为参数。它是FFMPEG解封装(flv,mp4,rmvb,avi)功能的结构体。
内部的成员变量,大家可以查看头文件。这里我们列举下一些常用重要的成员变量:

  • struct AVInputFormat *iformat:输入数据的封装格式
  • AVIOContext *pb:输入数据的缓存
  • unsigned int nb_streams:视音频流的个数
  • AVStream **streams:视音频流
  • char filename[1024]:文件名
  • int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000)
  • int bit_rate:比特率(单位bps,转换为kbps需要除以1000)
  • AVDictionary *metadata:元数据

视频的原数据(metadata)信息可以通过AVDictionary获取。元数据存储在AVDictionaryEntry结构体中

typedef struct AVDictionaryEntry {
    char *key;
    char *value;
} AVDictionaryEntry;

每一条元数据分为key和value两个属性。
在ffmpeg中通过av_dict_get()函数获得视频的原数据。

    cout << endl << endl << "======元信息=======" << endl;
    string meta, key, value;
    AVDictionaryEntry *m = NULL;
    while (m = av_dict_get(ictx->metadata, "", m, AV_DICT_IGNORE_SUFFIX)) {
        key=m->key;
        value=m->value;
        meta.append(key).append("\t:").append(value).append("\r\n");
    }
    cout << meta.c_str() << endl;
AVStream

AVStream是存储每一个视频/音频流信息的结构体。

  • int index:标识该视频/音频流
  • AVCodecContext *codec:指向该视频/音频流的AVCodecContext(它们是一一对应的关系)
  • AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。- FFMPEG其他结构体中也有这个字段,但是根据我的经验,只有AVStream中的time_base是可用的。PTS*time_base=真正的时间
  • int64_t duration:该视频/音频流长度
  • AVDictionary *metadata:元数据信息
  • AVRational avg_frame_rate:帧率(注:对视频来说,这个挺重要的)
  • AVPacket attached_pic:附带的图片。比如说一些MP3,AAC音频文件附带的专辑封面。
AVPacket

AVPacket是存储压缩编码数据相关信息的结构体。

  • uint8_t *data:压缩编码的数据。
    例如对于H.264来说。1个AVPacket的data通常对应一个NAL。
    注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流
    因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。

  • int size:data的大小

  • int64_t pts:显示时间戳

  • int64_t dts:解码时间戳

  • int stream_index:标识该AVPacket所属的视频/音频流。

源码

#include <iostream>
using namespace std;
//引入头文件
extern "C"
{
#include "libavformat/avformat.h"
    //引入时间
#include "libavutil/time.h"
}
//引入库
#pragma comment(lib,"avformat.lib")
//工具库,包括获取错误信息等
#pragma comment(lib,"avutil.lib")
//编解码的库
#pragma comment(lib,"avcodec.lib")

int avError(int errNum);

static double r2d(AVRational r)
{
    return r.num == 0 || r.den == 0 ? 0. : (double)r.num / (double)r.den;
}
int main() {
    //所有代码执行之前要调用av_register_all和avformat_network_init
    //初始化所有的封装和解封装 flv mp4 mp3 mov。不包含编码和解码
    av_register_all();

    //初始化网络库
    avformat_network_init();

    //使用的相对路径,执行文件在bin目录下。test.mp4放到bin目录下即可
    const char *inUrl = "test.flv";
    //输出的地址
    const char *outUrl = "rtmp://192.168.136.131/live/test";

    //////////////////////////////////////////////////////////////////
    //                   输入流处理部分
    /////////////////////////////////////////////////////////////////
    //打开文件,解封装 avformat_open_input
    //AVFormatContext **ps  输入封装的上下文。包含所有的格式内容和所有的IO。如果是文件就是文件IO,网络就对应网络IO
    //const char *url  路径
    //AVInputFormt * fmt 封装器
    //AVDictionary ** options 参数设置
    AVFormatContext *ictx = NULL;

    //打开文件,解封文件头
    int ret = avformat_open_input(&ictx, inUrl, 0, NULL);
    if (ret < 0) {
        return avError(ret);
    }
    cout << "avformat_open_input success!" << endl;
    //获取音频视频的信息 .h264 flv 没有头信息
    ret = avformat_find_stream_info(ictx, 0);
    if (ret != 0) {
        return avError(ret);
    }
    //打印视频视频信息
    //0打印所有  inUrl 打印时候显示,
    av_dump_format(ictx, 0, inUrl, 0);

    //////////////////////////////////////////////////////////////////
    //                   输出流处理部分
    /////////////////////////////////////////////////////////////////
    AVFormatContext * octx = NULL;
    //如果是输入文件 flv可以不传,可以从文件中判断。如果是流则必须传
    //创建输出上下文
    ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
    if (ret < 0) {
        return avError(ret);
    }
    cout << "avformat_alloc_output_context2 success!" << endl;
    //配置输出流
    //AVIOcontext *pb  //IO上下文
    //AVStream **streams  指针数组,存放多个输出流  视频音频字幕流
    //int nb_streams;
    //duration ,bit_rate

    //AVStream
    //AVRational time_base
    //AVCodecParameters *codecpar 音视频参数
    //AVCodecContext *codec
    //遍历输入的AVStream
    for (int i = 0; i < ictx->nb_streams; i++) {
        //创建一个新的流到octx中
        AVStream *out = avformat_new_stream(octx, ictx->streams[i]->codec->codec);
        if (!out) {
            return avError(0);
        }
        //复制配置信息 用于mp4 过时的方法
        //ret=avcodec_copy_context(out->codec, ictx->streams[i]->codec);
        ret = avcodec_parameters_copy(out->codecpar, ictx->streams[i]->codecpar);
        if (ret < 0) {
            return avError(ret);
        }
        out->codec->codec_tag = 0;
    }
    av_dump_format(octx, 0, outUrl, 1);

    //////////////////////////////////////////////////////////////////
    //                   准备推流
    /////////////////////////////////////////////////////////////////

    //打开IO
    ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
    if (ret < 0) {
        avError(ret);
    }

    //写入头部信息
    ret = avformat_write_header(octx, 0);
    if (ret < 0) {
        avError(ret);
    }
    cout << "avformat_write_header Success!" << endl;

    //推流每一帧数据
    //int64_t pts  [ pts*(num/den)  第几秒显示]
    //int64_t dts  解码时间 [P帧(相对于上一帧的变化) I帧(关键帧,完整的数据) B帧(上一帧和下一帧的变化)]  有了B帧压缩率更高。
    //uint8_t *data    
    //int size
    //int stream_index
    //int flag
    AVPacket avPacket;
    //获取当前的时间戳  微妙
    long long startTime = av_gettime();
    while (true)
    {
        ret = av_read_frame(ictx, &avPacket);
        if (ret < 0) {
            break;
        }
        cout << avPacket.pts << " " << flush;
        //计算转换时间戳 pts dts
        //获取时间基数
        AVRational itime = ictx->streams[avPacket.stream_index]->time_base;
        AVRational otime = octx->streams[avPacket.stream_index]->time_base;
        avPacket.pts = av_rescale_q_rnd(avPacket.pts, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
        avPacket.dts = av_rescale_q_rnd(avPacket.pts, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
        //到这一帧时候经历了多长时间
        avPacket.duration = av_rescale_q_rnd(avPacket.duration, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
        avPacket.pos = -1;
        //视频帧推送速度
        if (ictx->streams[avPacket.stream_index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            AVRational tb = ictx->streams[avPacket.stream_index]->time_base;
            //已经过去的时间
            long long now = av_gettime() - startTime;
            long long dts = 0;
            dts = avPacket.dts * (1000 * 1000 * r2d(tb));
            if (dts > now)
                av_usleep(dts - now);
            else {
                cout << "sss";
            }
        }
        //推送  会自动释放空间 不需要调用av_packet_unref
        ret = av_interleaved_write_frame(octx, &avPacket);
        if (ret < 0) {
            break;
        }
        //视频帧推送速度
        //if (avPacket.stream_index == 0)
        //  av_usleep(30 * 1000);
        //释放空间。内部指向的视频空间和音频空间
        //av_packet_unref(&avPacket);
    }
    return 0;
}

int avError(int errNum) {
    char buf[1024];
    //获取错误信息
    av_strerror(errNum, buf, sizeof(buf));
    cout << " failed! " << buf << endl;
    return -1;
}

彩蛋

上面的代码在推送flv格式文件时候可能没问题,当换成mp4或者rmvb时候可能出现各种问题。如果你是在无法解开这个问题,请看下节基于FFmpeg进行RTMP推流(二)

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

推荐阅读更多精彩内容