ArLiveLite 用到的 FFmpeg API

ArLiveLite 用到的 FFmpeg API

范围:ArLiveLite/pusher/ArFFWriter.cpp(推流封装/发送)+ ArLiveLite/player/ARFFPlayer.cpp(拉流解封装/解码)。ArLiveLite/player/FFBuffer.cpp 不直接调用任何 FFmpeg API(纯业务缓冲逻辑)。所有代码片段均为源文件原文摘录,行号可直接跳转核对;源文件按 GBK 保存、当前以 UTF-8 读取,中文注释部分会显示为乱码,摘录时已尽量避开纯乱码注释行。

0. API 一览表

类别 API 出现文件:行
全局初始化 avformat_network_init/deinit ARFFPlayer.cpp:43,53
全局初始化(已废弃,未调用) av_register_all ARFFPlayer.cpp:42(注释掉)
封装器创建 av_guess_formatavformat_alloc_output_context2 ArFFWriter.cpp:484,490
解封装器创建 avformat_alloc_contextavformat_open_inputavformat_find_stream_info ARFFPlayer.cpp:891,922,938
流/编解码器参数 avformat_new_streamavcodec_parameters_to_contextav_find_best_stream ArFFWriter.cpp:508,659 / ARFFPlayer.cpp:96,71
编解码器生命周期 avcodec_find_encoder/decoderavcodec_alloc_context3avcodec_open2avcodec_free_contextavcodec_close 两文件均有,见下
编解码(现代 API) avcodec_send_packetavcodec_receive_frame ARFFPlayer.cpp:606,611,699,704
编解码(旧 API,#if 0 死代码) avcodec_decode_audio4avcodec_decode_video2 ARFFPlayer.cpp:604,697
I/O avio_open2 ArFFWriter.cpp:764
收发包 av_interleaved_write_frameav_read_frame ArFFWriter.cpp:337,450 / ARFFPlayer.cpp:341
容器生命周期 avformat_write_headerav_write_traileravformat_close_inputavformat_free_context ArFFWriter.cpp:775,807,808,811 / ARFFPlayer.cpp:940,1031
Packet 工具 av_init_packetav_packet_rescale_tsav_packet_unref ArFFWriter.cpp:303,312 / ARFFPlayer.cpp:335,368
时间基转换 av_rescale_qav_rescaleav_q2d 两文件均大量使用
选项设置 av_dict_setav_dict_set_intav_opt_set(未启用) 两文件均有
重采样 swr_allocswr_alloc_set_optsswr_initswr_convertswr_free ARFFPlayer.cpp:632-646,990-1040
Frame/Packet 分配 av_frame_allocav_frame_freeav_frame_unrefav_malloc 两文件均有
调试/信息 av_dump_formatav_strerrorav_get_media_type_string 两文件均有
Seek av_seek_frame ARFFPlayer.cpp:230,291

1. 推流侧:ArFFWriter.cpp ------ 用 avformat 手搓一个 RTMP/FLV Muxer

1.1 创建输出上下文

cpp 复制代码
// ArFFWriter.cpp:479-500
bool ArFFWriter::Connect()
{
    AVOutputFormat* output_format = nullptr;
    output_format = av_guess_format(str_format_.c_str(), nullptr, nullptr);  // "flv" / "rtp_mpegts" / "rtsp"
    if (output_format == nullptr) { Release(); return false; }

    int error = avformat_alloc_output_context2(&format_context_, output_format, nullptr, str_url_.c_str());
    if (error < 0) { /* av_strerror + Release() */ }
}

str_format_ARFFPusher::startTask(见"具体功能"文档)按 URL scheme 传入 "flv"/"rtp_mpegts"/"rtsp"av_guess_format 就是 FFmpeg 里"按短名找 AVOutputFormat"的标准写法。

1.2 添加视频流并生成 extradata(SPS/PPS)------ 本仓库最复杂的一段 FFmpeg 代码

cpp 复制代码
// ArFFWriter.cpp:507-522
const AVCodec* codec = avcodec_find_encoder(codecId);      // AV_CODEC_ID_H264 / H265
vid_stream_ = avformat_new_stream(format_context_, codec); // 加一路视频流
AVCodecParameters* codecpar = vid_stream_->codecpar;
int w = 1920, h = 1080, nFps = 30, nBitrate = 1024;         // ⚠️ 硬编码,与真实编码参数无关,见下

codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
codecpar->codec_id = codecId;
codecpar->bit_rate = nBitrate * 1000;
codecpar->width = w;
codecpar->height = h;
codecpar->format = AV_PIX_FMT_YUV420P;

⚠️ 值得单独指出的细节 :这里被赋值的 codecpar真正输出流 vid_stream_->codecpar(不是后面临时编码器的),但 width/height/bit_rate 用的是本函数局部写死的 1920/1080/1024kbps,与 V_H264Encoder 实际编码使用的分辨率/码率毫无关系。也就是说推流生成的 FLV 容器里视频流的 codecpar 元数据(onMetaData 里的宽高信息来源之一)从一开始就是错的,只是大多数播放器最终显示尺寸看的是 SPS 里的真实宽高而不是这个 codecpar,所以现象上不容易被发现。

紧接着为了填 extradata(AVCDecoderConfigurationRecord 需要的 SPS/PPS),又单独造了一个完全独立、只为了拿 extradata的编码器:

cpp 复制代码
// ArFFWriter.cpp:542-556, 590-614, 634-647
AVCodecContext* vidEncodeCtx = avcodec_alloc_context3(codec);
vidEncodeCtx->pix_fmt = AV_PIX_FMT_YUV420P;
vidEncodeCtx->width = w;              // 同样是写死的 1920
vidEncodeCtx->height = h;             // 写死的 1080
vidEncodeCtx->time_base = { 1, nFps };
vidEncodeCtx->gop_size = 3 * nFps;
vidEncodeCtx->max_b_frames = 0;       // 无 B 帧,PTS==DTS 的前提

AVDictionary* param = 0;
if (vidEncodeCtx->codec_id == AV_CODEC_ID_H264) {
    av_dict_set(&param, "preset", "ultrafast", 0);
    av_dict_set(&param, "tune", "zerolatency", 0);
    av_dict_set(&param, "profile", "baseline", 0);
}
vidEncodeCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;  // 关键:不设这个 flag,extradata 不会被填充
int res = avcodec_open2(vidEncodeCtx, codec, &param);
if (res < 0) {
    // 硬编码兜底 SPS/PPS 字节数组(23 字节),来源见下方"兜底"小节
} else if (vidEncodeCtx->extradata_size > 0) {
    codecpar->extradata_size = vidEncodeCtx->extradata_size;
    codecpar->extradata = (uint8_t*)av_malloc(vidEncodeCtx->extradata_size + AV_INPUT_BUFFER_PADDING_SIZE);
    memcpy(codecpar->extradata, vidEncodeCtx->extradata, vidEncodeCtx->extradata_size);
}
avcodec_free_context(&vidEncodeCtx);   // 拿到 extradata 后立刻销毁,这个编码器从未真正编码过一帧画面

要点AV_CODEC_FLAG_GLOBAL_HEADER 是这段代码能工作的关键------设置这个 flag 后,libx264(或平台默认 H264 encoder)在 avcodec_open2 时就会把 SPS/PPS 写入 AVCodecContext::extradata,不需要真正编码一帧就能拿到。这是 FFmpeg "只要 extradata、不要编码"场景的标准取巧写法。

1.3 兜底:硬编码 SPS/PPS

cpp 复制代码
// ArFFWriter.cpp:618-630
if (res < 0) {
    if (vid_codec_type_ == CT_H264) {
        // 引用了一篇 B站播放器兼容性博客: https://www.cnblogs.com/subo_peng/p/7800658.html
        unsigned char sps_pps[23] = { 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x0a, 0xf8, 0x0f, 0x00, 0x44, 0xbe, 0x8,
              0x00, 0x00, 0x00, 0x01, 0x68, 0xce, 0x38, 0x80 };
        codecpar->extradata_size = 23;
        codecpar->extradata = (uint8_t*)av_malloc(23 + AV_INPUT_BUFFER_PADDING_SIZE);
        memcpy(codecpar->extradata, sps_pps, 23);
    }
}

如果目标 FFmpeg 编译时没有 libx264(avcodec_open2 失败),就直接塞一份写死的 SPS(0x67 0x42 0x00 0x0a...profile=0x42=66=baseline) + PPS(0x68 0xce 0x38 0x80)。这份 extradata 与真实编码画面的 SPS/PPS 完全无关,只是为了让 muxer/播放器"有 extradata 可用不报错"。

1.4 音频流参数(AAC/MP3/OPUS/G711A)

cpp 复制代码
// ArFFWriter.cpp:655-677
AVCodecID codecId = (aud_codec_type_ == CT_AAC) ? AV_CODEC_ID_AAC :
    (aud_codec_type_ == CT_MP3) ? AV_CODEC_ID_MP3 :
    (aud_codec_type_ == CT_OPUS) ? AV_CODEC_ID_OPUS :
    (aud_codec_type_ == CT_G711A) ? AV_CODEC_ID_PCM_ALAW : AV_CODEC_ID_NONE;
aud_stream_ = avformat_new_stream(format_context_, avcodec_find_encoder(codecId));
codecpar->bit_rate = 64000;
if (codecId == AV_CODEC_ID_PCM_ALAW) {
    codecpar->channels = 1; codecpar->channel_layout = AV_CH_LAYOUT_MONO; codecpar->sample_rate = 8000;
} else {
    codecpar->channels = 2; codecpar->channel_layout = AV_CH_LAYOUT_STEREO; codecpar->sample_rate = 44100;
}

extradata(AAC 的 AudioSpecificConfig)生成方式与视频完全一致:造一个独立 AVCodecContextavcodec_open2 拿 extradata 再销毁(:689-729)。

1.5 协议相关的 av_dict_set 选项(按 scheme 分支)

cpp 复制代码
// ArFFWriter.cpp:736-760
AVDictionary* options = nullptr;
if (是 rtmp:// 或 rtmps://) {
    char tc_url[1024];
    sprintf(tc_url, "%.*s", strrchr(str_url_.c_str(), '/') - str_url_.c_str(), str_url_.c_str());
    av_dict_set(&options, "rtmp_tcurl", tc_url, 0);
    av_dict_set(&options, "fflags", "flush_packets", 0);
    av_dict_set(&options, "rtmp_flashver", "FMLE/3.0 (compatible; FMSc/1.0)", 0);  // 伪装 Adobe FMLE,兼容部分 CDN 的白名单校验
}
else if (是 rtsp://) {
    av_dict_set(&options, "buffer_size", "4096000", 0);
    av_dict_set(&options, "rtsp_transport", "tcp", 0);
    av_dict_set(&options, "stimeout", "5000000", 0);   // 单位微秒 = 5s
    av_dict_set(&options, "max_delay", "500000", 0);
}
else if (是 rtp://) {  /* 同上,rtp_transport=tcp */ }
else {
    av_dict_set(&options, "fflags", "flush_packets", 0);
}

1.6 打开 I/O、写 Header、写包、写 Trailer

cpp 复制代码
// ArFFWriter.cpp:762-785
if (!(format_context_->oformat->flags & AVFMT_NOFILE)) {
    avio_open2(&format_context_->pb, format_context_->url, AVIO_FLAG_WRITE, nullptr, nullptr);
}
avformat_write_header(format_context_, &options);   // 真正发起 RTMP connect/createStream/publish 握手
av_dump_format(format_context_, 0, format_context_->url, 1);  // 调试信息打到 stderr

// SetVideoEncData / SetAudioEncData 内部(:303-337,399-450):
AVPacket av_packet;
av_init_packet(&av_packet);   // ⚠️ 不能用 av_packet = {0},否则生成文件不可拖动(作者踩坑注释 20230323)
av_packet_rescale_ts(&av_packet, {1,1000}, stream->time_base);
if (str_format_ == "flv") {
    // 对 FLV 特殊处理:直接用原始毫秒覆盖掉刚 rescale 的结果
    av_packet.pts = pts_ms; av_packet.dts = dts_ms;
}
av_interleaved_write_frame(format_context_, &av_packet);

// Release()(:801-823)
av_write_trailer(format_context_);
avformat_close_input(&format_context_);
avformat_free_context(format_context_);

2. 拉流侧:ARFFPlayer.cpp ------ avformat 解封装 + avcodec 解码 + swresample 重采样

2.1 全局初始化(Meyer's Singleton 包装)

cpp 复制代码
// ARFFPlayer.cpp:38-55
class FFContex {
protected:
    FFContex(void) {
        //av_register_all();  // 新版本已经不需要(旧 FFmpeg 需要,新版本内部自动注册所有 codec/format)
        avformat_network_init();
    };
public:
    static FFContex& Inst() { static FFContex gInst; return gInst; }
    virtual ~FFContex(void) { avformat_network_deinit(); };
};

用 C++11 function-local static 保证 avformat_network_init()(初始化网络协议模块,如 TLS/OpenSSL)全进程只调用一次,ARFFPlayer 构造函数里 FFContex::Inst() 触发。

2.2 打开输入、按协议设置网络选项

cpp 复制代码
// ARFFPlayer.cpp:887-927
fmt_ctx_ = avformat_alloc_context();
fmt_ctx_->interrupt_callback.callback = custom_interrupt_callback;  // 见 2.3,用于超时/主动中断阻塞 I/O
fmt_ctx_->interrupt_callback.opaque = this;

AVDictionary* options = NULL;
av_dict_set(&options, "nobuffer", "1", 0);
if (是 rtmp://) { av_dict_set(&options, "timeout", NULL, 0); }
if (是 rtsp://) {
    av_dict_set(&options, "rtsp_transport", b_use_tcp_ ? "tcp" : "udp", 0);
    av_dict_set(&options, "stimeout", "3000000", 0);   // 3s 超时
}
if (是 http:// 或 https://) {
    av_dict_set(&options, "timeout", "3000000", 0);
    av_dict_set_int(&options, "multiple_requests", 1, 0);
    av_dict_set_int(&options, "read_ahead_limit", INT_MAX, 0);
}
avformat_open_input(&fmt_ctx_, str_play_url_.c_str(), NULL, &options);

fmt_ctx_->probesize = 128 * 1024;              // 减小探测数据量,加快起播
fmt_ctx_->max_analyze_duration = 1 * AV_TIME_BASE;
avformat_find_stream_info(fmt_ctx_, NULL);

probesize/max_analyze_duration 调小是直播场景的典型优化------默认值是为点播文件设计的,用在直播流上会让"首次拿到流信息"的等待时间明显变长。

2.3 中断回调(超时机制的真正实现)

cpp 复制代码
// ARFFPlayer.cpp:115-118
static int custom_interrupt_callback(void *arg) {
    ARFFPlayer* splayer = (ARFFPlayer*)arg;
    return splayer->Timeout();   // 返回非 0,FFmpeg 会主动中断当前阻塞的 I/O 调用(如 avformat_open_input / av_read_frame)
}

这是 FFmpeg 提供的标准"从外部打断阻塞网络调用"机制------AVFormatContext::interrupt_callback 会被 FFmpeg 内部 I/O 层周期性调用,一旦返回非 0 就立即以 AVERROR_EXIT 返回,避免了自己起线程操作 socket 才能实现超时的麻烦。

2.4 查找并打开解码器(open_codec_context,视频/音频复用同一份逻辑)

cpp 复制代码
// ARFFPlayer.cpp:59-113
static int open_codec_context(int *stream_idx, AVCodecContext **dec_ctx, AVFormatContext *fmt_ctx, enum AVMediaType type)
{
    int stream_index = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);  // 自动挑选"最佳"流
    AVStream* st = fmt_ctx->streams[stream_index];
    const AVCodec* dec = avcodec_find_decoder(st->codecpar->codec_id);
    *dec_ctx = avcodec_alloc_context3(dec);
    avcodec_parameters_to_context(*dec_ctx, st->codecpar);   // 把 AVStream 的参数拷进独立的解码 context

    AVDictionary *opts = NULL;
    av_dict_set(&opts, "refcounted_frames", "1", 0);         // 用引用计数管理 AVFrame 内存,避免 decode 时被覆盖
    avcodec_open2(*dec_ctx, dec, &opts);
    *stream_idx = stream_index;
    return 0;
}

2.5 解码:现代 API 是活代码,旧 API 是 #if 0 保留对照

cpp 复制代码
// ARFFPlayer.cpp:600-619(音频)/ 696-712(视频,逻辑对称)
#if 0
    int ret = avcodec_decode_audio4(audio_dec_ctx_, avframe_, &frameFinished, &pkt);  // 旧 API,已弃用
#else
    int ret = avcodec_send_packet(audio_dec_ctx_, &pkt);
    if (ret >= 0) {
        ret = avcodec_receive_frame(audio_dec_ctx_, avframe_);
        frameFinished = (ret >= 0) ? 1 : 0;
    }
#endif

avcodec_decode_audio4/avcodec_decode_video2 是 FFmpeg 3.1 之前的老解码接口(一次调用完成"送包+取帧",且不支持一个包解出多帧的场景),avcodec_send_packet/avcodec_receive_frame(3.1 起引入)是现在推荐的异步风格接口。这里用 #if 0 把旧写法整段保留在代码里作为对照,而不是删掉------是阅读这类跨版本维护代码时常见的"保留旧实现供切换/调试"的写法。

2.6 音频重采样(libswresample

cpp 复制代码
// ARFFPlayer.cpp:990-998(初始化)
audio_convert_ctx_ = swr_alloc();
audio_convert_ctx_ = swr_alloc_set_opts(audio_convert_ctx_,
    av_get_default_channel_layout(n_out_channels_), AV_SAMPLE_FMT_S16, n_out_sample_hz_,   // 目标:立体声/S16/48000(固定输出格式)
    audio_dec_ctx_->channel_layout, audio_dec_ctx_->sample_fmt, audio_dec_ctx_->sample_rate, // 源:跟随实际解码出的流参数
    0, NULL);
swr_init(audio_convert_ctx_);
n_resmap_size_ = av_samples_get_buffer_size(NULL, av_get_channel_layout_nb_channels(...), frame_size, audio_dec_ctx_->sample_fmt, 1);
p_resamp_buffer_ = (uint8_t*)av_malloc(n_resmap_size_);

// OnBufferDecodeAudioData 里(:624-646):源参数变化时重建 SwrContext
if (avframe_->sample_rate != n_sample_hz_ || avframe_->channels != n_channels_) {
    swr_free(&audio_convert_ctx_);
    audio_convert_ctx_ = swr_alloc_set_opts(...);  // 用新的源参数重新初始化
    swr_init(audio_convert_ctx_);
}
int samples = swr_convert(audio_convert_ctx_, &p_resamp_buffer_, n_resmap_size_,
    (const uint8_t **)avframe_->data, avframe_->nb_samples);

要点:把"统一转成固定输出格式(48000Hz/立体声/S16)"和"源格式可能中途变化(比如流重新协商)需要重建 SwrContext"这两个问题一起处理了------直播场景里源音频参数理论上是可能变的,这里做了防御。

2.7 时间戳换算(对照"难点和复杂"文档的 bug)

cpp 复制代码
// 正确写法示例------OnBufferDecodeVideoData (:686),用的是 vstream_timebase_
int64_t pts = av_rescale_q(vid_packet->dts, vstream_timebase_, TIMEBASE_MS);

// 错误写法------ReadThreadProcess (:352,358),视频分支却用了 astream_timebase_
dts = av_rescale_q(packet->dts, astream_timebase_, TIMEBASE_MS);  // bug,应为 vstream_timebase_

同一个 av_rescale_q(int64_t ts, AVRational tb_src, AVRational tb_dst) API,在文件的两处调用点,一处用对了 timebase,一处用错了------具体分析见 难点和复杂.md 第 1 条。

2.8 收尾

cpp 复制代码
// ARFFPlayer.cpp:1014-1031 CloseFFDecode
avcodec_close(video_dec_ctx_);
avcodec_close(audio_dec_ctx_);
avformat_close_input(&fmt_ctx_);

3. 两侧对照小结

关注点 推流 ArFFWriter 拉流 ARFFPlayer
打开方式 avformat_alloc_output_context2 + 手动 avio_open2 avformat_open_input 一步到位(协议自动探测)
编解码器角色 只用来生成 extradata,不真正编码画面 真正用来解码收到的画面/声音
解码/编码 API 版本 不涉及解码 新旧 API 并存(#if 0 保留旧接口对照)
超时机制 av_dict_setstimeout/timeout 选项 额外加了 interrupt_callback 主动中断
时间戳特殊处理 FLV 分支覆盖 rescale 结果(有意为之) 存在一处 timebase 用错的 bug(无意)
协议差异化配置 按 scheme 设置 rtmp_tcurl/rtsp_transport/rtp_transport 按 scheme 设置 rtsp_transport/multiple_requests
相关推荐
EatFan11 小时前
前后端分离项目中 Token 到底应该怎么设计?Access Token + Refresh Token 实战
java·前端·后端
dsyyyyy110111 小时前
Typescript装饰器
前端·javascript·typescript
一拳不是超人12 小时前
一个没测暗色模式的 Bug,吃掉了我一半 谷歌扩展用户
前端·javascript·程序员
liangshanbo121512 小时前
React useState 函数式更新面试题整理
前端·react.js·前端框架
涛涛ing12 小时前
为什么你的页面在 Safari 上总出问题?Interop 2027 正在解决这个 20 年老毛病
前端
不一样的少年_13 小时前
WebP 压缩到底在干嘛?小白也能看懂的原理拆解
前端·后端·图片资源
问心无愧051313 小时前
ctf show web 177
前端·笔记
不一样的少年_13 小时前
PNG/JPG 如何变成 WebP?真相不是改后缀!
前端·后端·图片资源
杉氧13 小时前
RN 性能调优指南:重渲染(Re-renders)控制与长列表(FlatList)优化
android·前端·react native
一_个前端13 小时前
[JS] 一站式搞定 PDF、图片、Dom弹窗、表格的浏览器打印功能
前端