本文將介紹 FFmpeg 如何播放 RTSP/Webcam/File 流。流程如下:
RTSP/Webcam/File > FFmpeg open and decode to BGR/YUV > OpenCV/OpenGL display
- 程式碼: https://github.com/ikuokuo/rtsp-wasm-player, 子模組 rtsp-local-player
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 個人實踐的經驗分享,可關注公眾號!