FFmpeg - 将网络流保存到文件

1. 开发环境

● FFmpeg版本:7.1

● 开发环境:Ubuntu20.04

2. 基本流程

  • 网络包 -> 解复用/解封装 -> PES裸流 -> 再复用/封装 -> 保存到文件/转为网络流

3. 命令行实现网络流保存

  • ZL_MediaKit流媒体服务器地址: 192.168.16.230

  • 启动一个FFmpeg进程,向流媒体服务器推RTSP

    # TCP方式推RTSP流
    $ ffmpeg -re -stream_loop -1 -i ./clock.mp4 -vcodec h264 -acodec aac -f rtsp -rtsp_transport tcp rtsp://192.168.16.230/live/test
    
    # UDP方式推RTSP流,这也是RTSP的默认方式
    $ ffmpeg -re -stream_loop -1 -i ./clock.mp4 -vcodec h264 -acodec aac -f rtsp rtsp://192.168.16.230/live/test
    
    # -re: 实时速率读取输入,按照输入文件的原始帧率进行输出
    # -stream_loop -1: -1 表示无限循环,如果你想循环特定次数,可以用正整数替换 -1
    # -i ./clock.mp4: 指定输入文件为一个本地文件clock.mp4
    # -vcodec h264: 设置视频编码器为H.264,如果输入文件已经是H.264编码了,并不会重新编码
    # -acodec aac: 设置音频编码器为 AAC,同样,如果输入文件已经是 AAC 编码,不会重新编码。
    # -f rtsp: 指定输出格式为 RTSP 协议
    # -rtsp_transport tcp: 指定 RTSP 传输使用 TCP 协议,而不是默认的 UDP。TCP 通常更可靠,特别是在网络不稳定的情况下
    # rtsp://192.168.16.230/live/test: 这是输出的 RTSP 流地址。在这个例子中,流将被推送到 IP 地址为 192.168.16.230 的流媒体服务器,路径为 /live/test
    
  • 另启一个FFmpeg进程,从流媒体服务器拉取RTSP流,并保存为MP4格式

    $ ffmpeg -i rtsp://192.168.16.230/live/test -c copy output.mp4
    
  • 如下图:

  • ffplay播放保存的output.mp4

    $ ffplay output.mp4
    

4. FFmpeg代码

4.1 包含FFmpeg头文件

  • 建议不要一次引入FFmpeg所有的头文件,用到哪个模块就包含响应的头文件;这样一来可以帮助我们了解ffmpeg中各个模块功能划分,二来编译出的文件也没有那么大
  • 本例程中包含两个头文件即可
    extern "C"{
    #include "libavutil/mem.h"
    #include "libavformat/avformat.h"
    }
    

4.2 初始化FFmpeg

  • 本例中需要拉取网络流,因此需要初始化FFmpeg的网络组件
  • 设置FFmpeg内部日志等级
    // 初始化ffmpeg
    int Test::ffmpeg_init(void)
    {
        // 初始化FFmpeg的网络组件
        avformat_network_init();
        // 设置FFmpeg内部日志等级
        av_log_set_level(AV_LOG_INFO);
        
        return 0;
    }
    

4.3 打开输入源

  • 本例程中的输入源是一个网络流,可以是RTSP/RTMP...
    int Test::open_input(std::string iurl)
    {
        // 错误日志
        char errMsg[1024] = {0};
        
        // 输入格式上下文
        av_inputCtx = avformat_alloc_context();
        // 打开输入源
        int ret = avformat_open_input(&av_inputCtx, iurl.c_str(), nullptr, nullptr);
        if(ret < 0)	{
            av_strerror(ret, errMsg, sizeof(errMsg));
            fprintf(stderr, "%s errMsg:%s\n", __func__, errMsg);
            return -1;
        }
        
        // 从输入源中查找流
        ret = avformat_find_stream_info(av_inputCtx, nullptr);
        if(ret < 0)
        {
            // 关闭输入源
            avformat_close_input(&av_inputCtx);
            // 释放输入上下文
            avformat_free_context(av_inputCtx);
    
            av_strerror(ret, errMsg, sizeof(errMsg));
            fprintf(stderr, "%s errMsg:%s\n", __func__, errMsg);
            return -2;
        }
        
        fprintf(stdout, "%s open %s success!", __func__, iurl);
        
        return 0;
    }
    

