Bootstrap

ffmpeg实现接收RTSP流并抓拍保存图片的功能

rtsp_capture.h

#ifndef RTSP_CAPTURE_H
#define RTSP_CAPTURE_H

#pragma once
#include <string>
#include <thread>
#include <atomic>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}

class RtspCapture {
 public:
  RtspCapture(const std::string& url);
  ~RtspCapture();

  void take(uint32_t width, uint32_t height);

 private:
  bool initFFmpeg();
  void cleanup();
  bool saveFrame(const std::string& filename, AVFrame* frame, uint32_t width, uint32_t height);

 private:
  std::string m_url;
  std::string m_outputDir;

  AVFormatContext* m_formatContext;
  AVCodecContext* m_codecContext;
  SwsContext* m_swsContext;
  int m_videoStreamIndex;
  std::string m_rtspUrl;
};

#endif // RTSPCAPTURE_H

rtsp_capture.cpp

#include "rtsp_capture.h"
#include <chrono>
#include <iostream>

RtspCapture::RtspCapture(const std::string& url)
    : m_url(url)
      , m_formatContext(nullptr)
      , m_codecContext(nullptr)
      , m_swsContext(nullptr)
      , m_videoStreamIndex(-1)
{
  initFFmpeg();
}

RtspCapture::~RtspCapture() {
  cleanup();
}

bool RtspCapture::initFFmpeg() {

#if LIBAVFORMAT_VERSION_MAJOR < 58
  av_register_all(); // 对于旧版本,显式调用以确保所有编解码器和格式被注册
#endif

  avformat_network_init();

  // 设置RTSP超时选项
  AVDictionary* options = nullptr;
  // av_dict_set(&options, "rtsp_transport", "tcp", 0);
  av_dict_set(&options, "stimeout", "30000000", 0);
  av_dict_set(&options, "max_delay", "500000", 0);  // 设置最大延迟
  av_dict_set(&options, "buffer_size", "1024000", 0);  // 设置缓冲区大小

  // av_log_set_level(AV_LOG_DEBUG);

  // 打开RTSP流
  m_formatContext = avformat_alloc_context();
  if (avformat_open_input(&m_formatContext, m_url.c_str(), nullptr, &options) != 0) {
    std::cerr << "无法打开RTSP流:" << m_url << std::endl;
    return false;
  }

  std::cout << "打开RTSP流成功:" << m_url << std::endl;

  // 获取流信息
  if (avformat_find_stream_info(m_formatContext, nullptr) < 0) {
    std::cerr << "无法获取流信息" << std::endl;
    return false;
  }

  // 查找视频流
  m_videoStreamIndex = -1;
  for (unsigned int i = 0; i < m_formatContext->nb_streams; i++) {
    if (m_formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
      m_videoStreamIndex = i;
      break;
    }
  }

  if (m_videoStreamIndex == -1) {
    std::cerr << "找不到视频流" << std::endl;
    return false;
  }

  // 获取解码器
  const AVCodec* codec = avcodec_find_decoder(m_formatContext->streams[m_videoStreamIndex]->codecpar->codec_id);
  if (!codec) {
    std::cerr << "找不到解码器" << std::endl;
    return false;
  }

  // 创建解码器上下文
  m_codecContext = avcodec_alloc_context3(codec);
  avcodec_parameters_to_context(m_codecContext, m_formatContext->streams[m_videoStreamIndex]->codecpar);

  // 打开解码器
  if (avcodec_open2(m_codecContext, codec, nullptr) < 0) {
    std::cerr << "无法打开解码器" << std::endl;
    return false;
  }

  std::cout << "FFmpeg初始化成功!" << std::endl;

  return true;
}

static uint64_t GetCurTick()
{
  auto now = std::chrono::system_clock::now();
  auto duration = now.time_since_epoch();
  return std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
}

void RtspCapture::take(uint32_t width, uint32_t height) {
  AVPacket* packet = av_packet_alloc();
  AVFrame* frame = av_frame_alloc();
  if (av_read_frame(m_formatContext, packet) >= 0) {
    if (packet->stream_index == m_videoStreamIndex) {
      // 解码视频帧
      if (avcodec_send_packet(m_codecContext, packet) >= 0) {
        if (avcodec_receive_frame(m_codecContext, frame) >= 0) {
          // 生成文件名
          std::string filename = "img.jpg";
          // std::string filename = "snapshot_" +
          //                        std::to_string(GetCurTick()) + ".jpg";

          // 保存帧为JPEG
          saveFrame(filename, frame, width, height);
        }
      }
    }
    av_packet_unref(packet);
  }

  av_frame_free(&frame);
  av_packet_free(&packet);

}

