美文网首页GoCoding
FFmpeg 播放 RTSP/Webcam 流

FFmpeg 播放 RTSP/Webcam 流

作者: GoCodingInMyWay | 来源:发表于2021-08-30 14:21 被阅读0次

    本文将介绍 FFmpeg 如何播放 RTSP/Webcam/File 流。流程如下:

    RTSP/Webcam/File > FFmpeg open and decode to BGR/YUV > OpenCV/OpenGL display
    

    FFmpeg 准备

    git clone https://github.com/ikuokuo/rtsp-wasm-player.git
    cd rtsp-wasm-player
    export MY_ROOT=`pwd`
    
    # ffmpeg: https://ffmpeg.org/
    git clone --depth 1 -b n4.4 https://git.ffmpeg.org/ffmpeg.git $MY_ROOT/3rdparty/source/ffmpeg
    cd $MY_ROOT/3rdparty/source/ffmpeg
    ./configure --prefix=$MY_ROOT/3rdparty/ffmpeg-4.4 \
    --enable-gpl --enable-version3 \
    --disable-programs --disable-doc --disable-everything \
    --enable-decoder=h264 --enable-parser=h264 \
    --enable-decoder=hevc --enable-parser=hevc \
    --enable-hwaccel=h264_nvdec --enable-hwaccel=hevc_nvdec \
    --enable-demuxer=rtsp \
    --enable-demuxer=rawvideo --enable-decoder=rawvideo --enable-indev=v4l2 \
    --enable-protocol=file
    make -j`nproc`
    make install
    ln -s ffmpeg-4.4 $MY_ROOT/3rdparty/ffmpeg
    

    ./configure 手动选择了:解码 h264,hevc 、解封装 rtsp,rawvideo 、及协议 file ,以支持 RTSP/Webcam/File 流。

    其中, Webcam 因于 Linux ,故用的 v4l2。 Windows 可用 dshow, macOS 可用 avfoundation ,详见 Capture/Webcam

    这里依据自己需求进行选择,当然,也可以直接编译全部。

    FFmpeg 拉流

    拉流过程,主要涉及的模块:

    • avdevice: IO 设备支持(次要,为了 Webcam)
    • avformat: 打开流,解封装,拿小包(主要)
    • avcodec: 收包,解码,拿帧(主要)
    • swscale: 图像缩放,转码(次要)

    解封装,拿包

    完整代码,见 stream.cc

    打开输入流:

    // IO 设备注册 for Webcam
    avdevice_register_all();
    // Network 初始化 for RTSP
    avformat_network_init();
    
    // 打开输入流
    format_ctx_ = avformat_alloc_context();
    avformat_open_input(&format_ctx_, "rtsp://", nullptr, nullptr);
    

    找出视频流:

    avformat_find_stream_info(format_ctx_, nullptr);
    
    video_stream_ = nullptr;
    
    for (unsigned int i = 0; i < format_ctx_->nb_streams; i++) {
      auto codec_type = format_ctx_->streams[i]->codecpar->codec_type;
      if (codec_type == AVMEDIA_TYPE_VIDEO) {
        video_stream_ = format_ctx_->streams[i];
        break;
      } else if (codec_type == AVMEDIA_TYPE_AUDIO) {
        // ignore
      }
    }
    

    循环拿包:

    if (packet_ == nullptr) {
      packet_ = av_packet_alloc();
    }
    av_read_frame(format_ctx_, packet_);
    if (packet_->stream_index == video_stream_->GetIndex()) {
      // 如果是视频流,处理其解码、拿帧等
    }
    av_packet_unref(packet_);
    

    解码,拿帧

    完整代码,见 stream_video.cc

    解码初始化:

    if (codec_ctx_ == nullptr) {
      AVCodec *codec_ = avcodec_find_decoder(video_stream_->codecpar->codec_id);
    
      codec_ctx_ = avcodec_alloc_context3(codec_);
    
      avcodec_parameters_to_context(codec_ctx_, stream_->codecpar);
      avcodec_open2(codec_ctx_, codec_, nullptr);
    
      frame_ = av_frame_alloc();  // 帧
    }
    

    解码收包,返帧:

    int ret = avcodec_send_packet(codec_ctx_, packet);
    if (ret != 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) {
      throw StreamError(ret);
    }
    
    ret = avcodec_receive_frame(codec_ctx_, frame_);
    if (ret != 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) {
      throw StreamError(ret);
    }
    
    // frame_ is ok here
    

    注意处理特别返回码: EAGAIN 表示要继续收包、EOF 表示结束,另外还有些特别码。

    缩放,转码

    // 初始化
    if (sws_ctx_ == nullptr) {
      // 设定目标大小及编码
      auto pix_fmt = options_.sws_dst_pix_fmt;
      int width = options_.sws_dst_width;
      int height = options_.sws_dst_height;
      int align = 1;
      int flags = SWS_BICUBIC;
    
      sws_frame_ = av_frame_alloc();
    
      int bytes_n = av_image_get_buffer_size(pix_fmt, width, height, align);
      uint8_t *buffer = static_cast<uint8_t *>(
        av_malloc(bytes_n * sizeof(uint8_t)));
      av_image_fill_arrays(sws_frame_->data, sws_frame_->linesize, buffer,
        pix_fmt, width, height, align);
    
      sws_frame_->width = width;
      sws_frame_->height = height;
    
      // 实例化
      sws_ctx_ = sws_getContext(
          codec_ctx_->width, codec_ctx_->height, codec_ctx_->pix_fmt,
          width, height, pix_fmt, flags, nullptr, nullptr, nullptr);
      if (sws_ctx_ == nullptr) throw StreamError("Get sws context fail");
    }
    
    // 缩放或转码
    sws_scale(sws_ctx_, frame_->data, frame_->linesize, 0, codec_ctx_->height,
      sws_frame_->data, sws_frame_->linesize);
    
    // sws_frame_ as the result frame
    

    OpenCV 显示

    完整代码,见 main_ui_with_opencv.cc

    转码成 bgr24,用于显示:

    cv::namedWindow("ui");
    
    try {
      Stream stream;
      stream.Open(options);
    
      while (1) {
        auto frame = stream.GetFrameVideo();
        if (frame != nullptr) {
          cv::Mat image(frame->height, frame->width, CV_8UC3,
            frame->data[0], frame->linesize[0]);
          cv::imshow(win_name, image);
        }
        char key = static_cast<char>(cv::waitKey(10));
        if (key == 27 || key == 'q' || key == 'Q') {  // ESC/Q
          break;
        }
      }
    
      stream.Close();
    } catch (const StreamError &err) {
      LOG(ERROR) << err.what();
    }
    
    cv::destroyAllWindows();
    

    OpenGL 显示

    完整代码,见 glfw_frame.h, main_ui_with_opengl.cc

    转码成 yuyv420p 用于显示:

    void OnDraw() override {
      if (frame_ != nullptr) {
        auto width = frame_->width;
        auto height = frame_->height;
        auto data = frame_->data[0];
    
        auto len_y = width * height;
        auto len_u = (width >> 1) * (height >> 1);
    
        // yuyv420p 可直接寻址三个平面的数据,赋值进纹理
        texture_y_->Fill(width, height, data);
        texture_u_->Fill(width >> 1, height >> 1, data + len_y);
        texture_v_->Fill(width >> 1, height >> 1, data + len_y + len_u);
      }
    
      glBindVertexArray(vao_);
      glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    }
    

    片段着色器,直接转成 RGB

    #version 330 core
    in vec2 vTexCoord;
    
    uniform sampler2D yTex;
    uniform sampler2D uTex;
    uniform sampler2D vTex;
    
    // yuv420p to rgb888 matrix
    const mat4 YUV2RGB = mat4(
      1.1643828125,             0, 1.59602734375, -.87078515625,
      1.1643828125, -.39176171875,    -.81296875,     .52959375,
      1.1643828125,   2.017234375,             0,  -1.081390625,
                 0,             0,             0,             1
    );
    
    void main() {
      gl_FragColor = vec4(
        texture(yTex, vTexCoord).x,
        texture(uTex, vTexCoord).x,
        texture(vTex, vTexCoord).x,
        1
      ) * YUV2RGB;
    }
    

    结语

    本文代码想要编译运行的话,请依照 README 进行。

    GoCoding 个人实践的经验分享,可关注公众号!

    相关文章

      网友评论

        本文标题:FFmpeg 播放 RTSP/Webcam 流

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