FFmpeg 实现从摄像头获取流并通过RTMP推流

使用FFmpeg库实现从USB摄像头获取流并通过RTMP推流,FFmpeg版本为4.4.2-0。RTMP服务器使用的是SRS,拉流端使用VLC。

在Linux上查看摄像头信息可使用 v4l2-ctl 工具,查看命令如下:

bash 复制代码
v4l2-ctl --device=/dev/video0 --list-formats-ext

代码如下:

cpp 复制代码
#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, retVal = -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;
    AVPacket *packet = NULL;

    // 注册所有设备
    avdevice_register_all();

    // 查找输入格式
    fmt = av_find_input_format(input_format_name);
    if (!fmt)
    {
        fprintf(stderr, "av_find_input_format error");
        return -1;
    }

    // 设置分辨率
    av_dict_set(&options, "video_size", camera_resolution, 0);

    // 打开输入流并初始化格式上下文
    retVal = avformat_open_input(&in_context, device_name, fmt, &options);
    if (retVal != 0)
    {
        // 打印错误信息
        fprintf(stderr, "avformat_open_input error");
        return -1;
    }

    // 查找视频流索引
    video_streamid = av_find_best_stream(in_context, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (video_streamid < 0)
    {
        fprintf(stderr, "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)
    {
        fprintf(stderr, "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)
    {
        fprintf(stderr, "sws_getContext error\n");
        goto end;
    }

    // 创建帧
    AVFrame *input_frame = av_frame_alloc();
    AVFrame *frame_yuv420p = av_frame_alloc();
    if (!input_frame || !frame_yuv420p)
    {
        fprintf(stderr, "av_frame_alloc error\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;

    // 分配帧内存
    retVal = av_frame_get_buffer(frame_yuv420p, 0);
    if (retVal < 0)
    {
        fprintf(stderr, "av_frame_get_buffer error\n");
        goto end;
    }

    // 分配输出格式上下文
    avformat_alloc_output_context2(&out_context, NULL, "flv", NULL);
    if (!out_context)
    {
        fprintf(stderr, "avformat_alloc_output_context2 failed\n");
        goto end;
    }

    // 查找编码器
    codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec)
    {
        fprintf(stderr, "Codec not found\n");
        goto end;
    }
    printf("codec name: %s\n", codec->name);

    // 创建新的视频流
    out_stream = avformat_new_stream(out_context, NULL);
    if (!out_stream)
    {
        fprintf(stderr, "avformat_new_stream failed\n");
        goto end;
    }

    // 分配编码器上下文
    codec_context = avcodec_alloc_context3(codec);
    if (!codec_context)
    {
        fprintf(stderr, "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 = frame_yuv420p->width;
    codec_context->height = frame_yuv420p->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)
    {
        fprintf(stderr, "avcodec_open2 failed\n");
        goto end;
    }

    // 将编码器参数复制到流
    int result = avcodec_parameters_from_context(out_stream->codecpar, codec_context);
    if (result < 0)
    {
        fprintf(stderr, "avcodec_parameters_from_context failed\n");
        goto end;
    }

    // 打开url
    if (!(out_context->oformat->flags & AVFMT_NOFILE))
    {
        result = avio_open(&out_context->pb, url, AVIO_FLAG_WRITE);
        if (result < 0)
        {
            fprintf(stderr, "avio_open error (errmsg '%s')\n", av_err2str(result));
            goto end;
        }
    }

    // 写文件头
    result = avformat_write_header(out_context, NULL);
    if (result < 0)
    {
        fprintf(stderr, "avformat_write_header failed\n");
        goto end;
    }

    packet = av_packet_alloc();
    if (!packet)
    {
        fprintf(stderr, "av_packet_alloc failed\n");
        goto end;
    }

    // 读取帧并进行转换
    AVPacket pkt;
    while (av_read_frame(in_context, &pkt) >= 0)
    {
        if (pkt.stream_index == video_streamid)
        {
            // 把读取的帧数据(AVPacket)拷贝到输入帧(AVFrame)中
            retVal = 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 (retVal < 0)
            {
                av_packet_unref(&pkt);
                fprintf(stderr, "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++;
            // 发送帧到编码器
            result = avcodec_send_frame(codec_context, frame_yuv420p);
            if (result < 0)
            {
                fprintf(stderr, "avcodec_send_frame error (errmsg '%s')\n", av_err2str(result));
                break;
            }

            // 接收编码后的数据包
            while (result >= 0)
            {
                result = avcodec_receive_packet(codec_context, packet);
                if (result == AVERROR(EAGAIN) || result == AVERROR_EOF)
                {
                    break;
                }
                else if (result < 0)
                {
                    fprintf(stderr, "avcodec_receive_packet error (errmsg '%s')\n", av_err2str(result));
                    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服务器
                result = av_interleaved_write_frame(out_context, packet);
                if (result < 0)
                {
                    fprintf(stderr, "av_interleaved_write_frame error (errmsg '%d')\n", result);
                    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)
        avformat_free_context(out_context);

    return ret;
}
相关推荐
叶余1 小时前
FFmpeg命令行选项
ffmpeg
来吧~1 小时前
vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)
前端·vue.js·音视频
Bubluu1 小时前
浏览器点击视频裁剪当前帧,然后粘贴到页面
开发语言·javascript·音视频
深圳启明云端科技1 小时前
ESP-IDF HTTP POST请求发送音频-ESP32物联网方案
物联网·http·音视频
从后端到QT1 小时前
音视频采集推流时间戳记录方案
ffmpeg·音视频
ai产品老杨3 小时前
报警推送消息升级的名厨亮灶开源了。
vue.js·人工智能·安全·开源·音视频
莫固执,朋友3 小时前
Linux下编译 libwebsockets简介和使用示例
linux·websocket·音视频
Say-hai7 小时前
音视频入门知识(五):流媒体篇
音视频
EasyNVR14 小时前
互联网视频云平台EasyDSS无人机推流直播技术如何助力野生动植物保护工作?
音视频·无人机·视频监控
悠着,大嘟嘟15 小时前
FFmpeg音频解码详解
ffmpeg·音视频