bool RtspCapture::saveFrame(const std::string& filename,  AVFrame* frame, uint32_t width, uint32_t height) {

  // 获取JPEG编码器
  const AVCodec* jpegCodec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
  if (!jpegCodec) {
    std::cerr << "无法找到JPEG编码器" << std::endl;
    return false;
  }

  // 创建编码器上下文
  AVCodecContext* jpegContext = avcodec_alloc_context3(jpegCodec);
  if (!jpegContext) {
    std::cerr << "无法创建JPEG编码器上下文" << std::endl;
    return false;
  }

  // 设置编码器参数
  jpegContext->width = width;
  jpegContext->height = height;
  jpegContext->time_base = (AVRational){1, 1};
  jpegContext->pix_fmt = AV_PIX_FMT_YUVJ420P;  // JPEG使用的颜色空间
  jpegContext->codec_type = AVMEDIA_TYPE_VIDEO;
  jpegContext->codec_id = AV_CODEC_ID_MJPEG;

  // 打开编码器
  if (avcodec_open2(jpegContext, jpegCodec, nullptr) < 0) {
    std::cerr << "无法打开JPEG编码器" << std::endl;
    avcodec_free_context(&jpegContext);
    return false;
  }

  // 分配转换后的帧
  AVFrame* yuvFrame = av_frame_alloc();
  yuvFrame->format = AV_PIX_FMT_YUVJ420P;
  yuvFrame->width = m_codecContext->width;
  yuvFrame->height = m_codecContext->height;
  av_frame_get_buffer(yuvFrame, 0);

  // 创建缩放上下文
  SwsContext* swsContext = sws_getContext(
      m_codecContext->width, m_codecContext->height, m_codecContext->pix_fmt,
      m_codecContext->width, m_codecContext->height, AV_PIX_FMT_YUVJ420P,
      SWS_BILINEAR, nullptr, nullptr, nullptr
      );

  if (!swsContext) {
    std::cerr << "无法创建转换上下文" << std::endl;
    av_frame_free(&yuvFrame);
    avcodec_free_context(&jpegContext);
    return false;
  }

  // 转换颜色空间
  sws_scale(swsContext, frame->data, frame->linesize,
            0, m_codecContext->height, yuvFrame->data, yuvFrame->linesize);

  // 编码为JPEG
  AVPacket* jpegPacket = av_packet_alloc();
  int ret = avcodec_send_frame(jpegContext, yuvFrame);
  if (ret < 0) {
    std::cerr << "发送帧到编码器失败" << std::endl;
    av_packet_free(&jpegPacket);
    av_frame_free(&yuvFrame);
    avcodec_free_context(&jpegContext);
    sws_freeContext(swsContext);
    return false;
  }

  ret = avcodec_receive_packet(jpegContext, jpegPacket);
  if (ret < 0) {
    std::cerr << "从编码器接收数据失败" << std::endl;
    av_packet_free(&jpegPacket);
    av_frame_free(&yuvFrame);
    avcodec_free_context(&jpegContext);
    sws_freeContext(swsContext);
    return false;
  }

  // 写入文件
  FILE* outFile = fopen(filename.c_str(), "wb");
  if (outFile) {
    fwrite(jpegPacket->data, 1, jpegPacket->size, outFile);
    fclose(outFile);
    // std::cout << "已保存图片: " << filename << std::endl;
  } else {
    std::cerr << "无法创建输出文件" << std::endl;
  }

  return true;
}

void RtspCapture::cleanup() {
  if (m_codecContext) {
    avcodec_free_context(&m_codecContext);
  }
  if (m_formatContext) {
    avformat_close_input(&m_formatContext);
    avformat_free_context(m_formatContext);
  }
}

main.cpp

#include "rtsp_capture.h"

int main()
{
    std::unique_ptr<RtspCapture> rtsp_capture_ptr_;
    std::string url = "rtsp://admin:[email protected]:554/live1";
    rtsp_capture_ptr_ = std::make_unique<RtspCapture>(url);
    rtsp_capture_ptr_->take(640, 480);

    getchar();
    return 0;
}
;