ffmpeg的初始化配置,在合成工作都是根据这个ffmpeg的配置来做的,是和成ts流还是flv,是推动远端还是保存到本地, FFmpeg 的核心数据结构,负责协调编码、封装和写入操作。它相当于推流的“总指挥”。
先来看一下ffmpeg的推流器配置。
typedef struct
{
AVStream *stream; // 输出流
AVCodecContext *enc; // 编码器
int64_t next_timestamp; //下一个时间戳
int samples_count; // 采样数
AVPacket *packet; // 编码数据包
} OutputStream;
typedef struct
{
unsigned int config_id; //用与多路码流
int protocol_type; //流媒体TYPE
char network_addr[NETWORK_ADDR_LENGTH];//流媒体地址
enum AVCodecID video_codec; //视频编码器ID
enum AVCodecID audio_codec; //音频编码器ID
OutputStream video_stream; //VIDEO的STREAM配置
OutputStream audio_stream; //AUDIO的STREAM配置
AVFormatContext *oc; //是存储音视频封装格式中包含的信息的结构体,也是FFmpeg中统领全局的结构体,对文件的封装、编码操作从这里开始。
} RKMEDIA_FFMPEG_CONFIG; //FFMPEG配置
定义了我们流媒体的地址,和复合流是ts/flv等等。
在使用 FFmpeg 实现推流的过程中,每个步骤都有其特定的作用,这些步骤是基于音视频处理和网络传输的基本原理设计的。以下是对每个主要步骤的详细解释,以及它们为什么必不可少:
1. 初始化网络模块 (avformat_network_init)
作用: 初始化 FFmpeg 的网络功能,支持像 RTMP、HTTP 这样的网络协议。
为什么需要: FFmpeg 默认不加载网络相关模块,此步骤启用网络支持,确保推流到远程服务器(如 RTMP 服务器)时能够正确建立连接。如果不初始化,推流会因为缺少网络协议支持而失败。
原理: FFmpeg 的网络功能依赖底层库(如 librtmp 或内置的 TCP/UDP 实现),初始化会加载这些模块并注册相关协议。
2. 分配输出格式上下文 (avformat_alloc_output_context2)
//FLV_PROTOCOL is RTMP TCP
if (ffmpeg_config->protocol_type == FLV_PROTOCOL)
{
//初始化一个FLV的AVFormatContext
ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv", ffmpeg_config->network_addr);
if (ret < 0)
{
return -1;
}
}
//TS_PROTOCOL is SRT UDP RTSP
else if (ffmpeg_config->protocol_type == TS_PROTOCOL)
{
//初始化一个TS的AVFormatContext
ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "mpegts", ffmpeg_config->network_addr);
if (ret < 0)
{
return -1;
}
}
作用: 创建一个用于输出的容器对象(AVFormatContext),并指定输出格式(FLV/)。
为什么需要: 推流需要将音视频数据封装成某种格式(比如 FLV 是 RTMP 的常用容器格式),这个上下文对象管理流的元数据、格式信息和输出目标。如果没有这个对象,FFmpeg 无法知道数据要以什么形式输出到哪里。
原理: 输出格式上下文是 FFmpeg 的核心数据结构,负责协调编码、封装和写入操作。它相当于推流的“总指挥”。
3. 创建输出流 (avformat_new_stream)
//是否指定了视频编解码器。
if (fmt->video_codec != AV_CODEC_ID_NONE)
{
// 添加一个流(如视频流或音频流),并为其分配编码参数。
ret = add_stream(&ffmpeg_config->video_stream, ffmpeg_config->oc, &video_codec, fmt->video_codec);
if (ret < 0)
{
avcodec_free_context(&ffmpeg_config->video_stream.enc);
free_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);
avformat_free_context(ffmpeg_config->oc);
return -1;
}
ret = open_video(ffmpeg_config->oc, video_codec, &ffmpeg_config->video_stream, NULL);
if (ret < 0)
{
avformat_free_context(ffmpeg_config->oc);
}
}
//是否指定了音频编解码器。
if (fmt->audio_codec != AV_CODEC_ID_NONE)
{
//添加一路视频流
ret = add_stream(&ffmpeg_config->audio_stream, ffmpeg_config->oc, &audio_codec, fmt->audio_codec);
if (ret < 0)
{
avcodec_free_context(&ffmpeg_config->audio_stream.enc);
free_stream(ffmpeg_config->oc, &ffmpeg_config->audio_stream);
avformat_free_context(ffmpeg_config->oc);
return -1;
}
ret = open_audio(ffmpeg_config->oc, audio_codec, &ffmpeg_config->audio_stream, NULL);
if (ret < 0)
{
avformat_free_context(ffmpeg_config->oc);
}
}
作用: 在输出格式上下文中添加一个流(如视频流或音频流),并为其分配编码参数。
为什么需要: 推流通常包含视频和音频两种数据,每种数据需要独立的流来承载。创建流是为了定义这些数据的结构(如分辨率、帧率、采样率等),否则 FFmpeg 无法组织数据。
原理: 流(AVStream)是容器中的独立轨道,推流时服务器和客户端会根据流信息解码数据
4. 打开输出 URL (avio_open)
if (!(fmt->flags & AVFMT_NOFILE))
{
//第四步:打开输出文件
/**
* 作用:
* 建立与目标服务器(如 RTMP 服务器)的物理连接。
* 为什么需要:
* 推流是将数据发送到远程服务器的过程,必须先打开一个通道(类似于文件句柄或网络 socket),
* 否则数据无法传输。如果没有这一步,程序会因为找不到输出目标而报错。
* 原理:
* avio_open 使用底层 I/O 抽象层(AVIOContext)与服务器通信,支持多种协议(如 RTMP、HLS)。
* 对于 RTMP,它会通过 librtmp 初始化连接并完成握手。
*/
ret = avio_open(&ffmpeg_config->oc->pb, ffmpeg_config->network_addr, AVIO_FLAG_WRITE);
if (ret < 0)
{
free_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);
free_stream(ffmpeg_config->oc, &ffmpeg_config->audio_stream);
avformat_free_context(ffmpeg_config->oc);
return -1;
}
}
作用: 建立与目标服务器(如 RTMP 服务器)的物理连接。
为什么需要: 推流是将数据发送到远程服务器的过程,必须先打开一个通道(类似于文件句柄或网络 socket),否则数据无法传输。如果没有这一步,程序会因为找不到输出目标而报错。
原理: avio_open 使用底层 I/O 抽象层(AVIOContext)与服务器通信,支持多种协议(如 RTMP、HLS)。对于 RTMP,它会通过 librtmp 初始化连接并完成握手。
5. 写入流头部信息 (avformat_write_header)
// 第五步:写入文件头信息
//这行代码会根据ffmpeg_config->oc中指定的输出格式(如FLV或TS),
//生成并写入相应的头部信息到输出文件或流中。
avformat_write_header(ffmpeg_config->oc, NULL);
作用: 将流的元数据(比如编码格式、分辨率、时长等)写入输出流的开头。
为什么需要: 接收端(比如直播服务器或播放器)需要这些信息来正确解析后续的数据。如果缺少头部信息,接收端可能无法识别流格式,导致播放失败。
原理: 头部信息是容器格式(如 FLV)的标准部分,包含流的描述性数据,类似于文件的“索引”。
6. 写入数据 (av_interleaved_write_frame)
int write_ffmpeg_avpacket(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt)
{
/*将输出数据包时间戳值从编解码器重新调整为流时基 */
av_packet_rescale_ts(pkt, *time_base, st->time_base);
pkt->stream_index = st->index;
/**
* 第六步:写入数据
* 作用:
* 将编码后的音视频数据(AVPacket)写入输出流,发送到服务器。
* 为什么需要:
* 这是推流的核心步骤,所有的音视频内容都通过这一步传输到目标。
* 如果没有这一步,之前的准备工作就毫无意义,因为数据没有实际发送。
* 原理:
* FFmpeg 使用“交错写入”(interleaved)方式处理多路流(如视频和音频),确保时间戳对齐,符合实时传输的要求。
* 数据会被封装为 FLV 标签(Tag)并通过网络发送。
*/
return av_interleaved_write_frame(fmt_ctx, pkt);
}
作用: 将编码后的音视频数据(AVPacket)写入输出流,发送到服务器。
为什么需要: 这是推流的核心步骤,所有的音视频内容都通过这一步传输到目标。如果没有这一步,之前的准备工作就毫无意义,因为数据没有实际发送。
原理: FFmpeg 使用“交错写入”(interleaved)方式处理多路流(如视频和音频),确保时间戳对齐,符合实时传输的要求。数据会被封装为 FLV 标签(Tag)并通过网络发送。
7. 写入流尾部 (av_write_trailer)
//第7步: 写入流尾部 (av_write_trailer)
/**
*
* 作用: 标记流的结束,并写入必要的结尾信息
*/
av_write_trailer(ffmpeg_config.oc); // 写入AVFormatContext的尾巴
作用: 标记流的结束,并写入必要的结尾信息。
为什么需要: 某些容器格式需要在结束时添加元数据(如索引表),以确保接收端能正确处理整个流。如果不写入尾部,流可能会出现不完整或无法播放的问题。
原理: 尾部信息是容器格式规范的一部分,对于 FLV 来说,它可能只是简单地关闭流,但对于其他格式(如 MP4),它可能包含关键的索引。
8. 清理资源 (avio_closep, avformat_free_context)
//第八步:清理资源 (avio_closep, avformat_free_context)
/**
*
* 作用: 关闭网络连接并释放内存
*/
free_stream(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 释放VIDEO_STREAM的资源
free_stream(ffmpeg_config.oc, &ffmpeg_config.audio_stream); // 释放AUDIO_STREAM的资源
avio_closep(&ffmpeg_config.oc->pb); // 释放AVIO资源
avformat_free_context(ffmpeg_config.oc); // 释放AVFormatContext资源
作用: 关闭网络连接并释放内存。
为什么需要: 推流结束后,如果不清理资源,会导致内存泄漏或网络连接未正确关闭,影响程序稳定性。对于长时间运行的推流任务尤其重要。
原理: FFmpeg 的上下文对象和 I/O 句柄占用系统资源(如内存和 socket),需要显式释放以遵循良好的编程实践。
整体工作流程的意义,这些步骤共同构成了 FFmpeg 推流的完整工作流,反映了音视频处理和网络传输的本质:
1. 准备阶段: 初始化环境、定义格式和目标(步骤 1-3)。
2. 连接阶段: 建立与服务器的通信(步骤 4)。
3. 传输阶段: 发送元数据和实际数据(步骤 5-6)。
4. 结束阶段: 完成传输并清理(步骤 7-8)。