偶遇FFMpeg(四)-FFmpeg PC端推流

开编

之前在Android集成FFmpeg。主要还是基于命令行的方式进行操作。刚刚好最近又在研究推流相关的东西。看了一些博文。和做了一些实践。 就希望通过本文记录袭来。 本文的大体结构如下

目录.png

FFMPEG 开发环境搭建

笔者是在 Windows10 64+Visual Studio2017的环境下开发的

下载和安装VisualStudio2017

去官网下载和安装就可以

在项目中配置FFMPEG

  1. 下载FFMPEG相关的文件和解压 从FFMPEG WINDOW BUILD中下载 devshared两个部分的内容

下载示例图.png

  • dev压缩包内

dev_package.png

  • shared压缩包内

shared_package.png

  1. 创建VisualStudio项目和配置FFMPEG
  • 创建控制台项目

创建VisualStudio项目.png

  • 在项目中配置依赖项(重点)
  • 在左上角,点击项目。最后一下的弹出框中进行配置。

项目相关配置.png

  • 然后将dll的文件复制到当前的目录下。

文件复制到当前.png

  • 将Window编译调试,选择到正确的x64

正确的x64.png

  • 处理一些错误。让程序跑起来
  • 错误1: av_register_all过时。 解决方法: 暂时没有什么更好的办法,只能去头文件里面。把attribute_deprecated注释掉了

推流代码

大致先了解一下结构体和结构体之间的关系

结构体关系

结构体关系.png

结构体

  • AVFormatContext AVFormatContext是格式封装的上下文对象。 在这里,会比较熟悉的常用的成员变量有:
    • AVIOContext *pb:用来合成音频和视频,或者分解的AVIOContext
    • unsigned int nb_streams:视音频流的个数
    • AVStream **streams:视音频流
    • char filename[1024]:文件名
    • AVDictionary *metadata:存储视频元信息的metadata对象。
  • AVDictionaryEntry 每一条元数据分为keyvalue两个属性。
typedef struct AVDictionaryEntry {
    char *key;
    char *value;
} AVDictionaryEntry;

可以根据下面代码。取出这些数据

    AVFormatContext *fmt_ctx = NULL;
    AVDictionaryEntry *tag = NULL;
    int ret;

    if ((ret = avformat_open_input(&fmt_ctx, argv[1], NULL, NULL)))
        return ret;

    while ((tag = av_dict_get(fmt_ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)))
        printf("%s=%s\n", tag->key, tag->value);

    avformat_close_input(&fmt_ctx);
  • AVRational 表示媒体信息的一些分数,是分母和分子的结构。计算过程中,会多次使用这样的数据结构
typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;
  • 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所属的视频/音频流。

FFMPEG推流的套路

套路图如下:

FFMPEG推流的套路.png

整个方法的流向:

copy from leixiaohua.png

首先,我们先来熟悉一下这个整体的套路。其实推流的过程。我的理解是,经过解封装,按照原来的数据结构,提取和转成目标数据结构进行发送。 因为FFmpeg做好了封装,我们只要对其调用方法就可以了。

