美文网首页
windows平台ffmpeg学习笔记

windows平台ffmpeg学习笔记

作者: DD_Dog | 来源:发表于2019-10-25 14:19 被阅读0次

    学习视频:
    链接:https://pan.baidu.com/s/1rCKRxQcE3TxaGqXJhGRY6Q
    提取码:bpji
    视频顺序有点乱,我是按照下面的顺序学习的,可以参考。

    一、音视频基础

    1. mux封装:复用,按一定格式组织原音视频流,例如进行时间同步等
      demux解封装:解复用,按一定格式解出音视频流

    2. ES流,原始流,直接从编码器出来的数据流。
      PES流,P(packet),ES形成的分组称为PES分组,是用来传递ES的一种数据布局。
      TS流:ES形成的分组称为TS分组,是用来传递ES的一种数据布局。可以被任意截断
      rtsp流:RTSP(Real Time Streaming Protocol),RFC2326,实时流传输协议,是TCP/IP协议
      体系中的一个应用层协议。
      rtmp流:Real Time Messaging Protocol(实时消息传输协议),是abobe公司的协议
      hls流:苹果流,是Apple的动态码率自适应技术。主要用于PC和Apple终端的音视频服务。
      包括一个m3u8索引文件,TS媒体分片文件。

    3. 服务端:为客户端提供服务,提供数据服务
      客户端:为客户提供本地服务
      流媒体:采用酒店式传输方式在Internet播放的媒体格式。

    4. 推模式:当通知消息来之时,把所有样信息都通过参数的形式"推给"观察者
      拉模式:当通知消息来之时,通知的函数不带任何相关的信息,而是要观察者主动
      去"拉"信息
      实时流:Real Time stream实时传输的音视频流

    二、FFmpeg基础

    1. 开源库,支持Windows,Android,IOS等
    2. 音视频处理
    3. 开发语言C
    4. 源码下载:https://ffmpeg.zeranoe.com/builds/win64/static/
      可以下载源码,动态库,静态库等
    5. 基础流程:
      input file---demuxer--->encoded data packets---decoder--->decoded frames---encoder--->encoded data packets---muxer--->output file
    6. 为什么用ffmpeg?例如手机获取rtsp视频流保存到本地,要实现以下方案:
      1)实现rtsp客户端,接收音视频包
      2)解视频包(rtp->h264)
      3)解音频包(rtp->PCM(alaw))
      4)音频转码(PCM->AAC)
      5)重新封装音视频包
      而ffmpeg的调用流程:
      1)avformat_open_input 打开文件
      2)avforamt_find_stream_info 读取文件格式信息
      3)av_read_frame
      4)avforamt_alloc_output_context2 创建输出上下文
      5)av_write_frame 写输出

    三、环境搭建

    1. 下载源码,下载windows源码及动态库
    2. 新建VS项目,把源码中的include文件夹下面的头文件,lib下面的静态库.lib,以及下载的动态库中的dll库文件拷贝到工程目录下
      分别新建include,lib,bin目录存放。
    3. 项目中引入静态库:
      A、添加工程的头文件目录:工程---属性---配置属性---c/c++---常规---附加包含目录:加上头文件存放目录。
      B、添加文件引用的lib静态库路径:工程---属性---配置属性---链接器---常规---附加库目录:加上lib文件存放目录。
      C 然后添加工程引用的lib文件名:工程---属性---配置属性---链接器---输入---附加依赖项:加上lib文件名
    4. 写测试代码,例如引入extern "C" {#include "libavcodec/avcodec.h"},main方法调用av_register_all(),运行报错找不到dll动态库,
      这时需要把动态库拷贝到工程目录的运行目录,也就是生成.exe的目录。
    5. 注意,ffmpeg是C语言开发的,而VS工程是cpp语言,所以引入头文件应该使用extern "C"
      Demo源码的github地址

    四、保存网络流到本地

    ffmpeg基本工作流程:


    image.png

    ffmpeg方法流程:

    1. avformat_alloc_context();//创建输入上下文
    2. avformat_open_input();  //打开输入流
    3. avformat_find_stream_info();  //查找音视频信息
    4. avforamt_alloc_output_context2();  //创建输出上下文 
    5. avio_open();  //打开avio
    6. avformat_new_stream();  //从输入流创建输出流
        avcodec_copy_context();  //拷贝到输出流,根据nb_streams循环
    7. avformat_write_header();  //写头信息
    8. while(true){
            av_init_packet(); //初始化packet,用来存放编码过的数据
            av_read_frame();//读取帧数据
            av_interleaved_write_frame();//写数据
        }
    9. avformat_close_input();//关闭输入输出流
        avcodec_close();
    

    一些异常处理:

    1. ffmpeg avformat_open_input always returns “Protocol not found” rv=(-1330794744),在初始化的时候忘记调用av_register_all();
    2. VS引发了异常: 读取访问权限冲突,这是数组越界报的错误,点击调用堆栈可以跳到出错位置。

    关于RTSP流:
    RTSP流地址可以在网上查找,应该有很多的,当然也不仅仅限于rtsp,其它形式的流也是可以的,我使用的是:rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov,是可以使用的,但是过一段时间就不确定了,所以还是要多找找。如果不确定某个网络流是否可用,可以ping一下IP或者网址,也可以使用potPlayer打开,能ping通或者可以播放就证明流地址没问题。
    下面附上本章的Demo github地址

    五、网络流转发

    网络流转发的基本流程与上一章的网络流保存到本地类似,只不过最后输出是网络流。基本流程如下:


    image.png

    转发网络流到本地UDP端口

    1. 基于四章的工程,只需要修改OpenOutput(outputUrl)的地址即可,代码如下:
    //写到udp端口
    OpenOutput("udp://127.0.0.1:1234");     //打开输出流
    
    1. 还记得刚开始下载的ffmpeg的动态库吗,里面的bin目录下有ffplay.ext文件,这时使用cmd执行ffplay.exe udp://127.0.0.1:1234,来测试转发,
      但是报了如下错误:
    [udp @ 00000173c61ab540] bind failed: Error number -10048 occurred
    udp://127.0.0.1:1234: I/O error
    

    这是因为OpenOutput方法中的如下代码导致的:

    ret = avio_open2(&outputContext->pb, outputUrl.c_str(), AVIO_FLAG_READ_WRITE, nullptr, nullptr);
    

    我们其实只需要写权限即可,改成如下:

    ret = avio_open2(&outputContext->pb, outputUrl.c_str(), AVIO_FLAG_WRITE, nullptr, nullptr);
    

    再次重新运行就能播放转发的UDP流了。如下图所示:


    image.png

    红箭头指的窗口即为从UDP端口读取的流数据进行播放。

    1. 当然也可以使用其它播放器进行测试,我使用PotPlayer测试的,如下:
      打开输入地址:


      image.png

    播放效果:


    image.png

    转发为RTMP流
    需要修改两个地方:

    1. 修改OpenInput的地址
    OpenOutput("rtmp://127.0.0.1:1935/live/stream0");       //打开输出流
    
    1. 修改OpenInut方法中avformat_alloc_output_context2中的视频格式为flv
    int ret = avformat_alloc_output_context2(&outputContext, nullptr, "flv", outputUrl.c_str());
    

    另外如果要转发rtmp流,需要打开rtmp server,关于rtmp server的详情后面再述,现提供一个现成的可执行文件下载地址,打开crtmpserver.exe后,再使用ffplay.exe或者potplayer等播放器测试即可。

    附上本节github源码地址,upd-stream分支是转发为udp的,master是转发为rtmp的。

    使用ffmpeg的命令行实现上述功能
    其实ffmpeg的命令行即可完成我们所做的保存网络流,网络流转码等功能。
    进入下载的动态库的bin目录下cmd执行:

    1. 保存网络流命令
    ./ffmpeg.exe -i rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov -vcodec copy -acodec copy -f mpegts C:/Users/bian/Desktop/test2.mp4
    

    命令解释:
    -vcodec copy 表示视频格式直接拷贝,不做转码
    -acodec copy 表示音频格式直播拷贝,不做转码
    -f mpegts 表示输出格式
    最后加保存文件路径

    2.转发为UDP流

    ./ffmpeg.exe -i rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov -vcodec copy -acodec copy -f mpegts udp://127.0.0.1:1234
    
    1. 转发为rtmp流
      注意在转发前也要先运行crtmpserver.exe
    ./ffmpeg.exe -i rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov -vcodec copy -acodec copy -f flv rtmp://127.0.0.1:1935/live/stream0
    

    六、音频转码

    本节将PCM转为AAC格式。再来复习一下FFmpeg的工作流程:


    image.png

    本次demo也是基于保存网络流到本地的基础上的,可以看出,ffmpeg的使用流程是相当固定的。
    做出如下修改:

    1. 输入为麦克风,获取PCM数据,输入路径从设备管理器可以查看:


      image.png
    #include <dshow>
    //字符转码
    static char *dup_wchar_to_utf8(const wchar_t *w)
    {
        char *s = NULL;
        int l = WideCharToMultiByte(CP_UTF8, 0, w, -1, 0, 0, 0, 0);
        s = (char *)av_malloc(l);
        if (s)
            WideCharToMultiByte(CP_UTF8, 0, w, -1, s, l, 0, 0);
        return s;
    }
    //设置输入路径
    //避免找不到中文符号
    string fileAudioInput = dup_wchar_to_utf8(L"麦克风阵列=Realtek High Definition Audio");
    int ret = OpenInput(fileAudioInput);
    

    2. 初始化的方法中添加设备注册

    //ffmpeg初始化
    void  Init() {
        av_register_all();
        avcodec_register_all();
        avfilter_register_all();
        avformat_network_init();
        avdevice_register_all();//设备注册
        av_log_set_level(AV_LOG_ERROR);
    }
    

    3. 输出文件修改

    OpenOutput("C:/Users/bian/Desktop/aac.ts"); 
    

    4. 初始化CodecFilter,Codec编码器等,代码太多,不过多描述
    下面附上github源码地址

    做了几个简单的demo程序了,感觉好多方法不知道参数的意义,没有入门理解ffmpeg,后面要了解一引起基本原理了。

    七、Ffmpeg重要数据结构

    本章简单探索一下ffmpeg的一些数据结构,这样在后面的学习过程中可以有自己的思考,更能深入得理解为什么要这样写,有新的需求该如何写。

    重点学习以下几个结构体:

    1. AVFormatContext
    2. AVFrame
    3. AVPacket

    八、搭建简单直播系统

    直播架构图:


    image.png

    其实在讲转码的时候已经学习过了。下面学习一下使用wireshark抓取rtp包来分析。
    首先安装WireShark和npcap。
    打开WireShark,选择本地连接即可。

    设置使用UDP还是TCP传输:

    AVDictionary *options = nullptr;
    //参数设置使用UDP传输
    av_dict_set(&options, "rstp_transport", "udp", 0);
    int ret = avformat_open_input(&inputContext, inputUrl.c_str(), nullptr, &options);
    

    八、音频裁剪
    使用音频裁剪的流程与前面的工程类似,初始化,打开输入流,打开输出流,不同的地方在于把指定要裁剪的数据写入到输出文件。
    理解本项目的关键在于理解AVPacketAVRational的数据结构,尤其是pts和dts的理解,下面简单讲一下这两个数据结构的关键内容。

    AVPacket的dts和pts
    FFmpeg里有两种时间戳:DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)。 顾名思义,前者是解码的时间,后者是显示的时间。要仔细理解这两个概念,需要先了解FFmpeg中的packet和frame的概念。
    FFmpeg中用AVPacket结构体来描述解码前或编码后的压缩包,用AVFrame结构体来描述解码后或编码前的信号帧。 对于视频来说,AVFrame就是视频的一帧图像。这帧图像什么时候显示给用户,就取决于它的PTS。DTS是AVPacket里的一个成员,表示这个压缩包应该什么时候被解码。 如果视频里各帧的编码是按输入顺序(也就是显示顺序)依次进行的,那么解码和显示时间应该是一致的。可事实上,在大多数编解码标准(如H.264或HEVC)中,编码顺序和输入顺序并不一致,于是才会需要PTS和DTS这两种不同的时间戳。
    每个AVPacket的时间戳间隔是固定的,那么这个间隔如何计算呢?答案是denominator/帧率,那时间戳间隔如何用秒或者微秒表示呢?
    在FFMPEG中有三种时间单位:秒、微秒和dts/pts。从dts/pts转化为微秒公式:
    dts* AV_TIME_BASE/ denominator
    其中AV_TIME_BASE为1,000,000,denominator为90,000。
    现在就更好的理解了,denominator其实就是把一秒等分的个数。例如帧率为30,也就是一秒30帧,denominator为90000,一帧其实是90000/30=3000个,即(1秒/90000)*3000。
    denominator是AVRational结构体变量:

    typedef struct AVRational{
        int num; ///< Numerator
        int den; ///< Denominator
    } AVRational;
    

    同时根据上面所述,dts是某一帧的解码时间,pts是显示时间,那么pts肯定要大于dts的,因为要先解码才能播放吧,这一点一定要弄清楚。
    好下,下面上主程序:

    int main()
    {
        //定义裁剪位置,1)按包个数来裁剪,对应时间与帧率有关;2)根据时间戳裁剪
        int startPacketNum = 200;   //开始裁剪位置
        int discardPacketNum = 200; //裁剪的包个数
        int count = 0;
        //记录startPacketNum最后一个包的pts和dts,本例中的视频源pts和dts是不同的
        int64_t lastPacketPts = AV_NOPTS_VALUE;
        int64_t lastPacketDts = AV_NOPTS_VALUE;
        //时间戳间隔,不同的视频源可能不同,我的是3000
        int timerIntervel = 3000;
        int64_t lastPts = AV_NOPTS_VALUE;
        Init();
        //只支持视频流,不能带有音频流
        int ret = OpenInput("gaoxiao-v.mp4");//视频流,注意输入如果带有音频会失败,导致音频与视频不同步
        if (ret >= 0) {
            ret = OpenOutput("gaoxiao-v-caijian.mp4");
        }
        if (ret < 0) goto Error;
        while (true) {
            count++;
            auto packet = ReadPacketFromSource();
            if (packet) {
                if (count <= startPacketNum || count > startPacketNum + discardPacketNum) {
                    if (count >= startPacketNum + discardPacketNum) {
                        //需要调整dts和pts,调整策略和视频源的pts和dts的规律有关,不是固定的
                        packet->dts = -6000 + (count - 1 - discardPacketNum) * timerIntervel;
    
                        if (count % 4 == 0) {
                            packet->pts = packet->dts;
                        }
                        else if (count % 4 == 1) {
                            packet->pts = packet->dts + 3000;
                        }
                        else if (count % 4 == 2) {
                            packet->pts = packet->dts + 15000;
                        }
                        else if (count % 4 == 3) {
                            packet->pts = packet->dts + 6000;
                        }
                    }
    
                    ret = WritePacket(packet);
                }
            }
            else {
                break;
            }
        }
        cout << "cut file end\n" << endl;
    Error:
        CloseInput();
        CloseOutput();
    ...
        return 0;
    }
    

    关于上面代码中不理解的地方可能就是调整dts和pts的策略了,注释中也说明了,这跟视频的dts和pts的策略有关,我为什么这样写呢?下面我打印了原始视频的dts和pts

    pakcet.pts=0,pakcet.dts=-6000  //第一个packet,count=1
    pakcet.pts=12000,pakcet.dts=-3000   //count=2
    pakcet.pts=6000,pakcet.dts=0  //count=3
    pakcet.pts=3000,pakcet.dts=3000  //count=4
    pakcet.pts=9000,pakcet.dts=6000  //count=5
    pakcet.pts=24000,pakcet.dts=9000  //count=6
    pakcet.pts=18000,pakcet.dts=12000  //count=7
    pakcet.pts=15000,pakcet.dts=15000  //count=8
    pakcet.pts=21000,pakcet.dts=18000
    pakcet.pts=36000,pakcet.dts=21000
    pakcet.pts=30000,pakcet.dts=24000
    pakcet.pts=27000,pakcet.dts=27000
    pakcet.pts=33000,pakcet.dts=30000
    pakcet.pts=48000,pakcet.dts=33000
    pakcet.pts=42000,pakcet.dts=36000
    pakcet.pts=39000,pakcet.dts=39000
    pakcet.pts=45000,pakcet.dts=42000
    pakcet.pts=60000,pakcet.dts=45000
    pakcet.pts=54000,pakcet.dts=48000
    pakcet.pts=51000,pakcet.dts=51000
    pakcet.pts=57000,pakcet.dts=54000
    pakcet.pts=72000,pakcet.dts=57000
    pakcet.pts=66000,pakcet.dts=60000
    pakcet.pts=63000,pakcet.dts=63000
    ...
    

    从上面的Log中可以看到dts是非常有规律的,起始为-6000,按等差3000递增。而pts看着似乎有点乱,但是有有一定的规律,就是在特定packet包ptd=dts,这个packet的个数为4的整倍数,而不是4的整倍数的也很容易找出规律,模4余1时,pts=dts+3000,余2时pts=dts+15000,余3时pts=dts+6000。这样就OK了。
    下面附上github源码Demo地址

    相关文章

      网友评论

          本文标题:windows平台ffmpeg学习笔记

          本文链接:https://www.haomeiwen.com/subject/ykmsvctx.html