实验平台:Ubuntu20.04
摄像头:普通USB摄像头,输出格式为YUV422
1.配置RTMP服务器推流平台
使用Nginx 配置1935端口即可,贴上教程地址
ubuntu20.04搭建Nginx+rtmp服务器)
2.配置FFmpeg开发环境
过程较为简单,这里不再赘述,可以看博主的往期博客,贴上教程地址:配置FFmpeg开发环境 (Vscode+CMake
3.推流具体实现流程
总体流程图
3.1 设备初始化
有些摄像头可能支持输出多种参数,因此一定要检查摄像头支持的格式
v4l2-ctl: 一个命令行工具,用于控制和调试V4L2设备。可以查询设备信息、设置参数、捕获视频帧等。
v4l2-ctl --list-formats-ext # 列出设备支持的所有格式
v4l2-ctl --set-fmt-video=width=1920,height=1080,pixelformat=H264 # 设置视频格式
v4l2-ctl --stream-mmap --stream-count=100 --stream-to=output.raw # 捕获视频流
查看本次实验的摄像头的相关参数
marxist@ubuntu:~/Desktop/audio_test/build$ v4l2-ctl --list-formats-ext
ioctl: VIDIOC_ENUM_FMT
Type: Video Capture
[0]: 'MJPG' (Motion-JPEG, compressed)
Size: Discrete 1920x1080
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 640x480
Interval: Discrete 0.008s (120.101 fps)
Interval: Discrete 0.011s (90.000 fps)
Interval: Discrete 0.017s (60.500 fps)
Interval: Discrete 0.033s (30.200 fps)
Size: Discrete 1280x720
Interval: Discrete 0.017s (60.000 fps)
Interval: Discrete 0.033s (30.500 fps)
Size: Discrete 1024x768
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 800x600
Interval: Discrete 0.017s (60.000 fps)
Size: Discrete 1280x1024
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 320x240
Interval: Discrete 0.008s (120.101 fps)
[1]: 'YUYV' (YUYV 4:2:2)
Size: Discrete 1920x1080
Interval: Discrete 0.167s (6.000 fps)
Size: Discrete 640x480
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 1280x720
Interval: Discrete 0.111s (9.000 fps)
Size: Discrete 1024x768
Interval: Discrete 0.167s (6.000 fps)
Size: Discrete 800x600
Interval: Discrete 0.050s (20.000 fps)
Size: Discrete 1280x1024
Interval: Discrete 0.167s (6.000 fps)
Size: Discrete 320x240
Interval: Discrete 0.033s (30.000 fps)
由上述可知,摄像头一共支持两种格式,一是MJPG格式,已经由硬件压缩好的一种格式,一种就是常见的YUV422格式,YUV同样支持多种分辨率格式。
设置输入格式上下文,Linux系统对应的是V4L2,查找视频流信息
AVInputFormat *input_format = av_find_input_format("v4l2");
if ((ret = avformat_open_input(&input_ctx, "/dev/video0", input_format, &options)) < 0)
{
fprintf(stderr, "Could not open input\n");
return ret;
}
// 查找流信息
ret = avformat_find_stream_info(input_ctx, NULL);
if (ret < 0)
{
std::cerr << "could not find stream info" << std::endl;
return -1;
}
// 查找视频流
for (size_t i = 0; i < input_ctx->nb_streams; i++)
{
if (input_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
video_stream_index = i;
break;
}
}
if (video_stream_index == -1)
{
std::cerr << "no video stream found" << std::endl;
return -1;
}
3.2 初始化编码器
本次推流实验使用的是H264编码器,CPU软编码,没有使用到硬件编码,用到的库是X264。
主要流程为 查找编码器——分配编码器上下文——设置编码器参数——打开编码器
// 查找编码器
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec)
{
fprintf(stderr, "Could not find AV_CODEC_ID_H264\n");
return -1;
}
// 分配编码器上下文
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx)
{
std::cerr << "Could not allocate video codec context" << std::endl;
return -1;
}
// 设置编码器参数
codec_ctx->codec_id = codec->id;
codec_ctx->bit_rate = 400000;
codec_ctx->width = 1280;
codec_ctx->height = 720;
codec_ctx->time_base = (AVRational){1, 9};
codec_ctx->framerate = (AVRational){9, 1};
codec_ctx->gop_size = 10;
codec_ctx->max_b_frames = 0; // 不需要B帧
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; // 传入的420P 格式,而cam 默认输出422 则一会 需要作转换
// 打开编码器
if (avcodec_open2(codec_ctx, codec, NULL) < 0)
{
fprintf(stderr, "Could not open codec\n");
return -1;
}
这里将B帧参数设置为了0,因为加入B帧之后,虽然提高了压缩效率,但是也显著增加了编码的复杂性。编码B帧需要更多的计算资源,因为它不仅需要前向预测,还需要后向预测。对于资源受限的设备(如移动设备、嵌入式系统等),不使用B帧可以减少编码器的负担。
3.3 设置输出流
这里的输出流地址则特指的RTMP服务器地址,也就是说FFmpeg将编码好的数据传输到RTMP服务器。如果需要写入到文件,输出流地址也可以是文件路径。
相关代码操作
// 创建输出流
AVStream *out_stream = avformat_new_stream(output_ctx, codec);
if (!out_stream)
{
fprintf(stderr, "Could not avformat_new_stream\n");
return -1;
}
// 从输入流复制参数到输出流
avcodec_parameters_from_context(out_stream->codecpar, codec_ctx);
out_stream->time_base = codec_ctx->time_base;
// 打开输出URL
if (!(output_ctx->oformat->flags & AVFMT_NOFILE))
{
if (avio_open(&output_ctx->pb, output_url, AVIO_FLAG_WRITE) < 0)
{
std::cerr << "Could not open output URL" << std::endl;
return -1;
}
}
// 写输出文件头
if (avformat_write_header(output_ctx, NULL) < 0)
{
fprintf(stderr, "Could not write header\n");
return -1;
}
3.4 读取摄像头数据
av_read_frame(input_ctx, &pkt)
代码作用是从输入设备读取数据帧,封装到packet中。
根据上文已经获取到了视频流索引,在此判断一下是不是视频流,因为有些摄像头支持语音输入,packet中存放的也可能是音频流
pkt.stream_index == video_stream_index
3.5 颜色空间转换
在上述过程中, 已经指定输出YUV422的数据了因此需要转换为YUV420数据
大体流程为:原始帧—转换上下文—YUV420帧
首先初始化转换上下文
// 准备颜色空间色彩转换
SwsContext *sws_ctx = sws_getContext(
codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUYV422,
codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL);
if (!sws_ctx)
{
std::cerr << "Could not initialize the conversion context" << std::endl;
return -1;
}
分辨率与编码器参数保持一致
准备原始数据帧,从摄像头读取的数据包中得到
AVFrame *temp_frame = av_frame_alloc();
if (!temp_frame)
{
std::cerr << "Could not allocate temporary frame" << std::endl;
av_packet_unref(&pkt);
continue;
}
// 分配临时帧的内存空间
if (av_image_alloc(temp_frame->data, temp_frame->linesize, codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUYV422, 1) < 0)
{
std::cerr << "Could not allocate temporary frame buffer" << std::endl;
av_frame_free(&temp_frame);
av_packet_unref(&pkt);
continue;
}
// 将pkt.data中的数据填充到temp_frame
ret = av_image_fill_arrays(temp_frame->data, temp_frame->linesize, pkt.data, AV_PIX_FMT_YUYV422, codec_ctx->width, codec_ctx->height, 1);
if (ret < 0)
{
std::cerr << "Error filling arrays" << std::endl;
av_freep(&temp_frame->data[0]);
av_frame_free(&temp_frame);
av_packet_unref(&pkt);
continue;
}
初始化YUV420的帧
// 分配AVFrame并设置参数
AVFrame *frame = av_frame_alloc();
if (!frame)
{
std::cerr << "Could not allocate video frame" << std::endl;
return -1;
}
frame->format = codec_ctx->pix_fmt;
frame->width = codec_ctx->width;
frame->height = codec_ctx->height;
av_frame_get_buffer(frame, 32);
最后执行转换即可
sws_scale(sws_ctx, temp_frame->data, temp_frame->linesize, 0, codec_ctx->height, frame->data, frame->linesize);
3.6 编码并输出到RTMP服务器
得到YUV420的数据,就可以进行最后的操作了
// 编码视频数据
ret = avcodec_send_frame(codec_ctx, frame);
if (ret < 0)
{
std::cerr << "Error sending frame to encoder" << std::endl;
break;
}
while (ret >= 0)
{
ret = avcodec_receive_packet(codec_ctx, &pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
break;
}
else if (ret < 0)
{
std::cerr << "Error encoding frame" << std::endl;
break;
}
// 将编码后的视频数据推送到RTMP服务器
pkt.stream_index = out_stream->index;
av_packet_rescale_ts(&pkt, codec_ctx->time_base, out_stream->time_base);
pkt.pos = -1;
ret = av_interleaved_write_frame(output_ctx, &pkt);
if (ret < 0)
{
std::cerr << "Error writing frame" << std::endl;
break;
}
av_packet_unref(&pkt);
}
4.完整代码
extern "C"
{
#include <libavformat/avformat.h>
#include <libavdevice/avdevice.h>
#include <libavutil/opt.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include "libavutil/imgutils.h"
}
#include <iostream>
#include <cstdlib>
using namespace std;
int main(int argc, char *argv[])
{
const char *output_url = "rtmp://192.168.1.79:1935/orin/live"; // 替换为你的RTMP推流地址
AVFormatContext *input_ctx = NULL;
AVPacket pkt;
int ret;
int video_stream_index = -1;
AVDictionary *options = nullptr; // 摄像头相关参数
int64_t pts = 0; // 初始化 PTS
// 初始化libavformat和注册所有muxers, demuxers和协议
avdevice_register_all();
avformat_network_init();
// 打开摄像头开始
// // 摄像头支持多种参数,因此使用option 指定参数 最大支持到9帧
av_dict_set(&options, "video_size", "1280*720", 0);
av_dict_set(&options, "framerate", "9", 0);
av_dict_set(&options, "input_format", "yuyv422", 0);
AVInputFormat *input_format = av_find_input_format("v4l2");
if ((ret = avformat_open_input(&input_ctx, "/dev/video0", input_format, &options)) < 0)
{
fprintf(stderr, "Could not open input\n");
return ret;
}
// 查找流信息
ret = avformat_find_stream_info(input_ctx, NULL);
if (ret < 0)
{
std::cerr << "could not find stream info" << std::endl;
return -1;
}
// 查找视频流
for (size_t i = 0; i < input_ctx->nb_streams; i++)
{
if (input_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
video_stream_index = i;
break;
}
}
if (video_stream_index == -1)
{
std::cerr << "no video stream found" << std::endl;
return -1;
}
// 查找编码器
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec)
{
fprintf(stderr, "Could not find AV_CODEC_ID_H264\n");
return -1;
}
// 分配编码器上下文
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx)
{
std::cerr << "Could not allocate video codec context" << std::endl;
return -1;
}
// 设置编码器参数
codec_ctx->codec_id = codec->id;
codec_ctx->bit_rate = 400000;
codec_ctx->width = 1280;
codec_ctx->height = 720;
codec_ctx->time_base = (AVRational){1, 9};
codec_ctx->framerate = (AVRational){9, 1};
codec_ctx->gop_size = 10;
codec_ctx->max_b_frames = 0; // 不需要B帧
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; // 传入的420P 格式,而cam 默认输出422 则一会 需要作转换
// 打开编码器
if (avcodec_open2(codec_ctx, codec, NULL) < 0)
{
fprintf(stderr, "Could not open codec\n");
return -1;
}
// 分配输出上下文
AVFormatContext *output_ctx = nullptr;
ret = avformat_alloc_output_context2(&output_ctx, NULL, "flv", output_url);
if (!output_ctx)
{
fprintf(stderr, "Could not create output context\n");
return ret;
}
// 创建输出流
AVStream *out_stream = avformat_new_stream(output_ctx, codec);
if (!out_stream)
{
fprintf(stderr, "Could not avformat_new_stream\n");
return -1;
}
// 从输入流复制参数到输出流
avcodec_parameters_from_context(out_stream->codecpar, codec_ctx);
out_stream->time_base = codec_ctx->time_base;
// 打开输出URL
if (!(output_ctx->oformat->flags & AVFMT_NOFILE))
{
if (avio_open(&output_ctx->pb, output_url, AVIO_FLAG_WRITE) < 0)
{
std::cerr << "Could not open output URL" << std::endl;
return -1;
}
}
// 写输出文件头
if (avformat_write_header(output_ctx, NULL) < 0)
{
fprintf(stderr, "Could not write header\n");
return -1;
}
// 分配AVFrame并设置参数
AVFrame *frame = av_frame_alloc();
if (!frame)
{
std::cerr << "Could not allocate video frame" << std::endl;
return -1;
}
frame->format = codec_ctx->pix_fmt;
frame->width = codec_ctx->width;
frame->height = codec_ctx->height;
av_frame_get_buffer(frame, 32);
// 准备颜色空间色彩转换
SwsContext *sws_ctx = sws_getContext(
codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUYV422,
codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL);
if (!sws_ctx)
{
std::cerr << "Could not initialize the conversion context" << std::endl;
return -1;
}
while (true)
{
if (av_read_frame(input_ctx, &pkt) >= 0)
{
if (pkt.stream_index == video_stream_index)
{
// 从相机出来的原始帧 为YUV 422 需要转换为420P
// 数据是YUYV422格式,需要转换为YUV420P
AVFrame *temp_frame = av_frame_alloc();
if (!temp_frame)
{
std::cerr << "Could not allocate temporary frame" << std::endl;
av_packet_unref(&pkt);
continue;
}
// 分配临时帧的内存空间
if (av_image_alloc(temp_frame->data, temp_frame->linesize, codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUYV422, 1) < 0)
{
std::cerr << "Could not allocate temporary frame buffer" << std::endl;
av_frame_free(&temp_frame);
av_packet_unref(&pkt);
continue;
}
// 将pkt.data中的数据填充到temp_frame
ret = av_image_fill_arrays(temp_frame->data, temp_frame->linesize, pkt.data, AV_PIX_FMT_YUYV422, codec_ctx->width, codec_ctx->height, 1);
if (ret < 0)
{
std::cerr << "Error filling arrays" << std::endl;
av_freep(&temp_frame->data[0]);
av_frame_free(&temp_frame);
av_packet_unref(&pkt);
continue;
}
// 转换颜色空间到YUV420P
sws_scale(sws_ctx, temp_frame->data, temp_frame->linesize, 0, codec_ctx->height, frame->data, frame->linesize);
// 设置帧的 PTS
frame->pts = pts++;
// 编码视频数据
ret = avcodec_send_frame(codec_ctx, frame);
if (ret < 0)
{
std::cerr << "Error sending frame to encoder" << std::endl;
break;
}
while (ret >= 0)
{
ret = avcodec_receive_packet(codec_ctx, &pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
break;
}
else if (ret < 0)
{
std::cerr << "Error encoding frame" << std::endl;
break;
}
// 将编码后的视频数据推送到RTMP服务器
pkt.stream_index = out_stream->index;
av_packet_rescale_ts(&pkt, codec_ctx->time_base, out_stream->time_base);
pkt.pos = -1;
ret = av_interleaved_write_frame(output_ctx, &pkt);
if (ret < 0)
{
std::cerr << "Error writing frame" << std::endl;
break;
}
av_packet_unref(&pkt);
}
av_frame_free(&temp_frame);
}
}
}
av_write_trailer(output_ctx);
// 释放资源
av_frame_free(&frame);
avcodec_free_context(&codec_ctx);
avformat_close_input(&input_ctx);
if (output_ctx && !(output_ctx->oformat->flags & AVFMT_NOFILE))
{
avio_closep(&output_ctx->pb);
}
avformat_free_context(output_ctx);
sws_freeContext(sws_ctx);
return 0;
}
5.获取推流数据
常用的工具为VLC播放器,选择打开网络串流地址
就能播放推流画面了