按照套路图,我们知道,使用FFmpeg的话

  1. 第一步是得到整体封装的输入和输出的上下文对象AVFormatContext
    //注册所有的
    av_register_all();
    //初始化网络
    avformat_network_init();
    //配置输入和输出
    const char *inUrl = "dongfengpo.flv";
    const char *outUrl = "rtmp://localhost/live/test";

    AVFormatContext *ictx = NULL;
        //得到输入的上下文
    int ret = avformat_open_input(&ictx, inUrl, NULL, NULL);
    if (ret < 0)
    {
        return avError2(ret);
    }
    cout << " avformat_open_input success! " << endl;

    //去打印结果
    ret = avformat_find_stream_info(ictx, NULL);
    if (ret < 0)
    {
        return avError2(ret);
    }

    //将AVFormat打印出来
    av_dump_format(ictx, 0, inUrl, 0);

    //开始处理输出流
    int videoIndex = 0;
    //0.先得到AVFormat
    AVFormatContext *octx;
    AVOutputFormat *ofmt = NULL;

    ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
    if (ret < 0)
    {
        return avError2(ret);
    }
    cout << "avformat_alloc_output_context2 success!" << endl;

    ofmt = octx->oformat;
  1. 再创建输出的AVStream,并从输入AVFormatContext的其中取得AVStream,将对应的参数(主要是编码器信息)copy到其中。
        //开始遍历流,进行对应stream的创建
    for (int i = 0; i < ictx->nb_streams; i++)
    {
        //这里开始要创建一个新的AVStream
        AVStream *stream = ictx->streams[i];

        //判断是否是videoIndex。这里先记录下视频流。后面会对这个流进行操作
        if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            videoIndex = i;
        }

               //创建输出流
        AVCodec *c = avcodec_find_decoder(stream->codecpar->codec_id);
        AVStream *os = avformat_new_stream(octx, c);

        //应该将编解码器的参数从input中复制过来
                // 这里要注意的是,因为 os->codec这样的取法,已经过时了。所以使用codecpar
        ret = avcodec_parameters_copy(os->codecpar, stream->codecpar);
        if (ret < 0)
        {
            return avError2(ret);
        }
        cout << "avcodec_parameters_copy success!" << endl;
        cout << "avcodec_parameters_copy success! in stream codec tag" << stream->codecpar->codec_tag << endl;
        cout << "avcodec_parameters_copy success! out stream  codec tag" << os->codecpar->codec_tag << endl;

        //复制成功之后。还需要设置 codec_tag(编码器的信息?)
        os->codecpar->codec_tag = 0;
    }

    //检查一遍我们的输出
    av_dump_format(octx, 0, outUrl, 1);
  1. 因为是推流,所以第三部,就是通过avio_open链接网址,做好推流的准备
        //开始使用io进行推流
        //通过AVIO_FLAG_WRITE这个标记位,打开输出的AVFormatContext->AVIOContext
    ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
    if (ret < 0)
    {
        return avError2(ret);
    }
    cout << "avio_open success!" << endl;
  1. 推流的过程。首先通过 avformat_write_header写入头部信息。接着是通过av_read_frame函数读取输入的frame的数据,写入到AVPakcet 当中。处理每一帧的ptsdts。再通过av_interleaved_write_frame将这一个帧发送出去。最后,通过av_packet_unref释放AVPacket
    //先写头
    ret = avformat_write_header(octx, 0);
    if (ret < 0)
    {
        return avError2(ret);
    }
    //取得到每一帧的数据,写入
    AVPacket pkt;

    //为了让我们的代码发送流的速度,相当于整个视频播放的数据。需要记录程序开始的时间
    //后面再根据,每一帧的时间。做适当的延迟,防止我们的代码发送的太快了
    long long start_time = av_gettime();
    //记录视频帧的index,用来计算pts
    long long frame_index = 0;

    while (true)
    {
        //输入输出视频流
        AVStream *in_stream, *out_stream;

        //从输入流中读取数据 frame到AVPacket当中
        ret = av_read_frame(ictx, &pkt);
        if (ret < 0)
        {
            break;
        }

        //没有显示时间的时候,才会进入计算和校验
        //没有封装格式的裸流(例如H.264裸流)是不包含PTS、DTS这些参数的。在发送这种数据的时候,需要自己计算并写入AVPacket的PTS,DTS,duration等参数。如果没有pts,则进行计算
        if (pkt.pts == AV_NOPTS_VALUE)
        {
            //AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。
             //先得到流中的time_base
            AVRational time_base = ictx->streams[videoIndex]->time_base;
            //开始校对pts和 dts.通过time_base和dts转成真正的时间
            //得到的是每一帧的时间
            /*
            r_frame_rate 基流帧速率 。取得是时间戳内最小的帧的速率 。每一帧的时间就是等于 time_base/r_frame_rate
            av_q2d 转化为double类型
            */
            int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(ictx->streams[videoIndex]->r_frame_rate);
            //配置参数  这些时间,都是通过 av_q2d(time_base) * AV_TIME_BASE 来转成实际的参数
            pkt.pts = (double)(frame_index * calc_duration) / (double)av_q2d(time_base) * AV_TIME_BASE;
            //一个GOP中,如果存在B帧的话,只有I帧的dts就不等于pts
            pkt.dts = pkt.pts;
            pkt.duration = (double)calc_duration / (double)av_q2d(time_base) * AV_TIME_BASE;
        }

        //开始处理延迟.只有等于视频的帧,才会处理
        if (pkt.stream_index == videoIndex)
        {
            //需要计算当前处理的时间和开始处理时间之间的间隔??

            //0.先取时间基数
            AVRational time_base = ictx->streams[videoIndex]->time_base;

            //AV_TIME_BASE_Q 用小数表示的时间基数。等于时间基数的倒数
            AVRational time_base_r = { 1, AV_TIME_BASE };

            //计算视频播放的时间. 公式等于 pkt.dts * time_base / time_base_r`
            //.其实就是 stream中的time_base和定义的time_base直接的比例
            int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_r);
            //计算实际视频的播放时间。 视频实际播放的时间=代码处理的时间??
            int64_t now_time = av_gettime() - start_time;

            cout << time_base.num << " " << time_base.den << "  " << pkt.dts << "  " << pkt.pts << "   " << pts_time << endl;
            //如果显示的pts time 比当前的时间迟,就需要手动让程序睡一会,再发送出去,保持当前的发送时间和pts相同
            if (pts_time > now_time)
            {
                //睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
                av_usleep((unsigned int)(pts_time - now_time));
            }
        }

                //重新计算一次pts和dts.主要是通过 in_s的time_base 和 out_s的time_base进行计算和校对
        //先取得stream
        in_stream = ictx->streams[pkt.stream_index];
        out_stream = octx->streams[pkt.stream_index];

        //重新开始指定时间戳
        //计算延时后,重新指定时间戳。 这次是根据 in_stream 和 output_stream之间的比例
        //计算dts时,不再直接用pts,因为如有有B帧,就会不同
        //pts,dts,duration都也相同
        pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        pkt.duration = (int)av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
        //再次标记字节流的位置,-1表示不知道字节流的位置
        pkt.pos = -1;

        //如果当前的帧是视频帧,则将我们定义的frame_index往后推
        if (pkt.stream_index == videoIndex)
        {
            printf("Send %8d video frames to output URL\n", frame_index);
            frame_index++;
        }

        //发送!!!
        ret = av_interleaved_write_frame(octx, &pkt);
        if (ret < 0)
        {
            printf("发送数据包出错\n");
            break;
        }

        //使用完了,记得释放
        av_packet_unref(&pkt);
    }
    //写文件尾(Write file trailer)  
    av_write_trailer(octx);
    avformat_close_input(&ictx);
    /* close output */
    if (ictx && !(octx->flags & AVFMT_NOFILE))
        avio_close(octx->pb);
    avformat_free_context(octx);
    if (ret < 0 && ret != AVERROR_EOF) {
        printf("Error occurred.\n");
        return -1;
    }

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏我就是马云飞

