使用FFmpeg库(版本号为:4.4.2-0ubuntu0.22.04.1)实现从麦克风获取流并通过RTMP推流。
RTMP服务器使用的是SRS,我这边是跑在Ubuntu上的,最好是关闭掉系统防火墙,不然连接服务器好像会出问题,拉流端使用VLC。如果想降低延时,首先需要配置SRS为Realtime模式,拉流的话就不要用VLC了,使用 ffplay 来拉流播放,拉流命令如下:
ffplay -fflags nobuffer -flags low_delay -framedrop -strict experimental rtmp://192.168.3.230/live/livestream
局域网内用摄像头对着手机秒表实测延时只有零点几秒。
在Linux上查看摄像头信息可使用 v4l2-ctl 工具,查看命令如下:
v4l2-ctl --device=/dev/video0 --list-formats-ext
代码如下:
#include <libavdevice/avdevice.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
int main(void)
{
const char *input_format_name = "video4linux2"; // 输入格式名称,Linux下为video4linux2或v4l2
const char *device_name = "/dev/video0"; // 摄像头设备名称
const char *camera_resolution = "640x480"; // 摄像头分辨率
enum AVPixelFormat camera_pix_fmt = AV_PIX_FMT_YUYV422; // 摄像头像素格式
const char *url = "rtmp://192.168.3.230/live/livestream"; // rtmp地址
int frame_rate = 25; // 帧率
int ret = -1;
int video_streamid = -1;
int64_t frame_index = 0;
AVDictionary *options = NULL;
AVInputFormat *fmt = NULL;
AVFormatContext *in_context = NULL, *out_context = NULL;
struct SwsContext *sws_ctx = NULL;
AVCodecContext *codec_context = NULL;
AVStream *out_stream = NULL;
AVCodec *codec = NULL;
// 打印ffmpeg版本信息
printf("ffmpeg version: %s\n", av_version_info());
// 注册所有设备
avdevice_register_all();
// 查找输入格式
fmt = av_find_input_format(input_format_name);
if (!fmt)
{
printf("av_find_input_format error");
return -1;
}
// 设置分辨率
av_dict_set(&options, "video_size", camera_resolution, 0);
// 打开输入流并初始化格式上下文
ret = avformat_open_input(&in_context, device_name, fmt, &options);
if (ret != 0)
{
// 错误的时候释放options,成功的话 avformat_open_input 内部会释放
av_dict_free(&options);
printf("avformat_open_input error");
return -1;
}
// 查找流信息
if (avformat_find_stream_info(in_context, 0) < 0)
{
printf("avformat_find_stream_info failed\n");
return -1;
}
// 查找视频流索引
video_streamid = av_find_best_stream(in_context, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (video_streamid < 0)
{
printf("cannot find video stream");
goto end;
}
AVStream *video_stream = in_context->streams[video_streamid];
printf("input stream, width: %d, height: %d, format: %s\n",
video_stream->codecpar->width, video_stream->codecpar->height,
av_get_pix_fmt_name((enum AVPixelFormat)video_stream->codecpar->format));
// 检查实际获取到的格式是否为设置的摄像头像素格式
if (video_stream->codecpar->format != camera_pix_fmt)
{
printf("pixel format error");
goto end;
}
// 初始化转换上下文
sws_ctx = sws_getContext(
video_stream->codecpar->width, video_stream->codecpar->height, camera_pix_fmt,
video_stream->codecpar->width, video_stream->codecpar->height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL);
if (!sws_ctx)
{
printf("sws_getContext error\n");
goto end;
}
// 分配输出格式上下文
avformat_alloc_output_context2(&out_context, NULL, "flv", NULL);
if (!out_context)
{
printf("avformat_alloc_output_context2 failed\n");
goto end;
}
// 查找编码器
codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec)
{
printf("Codec not found\n");
goto end;
}
printf("codec name: %s\n", codec->name);
// 创建新的视频流
out_stream = avformat_new_stream(out_context, NULL);
if (!out_stream)
{
printf("avformat_new_stream failed\n");
goto end;
}
// 分配编码器上下文
codec_context = avcodec_alloc_context3(codec);
if (!codec_context)
{
printf("avcodec_alloc_context3 failed\n");
goto end;
}
// 设置编码器参数
codec_context->codec_id = AV_CODEC_ID_H264;
codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
codec_context->pix_fmt = AV_PIX_FMT_YUV420P;
codec_context->width = video_stream->codecpar->width;
codec_context->height = video_stream->codecpar->height;
codec_context->time_base = (AVRational){1, frame_rate}; // 设置时间基
codec_context->framerate = (AVRational){frame_rate, 1}; // 设置帧率
codec_context->bit_rate = 750 * 1000; // 设置比特率
codec_context->gop_size = frame_rate; // 设置GOP大小
codec_context->max_b_frames = 0; // 设置最大B帧数,不需要B帧时设置为0
av_opt_set(codec_context->priv_data, "profile", "baseline", 0); // 设置h264画质级别
av_opt_set(codec_context->priv_data, "tune", "zerolatency", 0); // 设置h264编码优化参数
// 检测输出上下文的封装格式,判断是否设置 AV_CODEC_FLAG_GLOBAL_HEADER
// AV_CODEC_FLAG_GLOBAL_HEADER:由原来编码时在每个关键帧前加入pps和sps,改变为在extradate这个字节区加入pps和sps
if (out_context->oformat->flags & AVFMT_GLOBALHEADER)
{
printf("set AV_CODEC_FLAG_GLOBAL_HEADER\n");
codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
// 打开编码器
if (avcodec_open2(codec_context, codec, NULL) < 0)
{
printf("avcodec_open2 failed\n");
goto end;
}
// 将编码器参数复制到流
ret = avcodec_parameters_from_context(out_stream->codecpar, codec_context);
if (ret < 0)
{
printf("avcodec_parameters_from_context failed\n");
goto end;
}
// 分配内存
AVFrame *input_frame = av_frame_alloc();
AVFrame *frame_yuv420p = av_frame_alloc();
if (!input_frame || !frame_yuv420p)
{
printf("av_frame_alloc error\n");
goto end;
}
AVPacket *packet = av_packet_alloc();
if (!packet)
{
printf("av_packet_alloc failed\n");
goto end;
}
// 设置帧格式
input_frame->format = camera_pix_fmt;
input_frame->width = video_stream->codecpar->width;
input_frame->height = video_stream->codecpar->height;
frame_yuv420p->format = AV_PIX_FMT_YUV420P;
frame_yuv420p->width = video_stream->codecpar->width;
frame_yuv420p->height = video_stream->codecpar->height;
// 分配帧内存
ret = av_frame_get_buffer(frame_yuv420p, 0);
if (ret < 0)
{
printf("av_frame_get_buffer error\n");
goto end;
}
// 打开url
if (!(out_context->oformat->flags & AVFMT_NOFILE))
{
ret = avio_open(&out_context->pb, url, AVIO_FLAG_WRITE);
if (ret < 0)
{
printf("avio_open error (errmsg '%s')\n", av_err2str(ret));
goto end;
}
}
// 写文件头
ret = avformat_write_header(out_context, NULL);
if (ret < 0)
{
printf("avformat_write_header failed\n");
goto end;
}
// 读取帧并进行转换
AVPacket pkt;
while (av_read_frame(in_context, &pkt) >= 0)
{
if (pkt.stream_index == video_streamid)
{
// 把读取的帧数据(AVPacket)拷贝到输入帧(AVFrame)中
ret = av_image_fill_arrays(input_frame->data, input_frame->linesize, pkt.data, camera_pix_fmt,
video_stream->codecpar->width, video_stream->codecpar->height, 1);
if (ret < 0)
{
av_packet_unref(&pkt);
printf("av_image_fill_arrays error\n");
break;
}
// 转换为 YUV420P
sws_scale(sws_ctx, (const uint8_t *const *)input_frame->data, input_frame->linesize, 0,
input_frame->height, frame_yuv420p->data, frame_yuv420p->linesize);
frame_yuv420p->pts = frame_index;
frame_index++;
// 发送帧到编码器
ret = avcodec_send_frame(codec_context, frame_yuv420p);
if (ret < 0)
{
printf("avcodec_send_frame error (errmsg '%s')\n", av_err2str(ret));
break;
}
// 接收编码后的数据包
while (ret >= 0)
{
ret = avcodec_receive_packet(codec_context, packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
break;
}
else if (ret < 0)
{
printf("avcodec_receive_packet error (errmsg '%s')\n", av_err2str(ret));
goto end;
}
packet->stream_index = out_stream->index;
// 将时间戳从编码器时间基转换到流时间基
av_packet_rescale_ts(packet, codec_context->time_base, out_stream->time_base);
packet->pos = -1;
// 推送到RTMP服务器
ret = av_interleaved_write_frame(out_context, packet);
if (ret < 0)
{
printf("av_interleaved_write_frame error (errmsg '%d')\n", ret);
av_packet_unref(packet);
goto end;
}
av_packet_unref(packet);
}
}
av_packet_unref(&pkt);
}
end:
// 释放资源
if (input_frame)
av_frame_free(&input_frame);
if (frame_yuv420p)
av_frame_free(&frame_yuv420p);
if (sws_ctx)
sws_freeContext(sws_ctx);
if (in_context)
avformat_close_input(&in_context);
if (packet)
av_packet_free(&packet);
if (codec_context)
avcodec_free_context(&codec_context);
if (out_context && !(out_context->flags & AVFMT_NOFILE))
avio_close(out_context->pb);
if (out_context)
avformat_free_context(out_context);
return 0;
}
相关博客:FFmpeg YUV编码为H264