21、音视频推流实战:使用FFmpeg API实现RTMP推流,搭建简单的直播推流工具
直播推流,说白了就是把摄像头采集的画面、麦克风采集的声音,实时地送到服务器上。你想想看,抖音、B站直播,背后都是这套逻辑。今天我们就用FFmpeg的API,手撸一个简单的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。
添加音视频流
接下来,我们要往输出上下文里添加流。每个流对应一路数据,比如视频流、音频流。
// 假设我们已经有了编码器上下文 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地址能不能通。
推流循环:读取、编码、发送
核心逻辑来了。我们要循环做三件事:从输入源读取数据、编码、写入输出。
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_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 -re -i input.mp4 -c copy -f flv rtmp://server/live/stream
如果命令行能推成功,你的代码大概率也能推成功。如果命令行都推不上去,那就先别折腾代码了,检查服务器配置吧。
嗯,到这里,一个简单的RTMP推流工具就搭建好了。代码量不大,但每一步都有它的道理。你动手写一遍,遇到问题再回来对照,理解会更深刻。