RxJava2 实战知识梳理(2) - 计算一段时间内数据的平均值

前言 今天,我们继续跟着 RxJava-Android-Samples 的脚步,一起看一下RxJava2在实战当中的应用,在这个项目中,第二个的例子的描述如下:...

2516
来自专栏架构之路

超清晰的makefile解释、编写与示例

Makefile范例教学 Makefile和GNU make可能是linux世界里最重要的档案跟指令了。编译一个小程式,可以用简单的command来进行编译;稍...

4608
来自专栏DeveWork

【译】WordPress 中的50个过滤器(1):何为过滤器?

这篇文章是来自tutsplus 上系列文章《50 Filters of WordPress》的开篇文,系列文章还在陆续发表中。Jeff 打算借助Github 进...

1919
来自专栏java工会

从零讲JAVA ,给你一条清晰地学习道路!该学什么就学什么!!

1807
来自专栏北京马哥教育

一致性hash原理与实现

一、背景介绍 memcached的分布式 memcached虽然称为“分布式”缓存服务器,但服务器端并没有“分布式”功能。服务器端内存存储功能,其实现非常简单。...

3907
来自专栏CDA数据分析师

SAS | 如何网络爬虫抓取网页数据

本人刚刚完成SAS正则表达式的学习,初学SAS网络爬虫,看到过一些前辈大牛们爬虫程序,感觉很有趣。现在结合实际例子,浅谈一下怎么做一些最基本的网页数据抓取。第一...

3588
来自专栏Crossin的编程教室

Python 爬虫爬取美剧网站

一直有爱看美剧的习惯,一方面锻炼一下英语听力,一方面打发一下时间。之前是能在视频网站上面在线看的,可是自从广电总局的限制令之后,进口的美剧英剧等貌似就不在像以前...

4197
来自专栏GopherCoder

『Go 语言学习专栏』-- 第十一期

1523
来自专栏开源FPGA

基于basys2驱动LCDQC12864B的verilog设计图片显示

  话不多说先上图 ? 前言        在做这个实验的时候在网上找了许多资料,都是关于使用单片机驱动LCD显示,确实用单片机驱动是要简单不少,记得在FPGA...

2595
来自专栏Android相关

处理器结构--MicroOp &&MacroOp Fusion

也成为微指令操作融合,将多个相同的汇编指令编译的uops融合到一个微指令中,使得ALU在执行指令时可以在一个Cycle中执行完毕,提高指令执行的吞吐量

1283

扫码关注云+社区

领取腾讯云代金券