FFmpeg 播放 RTSP/Webcam 流

GoCoding
• 阅读 1403

本文将介绍 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 个人实践的经验分享,可关注公众号!

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
GoCoding GoCoding
2年前
RTSP H264/HEVC 流 Wasm 播放
本文将介绍RTSPH264/HEVC裸流如何于网页前端播放。涉及WebSocket代理发送流数据,Wasm前端解码等。代码:https://github.com/ikuokuo/rtspwasmplayer相关模块:bashRTSPWebSocketProxyRTSP/Webcam/FileFFmpegopenPackets
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
GoCoding
GoCoding
Lv1
Go coding in my way :)
文章
32
粉丝
5
获赞
10