21、音视频推流实战:使用FFmpeg API实现RTMP推流,搭建简单的直播推流工具

直播推流,说白了就是把摄像头采集的画面、麦克风采集的声音,实时地送到服务器上。你想想看,抖音、B站直播,背后都是这套逻辑。今天我们就用FFmpeg的API,手撸一个简单的RTMP推流工具。

我个人习惯,做这种实战项目,先搞清楚数据流怎么走,再动手写代码。不然写到一半发现方向错了,那才叫痛苦。

推流的核心流程

先看一张图,把整个流程理清楚:

RTMP推流核心数据流 视频输入 (摄像头/文件) 音频输入 (麦克风/文件) av_read_frame 读取原始数据 avcodec_encode 压缩编码 av_write_frame FLV封装 RTMP 推送 视频流 音频流 编码 复用

嗯,流程其实不复杂。左边是输入源,中间是编码和封装,右边是推出去。每一步都有对应的FFmpeg API。

初始化输出上下文

第一步,我们要创建一个输出上下文。这个上下文会告诉FFmpeg:我们要往哪里推,用什么格式。

AVFormatContext *out_ctx = NULL;
avformat_alloc_output_context2(&out_ctx, NULL, "flv", "rtmp://your-server/live/stream");
if (!out_ctx) {
    fprintf(stderr, "创建输出上下文失败\n");
    return -1;
}

这里有个坑,我刚开始做的时候踩过。第三个参数"flv"不能写成"rtmp"。为什么?因为RTMP协议传输的数据,实际上是用FLV格式封装的。你写"rtmp"的话,FFmpeg不认识这个muxer。

注意:输出格式必须指定为"flv",而不是"rtmp"。RTMP是传输协议,FLV才是封装格式。

添加音视频流

接下来,我们要往输出上下文里添加流。每个流对应一路数据,比如视频流、音频流。

// 假设我们已经有了编码器上下文 enc_ctx
AVStream *out_stream = avformat_new_stream(out_ctx, NULL);
if (!out_stream) {
    fprintf(stderr, "创建输出流失败\n");
    return -1;
}

// 复制编码器参数到输出流
avcodec_parameters_from_context(out_stream->codecpar, enc_ctx->codecctx);

avcodec_parameters_from_context这一步很容易被忽略。我记得有一次调试了半天,发现推出去的流播放器不认,就是因为参数没传过去。

打开输出IO并写入头信息

流添加好了,接下来要打开网络连接,写入FLV的文件头。

if (!(out_ctx->oformat->flags & AVFMT_NOFILE)) {
    int ret = avio_open(&out_ctx->pb, out_ctx->url, AVIO_FLAG_WRITE);
    if (ret < 0) {
        fprintf(stderr, "打开输出IO失败\n");
        return ret;
    }
}

avformat_write_header(out_ctx, NULL);

avio_open会建立RTMP连接。如果服务器地址不对、或者服务器没开,这里就会报错。我建议你在调用avio_open之前,先手动用ffplay或者VLC测试一下RTMP地址能不能通。

小技巧:调试阶段可以用 ffplay rtmp://your-server/live/stream 先测试服务器是否正常。

推流循环:读取、编码、发送

核心逻辑来了。我们要循环做三件事:从输入源读取数据、编码、写入输出。

AVPacket pkt;
while (1) {
    // 1. 从输入读取
    int ret = av_read_frame(in_ctx, &pkt);
    if (ret < 0) break;

    // 2. 发送到编码器
    ret = avcodec_send_packet(enc_ctx, &pkt);
    if (ret < 0) {
        av_packet_unref(&pkt);
        continue;
    }

    // 3. 接收编码后的包
    AVPacket enc_pkt;
    av_init_packet(&enc_pkt);
    ret = avcodec_receive_packet(enc_ctx, &enc_pkt);
    if (ret == 0) {
        // 4. 写入输出(推流)
        enc_pkt.stream_index = out_stream->index;
        av_interleaved_write_frame(out_ctx, &enc_pkt);
        av_packet_unref(&enc_pkt);
    }

    av_packet_unref(&pkt);
}

这里有个细节:av_interleaved_write_frame会自动处理时间戳和交错。你想想看,如果视频和音频的时间戳不对齐,播放器那边就会卡顿或者音画不同步。

关键点:av_interleaved_write_frame 比 av_write_frame 更智能,它会自动排序包,保证音视频交错正确。

收尾工作:写入尾信息

推流结束或者用户中断时,要记得写尾信息,关闭连接。

av_write_trailer(out_ctx);

if (out_ctx && !(out_ctx->oformat->flags & AVFMT_NOFILE)) {
    avio_closep(&out_ctx->pb);
}
avformat_free_context(out_ctx);

av_write_trailer会写入FLV的结束标记。如果不调用,播放器那边可能会一直等待数据,导致无法正常结束播放。

完整代码结构

把上面这些拼起来,就是一个完整的推流工具了。我整理了一个最小可用的代码框架:

int main(int argc, char *argv[]) {
    // 1. 初始化输入(打开文件或设备)
    AVFormatContext *in_ctx = NULL;
    avformat_open_input(&in_ctx, argv[1], NULL, NULL);
    avformat_find_stream_info(in_ctx, NULL);

    // 2. 找到视频流,创建编码器
    // ...(省略具体查找逻辑)

    // 3. 初始化输出
    AVFormatContext *out_ctx = NULL;
    avformat_alloc_output_context2(&out_ctx, NULL, "flv", argv[2]);

    // 4. 添加输出流
    // ...(省略具体添加逻辑)

    // 5. 打开IO,写头
    avio_open(&out_ctx->pb, argv[2], AVIO_FLAG_WRITE);
    avformat_write_header(out_ctx, NULL);

    // 6. 推流循环
    // ...(省略循环逻辑)

    // 7. 收尾
    av_write_trailer(out_ctx);
    avio_closep(&out_ctx->pb);
    avformat_free_context(out_ctx);
    avformat_close_input(&in_ctx);

    return 0;
}

编译的时候记得链接FFmpeg库:

gcc push_stream.c -o push_stream -lavformat -lavcodec -lavutil -lswscale -lswresample -lm

常见问题与避坑

问题 原因 解决办法
avio_open返回-5 RTMP服务器地址不可达 检查网络、服务器是否启动
推流后播放器黑屏 编码参数没正确传递给输出流 检查avcodec_parameters_from_context
音画不同步 时间戳没有正确转换 使用av_rescale_q_rnd转换时间基
推流卡顿 编码速度跟不上采集速度 降低编码质量或使用硬件编码

我曾经遇到过一个特别诡异的问题:推流到某CDN平台,播放器总是延迟30秒。后来发现是CDN那边缓存设置的问题,跟FFmpeg没关系。所以遇到问题,先怀疑自己的代码,再怀疑网络,最后怀疑服务器配置。

调试建议:先用ffmpeg命令行推流测试服务器是否正常:
ffmpeg -re -i input.mp4 -c copy -f flv rtmp://server/live/stream

如果命令行能推成功,你的代码大概率也能推成功。如果命令行都推不上去,那就先别折腾代码了,检查服务器配置吧。

嗯,到这里,一个简单的RTMP推流工具就搭建好了。代码量不大,但每一步都有它的道理。你动手写一遍,遇到问题再回来对照,理解会更深刻。


公众号:蓝海资料掘金营,微信deep3321