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

RTSP/Webcam/File > FFmpeg open and decode to BGR/YUV > OpenCV/OpenGL display
  • 代码: https://github.com/ikuokuo/rt..., 子模块 rtsp-local-player

FFmpeg 筹备

git clone https://github.com/ikuokuo/rtsp-wasm-player.gitcd rtsp-wasm-playerexport MY_ROOT=`pwd`# ffmpeg: https://ffmpeg.org/git clone --depth 1 -b n4.4 https://git.ffmpeg.org/ffmpeg.git $MY_ROOT/3rdparty/source/ffmpegcd $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=filemake -j`nproc`make installln -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 Webcamavdevice_register_all();// Network 初始化 for RTSPavformat_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 corein vec2 vTexCoord;uniform sampler2D yTex;uniform sampler2D uTex;uniform sampler2D vTex;// yuv420p to rgb888 matrixconst 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 集体实际的教训分享,可关注公众号!