本文将介绍 FFmpeg 如何播放 RTSP/Webcam/File 流。流程如下:
1RTSP/Webcam/File > FFmpeg open and decode to BGR/YUV > OpenCV/OpenGL display
- 代码: https://github.com/ikuokuo/rtsp-wasm-player, 子模块 rtsp-local-player
FFmpeg 准备
1git clone https://github.com/ikuokuo/rtsp-wasm-player.git 2cd rtsp-wasm-player 3export MY_ROOT=`pwd` 4 5# ffmpeg: https://ffmpeg.org/ 6git clone --depth 1 -b n4.4 https://git.ffmpeg.org/ffmpeg.git $MY_ROOT/3rdparty/source/ffmpeg 7cd $MY_ROOT/3rdparty/source/ffmpeg 8./configure --prefix=$MY_ROOT/3rdparty/ffmpeg-4.4 \ 9--enable-gpl --enable-version3 \ 10--disable-programs --disable-doc --disable-everything \ 11--enable-decoder=h264 --enable-parser=h264 \ 12--enable-decoder=hevc --enable-parser=hevc \ 13--enable-hwaccel=h264_nvdec --enable-hwaccel=hevc_nvdec \ 14--enable-demuxer=rtsp \ 15--enable-demuxer=rawvideo --enable-decoder=rawvideo --enable-indev=v4l2 \ 16--enable-protocol=file 17make -j`nproc` 18make install 19ln -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 。
打开输入流:
1// IO 设备注册 for Webcam 2avdevice_register_all(); 3// Network 初始化 for RTSP 4avformat_network_init(); 5 6// 打开输入流 7format_ctx_ = avformat_alloc_context(); 8avformat_open_input(&format_ctx_, "rtsp://", nullptr, nullptr);
找出视频流:
1avformat_find_stream_info(format_ctx_, nullptr); 2 3video_stream_ = nullptr; 4 5for (unsigned int i = 0; i < format_ctx_->nb_streams; i++) { 6 auto codec_type = format_ctx_->streams[i]->codecpar->codec_type; 7 if (codec_type == AVMEDIA_TYPE_VIDEO) { 8 video_stream_ = format_ctx_->streams[i]; 9 break; 10 } else if (codec_type == AVMEDIA_TYPE_AUDIO) { 11 // ignore 12 } 13}
循环拿包:
1if (packet_ == nullptr) { 2 packet_ = av_packet_alloc(); 3} 4av_read_frame(format_ctx_, packet_); 5if (packet_->stream_index == video_stream_->GetIndex()) { 6 // 如果是视频流,处理其解码、拿帧等 7} 8av_packet_unref(packet_);
解码,拿帧
完整代码,见 stream_video.cc 。
解码初始化:
1if (codec_ctx_ == nullptr) { 2 AVCodec *codec_ = avcodec_find_decoder(video_stream_->codecpar->codec_id); 3 4 codec_ctx_ = avcodec_alloc_context3(codec_); 5 6 avcodec_parameters_to_context(codec_ctx_, stream_->codecpar); 7 avcodec_open2(codec_ctx_, codec_, nullptr); 8 9 frame_ = av_frame_alloc(); // 帧 10}
解码收包,返帧:
1int ret = avcodec_send_packet(codec_ctx_, packet); 2if (ret != 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { 3 throw StreamError(ret); 4} 5 6ret = avcodec_receive_frame(codec_ctx_, frame_); 7if (ret != 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { 8 throw StreamError(ret); 9} 10 11// frame_ is ok here
注意处理特别返回码:
EAGAIN表示要继续收包、EOF表示结束,另外还有些特别码。
缩放,转码
1// 初始化 2if (sws_ctx_ == nullptr) { 3 // 设定目标大小及编码 4 auto pix_fmt = options_.sws_dst_pix_fmt; 5 int width = options_.sws_dst_width; 6 int height = options_.sws_dst_height; 7 int align = 1; 8 int flags = SWS_BICUBIC; 9 10 sws_frame_ = av_frame_alloc(); 11 12 int bytes_n = av_image_get_buffer_size(pix_fmt, width, height, align); 13 uint8_t *buffer = static_cast<uint8_t *>( 14 av_malloc(bytes_n * sizeof(uint8_t))); 15 av_image_fill_arrays(sws_frame_->data, sws_frame_->linesize, buffer, 16 pix_fmt, width, height, align); 17 18 sws_frame_->width = width; 19 sws_frame_->height = height; 20 21 // 实例化 22 sws_ctx_ = sws_getContext( 23 codec_ctx_->width, codec_ctx_->height, codec_ctx_->pix_fmt, 24 width, height, pix_fmt, flags, nullptr, nullptr, nullptr); 25 if (sws_ctx_ == nullptr) throw StreamError("Get sws context fail"); 26} 27 28// 缩放或转码 29sws_scale(sws_ctx_, frame_->data, frame_->linesize, 0, codec_ctx_->height, 30 sws_frame_->data, sws_frame_->linesize); 31 32// sws_frame_ as the result frame
OpenCV 显示
完整代码,见 main_ui_with_opencv.cc 。
转码成 bgr24,用于显示:
1cv::namedWindow("ui"); 2 3try { 4 Stream stream; 5 stream.Open(options); 6 7 while (1) { 8 auto frame = stream.GetFrameVideo(); 9 if (frame != nullptr) { 10 cv::Mat image(frame->height, frame->width, CV_8UC3, 11 frame->data[0], frame->linesize[0]); 12 cv::imshow(win_name, image); 13 } 14 char key = static_cast<char>(cv::waitKey(10)); 15 if (key == 27 || key == 'q' || key == 'Q') { // ESC/Q 16 break; 17 } 18 } 19 20 stream.Close(); 21} catch (const StreamError &err) { 22 LOG(ERROR) << err.what(); 23} 24 25cv::destroyAllWindows();
OpenGL 显示
完整代码,见 glfw_frame.h, main_ui_with_opengl.cc 。
转码成 yuyv420p 用于显示:
1void OnDraw() override { 2 if (frame_ != nullptr) { 3 auto width = frame_->width; 4 auto height = frame_->height; 5 auto data = frame_->data[0]; 6 7 auto len_y = width * height; 8 auto len_u = (width >> 1) * (height >> 1); 9 10 // yuyv420p 可直接寻址三个平面的数据,赋值进纹理 11 texture_y_->Fill(width, height, data); 12 texture_u_->Fill(width >> 1, height >> 1, data + len_y); 13 texture_v_->Fill(width >> 1, height >> 1, data + len_y + len_u); 14 } 15 16 glBindVertexArray(vao_); 17 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); 18}
片段着色器,直接转成 RGB:
1#version 330 core 2in vec2 vTexCoord; 3 4uniform sampler2D yTex; 5uniform sampler2D uTex; 6uniform sampler2D vTex; 7 8// yuv420p to rgb888 matrix 9const mat4 YUV2RGB = mat4( 10 1.1643828125, 0, 1.59602734375, -.87078515625, 11 1.1643828125, -.39176171875, -.81296875, .52959375, 12 1.1643828125, 2.017234375, 0, -1.081390625, 13 0, 0, 0, 1 14); 15 16void main() { 17 gl_FragColor = vec4( 18 texture(yTex, vTexCoord).x, 19 texture(uTex, vTexCoord).x, 20 texture(vTex, vTexCoord).x, 21 1 22 ) * YUV2RGB; 23}
结语
本文代码想要编译运行的话,请依照 README 进行。
GoCoding 个人实践的经验分享,可关注公众号!