4.4 创建输出流

  • 本例程中的将输出流保存到文件中,输出流就是一个文件
    // 创建输出流
    int Test::open_output(std::string ourl)
    {
        // 错误日志
        char errMsg[1024] = {0};
        
        // 输出上下文, ts流
        int ret = avformat_alloc_output_context2(&av_outputCtx, nullptr, "mpegts", ourl.c_str());
        if(ret < 0){
            av_strerror(ret, errMsg, sizeof(errMsg));
            fprintf(stderr, "%s avformat_alloc_output_context2 failed! errMsg:%s\n", __func__, errMsg);
            return -1;
        }
        
        // 创建输出流,写入文件调用avio_open2
        ret = avio_open2(&av_outputCtx->pb, ourl.c_str(), AVIO_FLAG_READ_WRITE, nullptr, nullptr);
            if(ret < 0){
            av_strerror(ret, errMsg, sizeof(errMsg));
            fprintf(stderr, "%s avio_open2 failed! errMsg:%s\n", __func__, errMsg);
            goto ERR1;
        }
        
        // 遍历输入流,拷贝输入流的编码参数到输出流
        for(int i = 0; i < av_inputCtx->nb_streams; i++){
        
            // 创建输出流
            AVStream *out_stream = avformat_new_stream(av_outputCtx, nullptr);
            // 拷贝输入流的编码参数到输出流
            ret = avcodec_parameters_copy(out_stream->codecpar, av_inputCtx->streams[i]->codecpar);
            if(ret < 0){
                av_strerror(ret, errMsg, sizeof(errMsg));
                fprintf(stderr, "%s avcodec_parameters_copy failed! errMsg:%s\n", __func__, errMsg);
                goto ERR2;
            }
        
        }
        
        // 初始化媒体文件的头部信息
        ret = avformat_write_header(av_outputCtx, nullptr);
        if(ret < 0){
            av_strerror(ret, errMsg, sizeof(errMsg));
            fprintf(stderr, "%s avformat_write_header failed! errMsg:%s\n", __func__, errMsg);
            goto ERR2;
        }
        
        fprintf(stdout, "%s open %s success!", __func__, ourl.c_str());
        
        return 0;
        
    ERR2:
        if(av_outputCtx){
            avio_closep(&av_outputCtx->pb);
        }
        
    ERR1:
        avformat_free_context(av_outputCtx);
        
        return ret;
    }
    

4.5 从输入流中读取一个包

  • 从输入流中读取一个包
    std::shared_ptr<AVPacket> Test::read_packet_from_source()
    {
      // av packet, std::shared_ptr
      std::shared_ptr<AVPacket> packet(av_packet_alloc(), [](AVPacket *p){av_packet_free(&p);});
      if(!packet){
        fprintf(stderr, "%s av_packet_alloc failed!", __func__);
        return nullptr;
      }
    
      // 从输入流中获取一个编码后的包 PES包
      int ret = av_read_frame(av_inputCtx, packet.get());
      if(ret < 0){
        av_strerror(ret, errMsg, sizeof(errMsg));
        fprintf(stderr, "%s avformat_write_header failed! errMsg:%s\n", __func__, errMsg);
        return nullptr;
      }
    	
      return packet;
    }
    

4.6 向输出流中写入一个包

  • 向输出流中写入一个包
    // 向输出流中写包
    int Test::write_packet_to_target(std::shared_ptr<AVPacket> packet)
    {
      auto inputStream = av_inputCtx->streams[packet->stream_index];
      auto outputStream = av_outputCtx->streams[packet->stream_index];
    
    	// 时间基转换
    	av_packet_rescale_ts(packet.get(), inputStream->time_base, outputStream->time_base);
    	return av_interleaved_write_frame(av_outputCtx, packet.get());
    }
    

4.7 拉取RTSP流,并保存到文件

  • 拉取RTSP流,并保存到文件
// 获取RTSP网络流,保存到文件;也可以获取其它协议的网络流,如RTMP,SRT
int Test::rtsp_save_to_file(void)
{
  // 初始化
  ffmpeg_init();
  // 打开输入流
  int ret = open_input(std::string("rtsp://192.168.16.230/live/test"));
  if(ret < 0){
    return -1;
  }

  // 打开输出流
  ret = open_output(std::string("/tmp/test.ts"));

  // 循环从输入流中读包,写入到输出流中
  while(true){
    auto packet = read_packet_from_source();
    if(packet){
      write_packet_to_target(packet);
      fprintf(stdout, "%s writePacket success", __func__);
    }else{
      fprintf(stderr, "%s writePacket failed!\n", __func__);
      return -1;
    }
  }

  return 0;
}

4.8 释放资源

  • 释放资源
    // 释放资源
    void Test::release(void)
    {
      // 关闭输入源
      avformat_close_input(&av_inputCtx);
      // 释放输入上下文
    
      if(av_inputCtx)
        avformat_free_context(av_inputCtx);
    
      // 关闭输出源
      if(av_outputCtx->pb)
        avio_close(av_outputCtx->pb);
      // 释放输出格式上下文
      if(av_outputCtx)
        avformat_free_context(av_outputCtx);
    
      // 释放ffmpeg网络资源
      avformat_network_deinit();
    }
    
posted @ 2024-12-24 19:36  zhijun  阅读(5)  评论(0编辑  收藏  举报