FFmpeg 播放播放 HTTP网络流读取数据过程分析
播放 HTTP 网络流时创建 AVIOContext 的流程是一个多层次的协议栈初始化过程。本文针对ffmpeg 播放HTTP网络流做流程分析,如有错误,望各位大佬指正,感谢。
1、avformat_open_input初始化流程
avformat_open_input创建完整流程:
下面是avformat_open_input函数主要内容说明:
/*** 打开一个输入媒体文件并初始化 AVFormatContext。* * @param ps 返回的 AVFormatContext 指针* @param filename 输入文件名或 URL* @param fmt 指定的输入格式(可为 NULL)* @param options 额外的参数选项字典(可为 NULL)*/
int avformat_open_input(AVFormatContext **ps, const char *filename,const AVInputFormat *fmt, AVDictionary **options)
{AVFormatContext *s = *ps;FFFormatContext *si;AVDictionary *tmp = NULL;ID3v2ExtraMeta *id3v2_extra_meta = NULL;int ret = 0;// 1. 创建并初始化 AVFormatContext;如果调用者未分配,则默认分配一个空上下文if (!s && !(s = avformat_alloc_context()))return AVERROR(ENOMEM);si = ffformatcontext(s);// 2. 设置输入格式(如果上层指定)if (fmt)s->iformat = fmt;// 3. 拷贝调用者提供的 options 字典(tmp 将在整个流程中使用)if (options)av_dict_copy(&tmp, *options, 0);// 4. 标记是否使用了自定义 AVIOContext(如通过 s->pb 提前设置)if (s->pb)s->flags |= AVFMT_FLAG_CUSTOM_IO;// 5. 应用全局和私有参数选项(如 probe_size、format_whitelist 等)if ((ret = av_opt_set_dict(s, &tmp)) < 0)goto fail;// 6. 拷贝输入 URLif (!(s->url = av_strdup(filename ? filename : ""))) {ret = AVERROR(ENOMEM);goto fail;}// 7. 初始化输入:打开 AVIOContext,探测格式,设置 s->iformatif ((ret = init_input(s, filename, &tmp)) < 0)goto fail;s->probe_score = ret;// 8. 继承 AVIOContext 的协议白名单/黑名单(用于后续 IO 安全验证)if (!s->protocol_whitelist && s->pb && s->pb->protocol_whitelist) {s->protocol_whitelist = av_strdup(s->pb->protocol_whitelist);if (!s->protocol_whitelist)goto fail;}if (!s->protocol_blacklist && s->pb && s->pb->protocol_blacklist) {s->protocol_blacklist = av_strdup(s->pb->protocol_blacklist);if (!s->protocol_blacklist)goto fail;}// 9. 格式白名单校验(防止非期望格式被打开)if (s->format_whitelist && av_match_list(s->iformat->name, s->format_whitelist, ',') <= 0) {av_log(s, AV_LOG_ERROR, "Format not on whitelist '%s'\n", s->format_whitelist);ret = AVERROR(EINVAL);goto fail;}// 10. 跳过初始字节(如某些格式要求跳过头部)avio_skip(s->pb, s->skip_initial_bytes);// 11. 检查文件名是否符合需要带数字编号的格式(如 image%03d.jpg)if (s->iformat->flags & AVFMT_NEEDNUMBER) {if (!av_filename_number_test(filename)) {ret = AVERROR(EINVAL);goto fail;}}// 初始化时间戳和持续时间s->duration = s->start_time = AV_NOPTS_VALUE;// 12. 分配 demuxer 的私有数据区域if (ffifmt(s->iformat)->priv_data_size > 0) {s->priv_data = av_mallocz(ffifmt(s->iformat)->priv_data_size);if (!s->priv_data) {ret = AVERROR(ENOMEM);goto fail;}// 初始化私有类结构及默认参数if (s->iformat->priv_class) {*(const AVClass **) s->priv_data = s->iformat->priv_class;av_opt_set_defaults(s->priv_data);if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0)goto fail;}}// 13. 尝试读取 ID3v2 元数据(常用于 MP3 的标题、专辑、封面等)if (s->pb)ff_id3v2_read_dict(s->pb, &si->id3v2_meta, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);// 14. 调用 demuxer 的 read_header() 函数,读取流头部信息if (ffifmt(s->iformat)->read_header)if ((ret = ffifmt(s->iformat)->read_header(s)) < 0) {if (ffifmt(s->iformat)->flags_internal & FF_INFMT_FLAG_INIT_CLEANUP)goto close;goto fail;}// 成功,返回结果前的一些补充处理(略)*ps = s;return 0;close:if (ffifmt(s->iformat)->read_close)ffifmt(s->iformat)->read_close(s);
fail:ff_id3v2_free_extra_meta(&id3v2_extra_meta);av_dict_free(&tmp);if (s->pb && !(s->flags & AVFMT_FLAG_CUSTOM_IO))avio_closep(&s->pb);avformat_free_context(s);*ps = NULL;return ret;
}
1.1 avformat_alloc_context分配
AVFormatContext 使用专用的分配函数avformat_alloc_context分配数据结构并且初始化。
AVFormatContext *avformat_alloc_context(void)
{// 分配 FFFormatContext(内部包含 AVFormatContext)并清零// FFFormatContext 是包含 AVFormatContext 的私有封装结构FFFormatContext *const si = av_mallocz(sizeof(*si));AVFormatContext *s;// 如果内存分配失败,返回 NULLif (!si)return NULL;// 获取 AVFormatContext 指针(即 FFFormatContext.pub 成员)s = &si->pub;// 设置类信息(用于 log 输出和反射配置项等)s->av_class = &av_format_context_class;// 设置默认的 IO 打开函数:如果用户未手动设置 s->io_open,则使用该函数打开 IOs->io_open = io_open_default;// 设置默认的 IO 关闭函数(close2 支持 flags)s->io_close2 = io_close2_default;// 返回已分配和初始化的 AVFormatContextreturn s;
}
1.2 init_input()
init_input() 是 avformat_open_input() 的核心辅助函数之一,作用是:初始化输入文件源,并确定输入媒体格式 AVInputFormat(即解复用器 iformat)。
它负责完成:
- 打开输入(如本地文件、网络流、或已有 AVIOContext)
- 探测并设置媒体格式(如 MPEG-TS、MP4、FLV 等)
- 初始化 AVIOContext *pb
//默认的 io_open 函数是 io_open_default():
static int init_input(AVFormatContext *s, const char *filename,AVDictionary **options)
{int ret;AVProbeData pd = { filename, NULL, 0 };int score = AVPROBE_SCORE_RETRY;// 1. 已有 pb(自定义 AVIO)if (s->pb) {s->flags |= AVFMT_FLAG_CUSTOM_IO;// 1.1 子分支 A:未指定格式 → 探测格式if (!s->iformat)return av_probe_input_buffer2(s->pb, &s->iformat, filename,s, 0, s->format_probesize);else if (s->iformat->flags & AVFMT_NOFILE) // 1.2 指定了格式 & 带 AVFMT_NOFILEav_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and ""will be ignored with AVFMT_NOFILE format.\n");return 0;}// 2. 未绑定 AVIOContextif ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||(!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))return score;// 3. 打开 AVIOContext(标准流程)if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)return ret;// 4. 如果此时已知道格式 → 不再探测if (s->iformat)return 0;// 5. 探测格式return av_probe_input_buffer2(s->pb, &s->iformat, filename,s, 0, s->format_probesize);
}
1.2.1 io_open_default
读取数据协议匹配: io_open_default()
位置:libavformat/avio.c
static int io_open_default(AVFormatContext *s, AVIOContext **pb, ...)
{return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options, s->protocol_whitelist, s->protocol_blacklist);
}
1.2.1.1 ffio_open_whitelist() 匹配读取数据协议
位置:libavformat/avio.c
int ffio_open_whitelist(AVIOContext **s, const char *filename, int flags,const AVIOInterruptCB *int_cb, AVDictionary **options,const char *whitelist, const char *blacklist)
{URLContext *h;*s = NULL;// 1. 分配URLContexterr = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL);// 2. 分配AVIOContexterr = ffio_fdopen(s, h);return 0;
}
ffurl_open_whitelist内部调用ffurl_alloc进行分配URLContext。
int ffurl_open_whitelist(URLContext **puc, const char *filename, int flags,const AVIOInterruptCB *int_cb, AVDictionary **options,const char *whitelist, const char* blacklist,URLContext *parent)
{AVDictionary *tmp_opts = NULL;AVDictionaryEntry *e;// 1.ffurl_alloc 分配URLContextint ret = ffurl_alloc(puc, filename, flags, int_cb);// 2.连接URLContextret = ffurl_connect(*puc, options);
}
1.2.2.2 ffurl_alloc查找URLProtocol 和分配URLContext
ffurl_alloc内部基于路径查找对应的协议,第一步查找对应的协议,并分配URLProtocol结构,第二步基于URLProtocol分配URLContext。使用url_find_protocol找找对应的协议,并返回URLProtocol结构体。
int ffurl_alloc(URLContext **puc, const char *filename, int flags,const AVIOInterruptCB *int_cb)
{const URLProtocol *p = NULL;// 1.查找对应的协议,并返回URLProtocol结构p = url_find_protocol(filename);// 2. 分配URLContext,并给puc赋值。if (p)return url_alloc_for_protocol(puc, p, filename, flags, int_cb);*puc = NULL;return AVERROR_PROTOCOL_NOT_FOUND;
}
使用url_alloc_for_protocol分配URLContext,并将URLProtocol的对象赋值为URLContext内部的port指针。
static int url_alloc_for_protocol(URLContext **puc, const URLProtocol *up,const char *filename, int flags,const AVIOInterruptCB *int_cb)
{URLContext *uc;// 1.申请URLContextuc = av_mallocz(sizeof(URLContext) + strlen(filename) + 1);// 2.设置映射关系,并把URLProtocol设置给protuc->av_class = &url_context_class;uc->filename = (char *)&uc[1];strcpy(uc->filename, filename);// 3.将port给赋值up,为后面write_packet、read_packet 和seek函数指针赋值做准备。uc->prot = up;uc->flags = flags;uc->is_streamed = 0; /* default = not streamed */uc->max_packet_size = 0; /* default: stream file */// 3.注册回调if (int_cb)uc->interrupt_callback = *int_cb;*puc = uc;return 0;}
1.2.2.3关键创建点 ffio_fdopen 分配AVIOContext
在avio_alloc_context中进行FFIOContext分配,并调用ffio_init_context进行处理,最后返回FFIOContext内部的AVIOContext pub指针。ffio_fdopen位置:libavformat/avio_internal.h,其是分配AVIOContext,并将AVIOContext内部的IO函数指针和URLContext内部的URLProtocol *port 具体的协议函数进行绑定。实现了文件IO读取网络IO的映射。
// libavformat/aviobuf.c
int ffio_fdopen(AVIOContext **sp, URLContext *h)
{AVIOContext *s;// 1.分配AVIOContext,并且调用ffurl_read2、ffurl_write2和ffurl_seek2为IO映射做准备。*sp = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE, h,ffurl_read2, ffurl_write2, ffurl_seek2);s = *sp;// 2.设置回调函数,此时的port就是上面具体协议的URLProtocol内部的函数。if(h->prot) {s->read_pause = h->prot->url_read_pause;s->read_seek = h->prot->url_read_seek;if (h->prot->url_read_seek)s->seekable |= AVIO_SEEKABLE_TIME;}s->av_class = &ff_avio_class;return 0;
}
//其中ffurl_read2、ffurl_write2和ffurl_seek2都是接口函数。
//以ffurl_read2为例,其内部就是将传入的void* 数据转换成原来的URLContext,
//并且调用retry_transfer_wrapper
int ffurl_read2(void *urlcontext, uint8_t *buf, int size)
{URLContext *h = urlcontext;return retry_transfer_wrapper(h, buf, NULL, size, 1, 1);
}
int ffurl_write2(void *urlcontext, const uint8_t *buf, int size)
{URLContext *h = urlcontext;return retry_transfer_wrapper(h, NULL, buf, size, size, 0);
}
int64_t ffurl_seek2(void *urlcontext, int64_t pos, int whence)
{URLContext *h = urlcontext;ret = h->prot->url_seek(h, pos, whence & ~AVSEEK_FORCE);return ret;
}
static inline int retry_transfer_wrapper(URLContext *h, uint8_t *buf,const uint8_t *cbuf,int size, int size_min,int read)
{while (len < size_min) {// 1.检查回调,如果有则调用if (ff_check_interrupt(&h->interrupt_callback))return AVERROR_EXIT;// 2.URLContext内部的port就是具体协议的ff_http_protocol,// 其在 libavformat/http.c ff_http_protocol 就已经定义。// .url_open = http_open,// .url_read = http_read,// .url_write = http_write,// .url_seek = http_seek,// .url_close = http_close, 调用url_open就是调用http_open。ret = read ? h->prot->url_read (h, buf + len, size - len):h->prot->url_write(h, cbuf + len, size - len);}return len;
}
- 核心创建函数:avio_alloc_context()
- 绑定关键回调:AVIOContext *s ;
- s->write_packet -> ffurl_write → 最终调用 http_read()
- s->read_packet -> ffurl_read → 最终调用 http_read()
- s->seek -> ffurl_seek → 最终调用 http_seek()
AVIOContext *avio_alloc_context(unsigned char *buffer,int buffer_size,int write_flag,void *opaque,int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),int (*write_packet)(void *opaque, const uint8_t *buf, int buf_size),int64_t (*seek)(void *opaque, int64_t offset, int whence))
{FFIOContext *s = av_malloc(sizeof(*s));if (!s)return NULL;ffio_init_context(s, buffer, buffer_size, write_flag, opaque,read_packet, write_packet, seek);return &s->pub;
}
ffio_init_context初始化中将AVIOContext的write_packet、read_packet 和seek 进行赋值。
void ffio_init_context(FFIOContext *ctx,unsigned char *buffer,int buffer_size,int write_flag,void *opaque,int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),int (*write_packet)(void *opaque, const uint8_t *buf, int buf_size),int64_t (*seek)(void *opaque, int64_t offset, int whence))
{AVIOContext *const s = &ctx->pub;memset(ctx, 0, sizeof(*ctx));s->buffer = buffer;ctx->orig_buffer_size =s->buffer_size = buffer_size;s->buf_ptr = buffer;s->buf_ptr_max = buffer;s->opaque = opaque;s->direct = 0;// 文件IO Cotext的AVIOContext内部的// write_packet指向write_packet,read_packet指向read_packet,seek指向seeks->write_packet = write_packet;s->read_packet = read_packet;s->seek = seek;s->seekable = seek ? AVIO_SEEKABLE_NORMAL : 0;ctx->short_seek_threshold = SHORT_SEEK_THRESHOLD;
}
HTTP 协议定义:ff_http_protocol
// libavformat/http.c
const URLProtocol ff_http_protocol = {.name = "http",.url_open = http_open,.url_read = http_read,.url_write = http_write,.url_seek = http_seek,.url_close = http_close,.priv_data_size = sizeof(HTTPContext),.flags = URL_PROTOCOL_FLAG_NETWORK,.default_whitelist = "http,https,tcp,tls,crypto"
};
- http_open() 建立 TCP 连接
- http_read() 处理 HTTP 响应和数据读取
1.2.2 av_probe_input_buffer2 探测格式
从 AVIOContext(即输入源,例如文件、网络流、内存流)中读取一部分数据,根据内容自动识别对应的解复用器 AVInputFormat。
int av_probe_input_buffer2(AVIOContext *pb, const AVInputFormat **fmt,const char *filename, void *logctx,unsigned int offset, unsigned int max_probe_size)
{AVProbeData pd = { filename ? filename : "" };for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt && !eof;probe_size = FFMIN(probe_size << 1,FFMAX(max_probe_size, probe_size + 1))) {score = probe_size < max_probe_size ? AVPROBE_SCORE_RETRY : 0;// 1. avio_read() 读取数据到缓冲区用于识别if ((ret = avio_read(pb, buf + buf_offset,probe_size - buf_offset)) < 0) {/* Fail if error was not end of file, otherwise, lower score. */if (ret != AVERROR_EOF)goto fail;score = 0;ret = 0; /* error was end of file, nothing read */eof = 1;}// 2. av_probe_input_format2() 识别文件格式*fmt = av_probe_input_format2(&pd, 1, &score);}return ret < 0 ? ret : score;
}
1.2.2.1 avio_read 读取数据
avio_read() 是 FFmpeg 中 AVIOContext 的核心读取函数,用于从输入源读取原始字节流数据(例如文件、网络、内存等)。
/*** @brief 从 AVIOContext 中读取数据* * 从 AVIOContext(可能是文件、网络等)中读取 size 字节到 buf 中,支持 direct 模式、* 缓冲区读取、EOF 检测、错误返回等功能。** @param s AVIOContext 上下文* @param buf 目标缓冲区* @param size 要读取的字节数* @return 实际读取的字节数,或错误码(<0)*/
int avio_read(AVIOContext *s, unsigned char *buf, int size)
{int len, size1;size1 = size; // 保留原始请求的大小,用于计算最终读取量while (size > 0) {// 计算当前 buffer 可读数据量(可能是0)len = FFMIN(s->buf_end - s->buf_ptr, size);// 情况一:当前 buffer 无可用数据,或处于写模式if (len == 0 || s->write_flag) {// 情况 1.1:是否可直接从底层协议读取,跳过缓冲区if ((s->direct || size > s->buffer_size) &&!s->update_checksum && s->read_packet) {// 调用底层协议的 read_packet 回调函数,如 http_read, file_read 等len = read_packet_wrapper(s, buf, size);// 遇到 EOF,记录 eof 状态并退出if (len == AVERROR_EOF) {s->eof_reached = 1;break;}// 遇到错误,记录 error 并退出else if (len < 0) {s->eof_reached = 1;s->error = len;break;} else {// 成功读取 len 字节,更新读指针等状态s->pos += len;ffiocontext(s)->bytes_read += len;s->bytes_read = ffiocontext(s)->bytes_read;// 减少剩余读取大小,buf 指针前移size -= len;buf += len;// 重置 buffer 指针,清空 buffers->buf_ptr = s->buffer;s->buf_end = s->buffer;}} else {// 情况 1.2:使用内部 buffer,填充缓存区fill_buffer(s);// 如果仍无数据可读,退出(EOF 或错误)len = s->buf_end - s->buf_ptr;if (len == 0)break;}} else {// 情况二:buffer 中有数据,直接拷贝 len 字节到目标 bufmemcpy(buf, s->buf_ptr, len);buf += len;s->buf_ptr += len;size -= len;}}// 如果一次字节都没读到if (size1 == size) {if (s->error)return s->error;if (avio_feof(s))return AVERROR_EOF;}// 返回实际读取字节数return size1 - size;
}
1.2.2.2 av_probe_input_format2探测格式
av_probe_input_format2() 是 FFmpeg 中用于 根据探测数据 (AVProbeData) 推断输入媒体格式 (AVInputFormat) 的函数。
const AVInputFormat *av_probe_input_format2(const AVProbeData *pd,int is_opened, int *score_max) {int score_ret;const AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret);if (score_ret > *score_max) {*score_max = score_ret;return fmt;} elsereturn NULL;
}
const AVInputFormat *av_probe_input_format3(const AVProbeData *pd,int is_opened, int *score_ret)
{AVProbeData lpd = *pd;const AVInputFormat *fmt = NULL, *fmt1 = NULL;int score, score_max = 0;void *i = 0;const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE];if (!lpd.buf)lpd.buf = (unsigned char *) zerobuffer;// 1.遍历所有注册的输入格式while ((fmt1 = av_demuxer_iterate(&i))) {if (fmt1->flags & AVFMT_EXPERIMENTAL)continue;if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2"))continue;score = 0;if (ffifmt(fmt1)->read_probe) {// 2.如果格式有read_probe函数,则调用它// 如果read_probe函数返回非0,则认为是一个合适的格式// 如果read_probe函数返回0,则继续检查其他条件score = ffifmt(fmt1)->read_probe(&lpd);}if (score > score_max) {score_max = score;fmt = fmt1;} else if (score == score_max)fmt = NULL;}return fmt;
}
read_probe() 是每个格式自己的探测函数,例如:
- mov_probe()(用于 MP4、MOV)
- mpegts_probe()(用于 TS 流)
- flv_probe()(用于 FLV)
MP4 格式探测:
// libavformat/mov.c
static int mov_probe(const AVProbeData *p)
{// 检查 MP4 特征签名if (AV_RB32(p->buf) == MKBETAG('f','t','y','p')) {return AVPROBE_SCORE_MAX; // 最高分匹配}// 检查其他 MP4 特征if (AV_RB32(p->buf + 4) == MKBETAG('m','o','o','v') ||AV_RB32(p->buf + 4) == MKBETAG('m','d','a','t')) {return AVPROBE_SCORE_EXTENSION; // 扩展名匹配}return 0;
}
最终确定demuxer
// libavformat/mov.c
const AVInputFormat ff_mov_demuxer = {.name = "mov,mp4,m4a,3gp,3g2,mj2",.long_name = NULL_IF_CONFIG_SMALL("QuickTime / MOV"),.priv_data_size = sizeof(MOVContext),.extensions = "mov,mp4,m4a,3gp,3g2,mj2", // 包含 mp4.read_probe = mov_probe,.read_header = mov_read_header,.read_packet = mov_read_packet,.read_close = mov_read_close,.read_seek = mov_read_seek,
};
1.3 demuxer read_header
在做完前面的avformat_alloc_context和init_input(io_open_default、av_probe_input_buffer2) 之后,调用demuxer 读取read_header接口。
int avformat_open_input(AVFormatContext **ps, const char *filename,const AVInputFormat *fmt, AVDictionary **options)
{//分配 demuxer 的私有数据区域if (ffifmt(s->iformat)->priv_data_size > 0) {s->priv_data = av_mallocz(ffifmt(s->iformat)->priv_data_size);if (!s->priv_data) {ret = AVERROR(ENOMEM);goto fail;}// 初始化私有类结构及默认参数if (s->iformat->priv_class) {*(const AVClass **) s->priv_data = s->iformat->priv_class;av_opt_set_defaults(s->priv_data);if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0)goto fail;}}//调用 demuxer 的 read_header() 函数,读取流头部信息if (ffifmt(s->iformat)->read_header)if ((ret = ffifmt(s->iformat)->read_header(s)) < 0) {if (ffifmt(s->iformat)->flags_internal & FF_INFMT_FLAG_INIT_CLEANUP)goto close;goto fail;}
}
以http为例,read_header读取header分析。HTTP 的 demuxer 工作流程:
2、HTTP av_read_frame读数据流程
- av_read_frame() 调用 avio_read(s->pb, …)
- 触发 AVIOContext.read_packet() 即 ffurl_read()
- ffurl_read() 调用 URLContext.prot->url_read()
- 对于 HTTP:http_read() 执行。
- 具体流程图如下所示:
3、HTTP avio_read 数据流程图
4、HTTP 播放代码示例
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h> // 用于 usleep// 错误处理宏
#define CHECK_ERROR(ret, message) \if (ret < 0) { \char err_buf[AV_ERROR_MAX_STRING_SIZE]; \av_strerror(ret, err_buf, sizeof(err_buf)); \fprintf(stderr, "%s: %s\n", message, err_buf); \goto cleanup; \}int main(int argc, char* argv[]) {if (argc < 2) {fprintf(stderr, "Usage: %s <http-url>\n", argv[0]);return 1;}const char* url = argv[1];// 初始化变量AVFormatContext* fmt_ctx = NULL;AVCodecParameters* codecpar = NULL;const AVCodec* codec = NULL;AVCodecContext* codec_ctx = NULL;AVPacket* pkt = NULL;AVFrame* frame = NULL;AVFrame* rgb_frame = NULL;struct SwsContext* sws_ctx = NULL;int video_stream_index = -1;int ret = 0;// 1. 初始化FFmpeg网络模块avformat_network_init();// 2. 打开HTTP流fmt_ctx = avformat_alloc_context();if (!fmt_ctx) {fprintf(stderr, "Could not allocate format context\n");return 1;}// 设置HTTP选项AVDictionary* options = NULL;av_dict_set(&options, "reconnect", "1", 0); // 启用自动重连av_dict_set(&options, "reconnect_at_eof", "1", 0); // 在EOF时重连av_dict_set(&options, "reconnect_streamed", "1", 0); // 流式重连av_dict_set(&options, "reconnect_delay_max", "5", 0); // 最大重连延迟5秒av_dict_set(&options, "rw_timeout", "5000000", 0); // 5秒超时av_dict_set(&options, "user_agent", "FFmpeg/7.0", 0); // 设置User-Agent// 打开输入流ret = avformat_open_input(&fmt_ctx, url, NULL, &options);CHECK_ERROR(ret, "Could not open input");av_dict_free(&options);// 3. 获取流信息ret = avformat_find_stream_info(fmt_ctx, NULL);CHECK_ERROR(ret, "Could not find stream information");// 4. 查找视频流for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {video_stream_index = i;codecpar = fmt_ctx->streams[i]->codecpar;break;}}if (video_stream_index == -1) {fprintf(stderr, "Could not find video stream\n");goto cleanup;}// 5. 获取解码器codec = avcodec_find_decoder(codecpar->codec_id);if (!codec) {fprintf(stderr, "Unsupported codec: %d\n", codecpar->codec_id);goto cleanup;}// 6. 创建解码器上下文codec_ctx = avcodec_alloc_context3(codec);if (!codec_ctx) {fprintf(stderr, "Could not allocate codec context\n");goto cleanup;}// 复制参数到解码器上下文ret = avcodec_parameters_to_context(codec_ctx, codecpar);CHECK_ERROR(ret, "Could not copy codec parameters");// 设置低延迟解码选项AVDictionary* codec_options = NULL;av_dict_set(&codec_options, "tune", "zerolatency", 0);av_dict_set(&codec_options, "threads", "auto", 0);// 7. 打开解码器ret = avcodec_open2(codec_ctx, codec, &codec_options);CHECK_ERROR(ret, "Could not open codec");av_dict_free(&codec_options);// 8. 准备数据包和帧pkt = av_packet_alloc();frame = av_frame_alloc();if (!pkt || !frame) {fprintf(stderr, "Could not allocate packet or frame\n");goto cleanup;}// 9. 准备图像转换器sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt,codec_ctx->width, codec_ctx->height, AV_PIX_FMT_RGB24,SWS_BILINEAR, NULL, NULL, NULL);if (!sws_ctx) {fprintf(stderr, "Could not create scale context\n");goto cleanup;}// 10. 创建RGB帧rgb_frame = av_frame_alloc();if (!rgb_frame) {fprintf(stderr, "Could not allocate RGB frame\n");goto cleanup;}rgb_frame->format = AV_PIX_FMT_RGB24;rgb_frame->width = codec_ctx->width;rgb_frame->height = codec_ctx->height;ret = av_frame_get_buffer(rgb_frame, 0);CHECK_ERROR(ret, "Could not allocate RGB frame buffer");// 11. 主播放循环int frame_count = 0;int64_t last_pts = AV_NOPTS_VALUE;AVRational time_base = fmt_ctx->streams[video_stream_index]->time_base;while (1) {// 读取数据包ret = av_read_frame(fmt_ctx, pkt);if (ret < 0) {// 处理错误或文件结束if (ret == AVERROR_EOF) {printf("End of stream\n");break;}// 网络错误处理if (ret == AVERROR(EAGAIN)) continue;char err_buf[AV_ERROR_MAX_STRING_SIZE];av_strerror(ret, err_buf, sizeof(err_buf));printf("Read error: %s. Retrying...\n", err_buf);// 等待后重试usleep(100000); // 100mscontinue;}// 只处理视频流if (pkt->stream_index == video_stream_index) {// 发送数据包到解码器ret = avcodec_send_packet(codec_ctx, pkt);if (ret < 0) {av_packet_unref(pkt);continue;}// 接收解码帧while (ret >= 0) {ret = avcodec_receive_frame(codec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {break;}if (ret < 0) {break;}// 帧计数器frame_count++;printf("Frame %d (size=%dx%d)\n", frame_count, frame->width, frame->height);// 转换到RGB格式sws_scale(sws_ctx, (const uint8_t* const*)frame->data, frame->linesize,0, frame->height,rgb_frame->data, rgb_frame->linesize);// 在这里添加渲染代码// 例如:save_to_ppm(rgb_frame, frame_count);// 或者:display_frame(rgb_frame);// 简单的帧率控制if (last_pts != AV_NOPTS_VALUE) {int64_t frame_delay = av_rescale_q(frame->pts - last_pts, time_base, (AVRational){1, 1000000} // 微秒);// 确保最小延迟(例如 10ms)if (frame_delay > 10000) {usleep(frame_delay);}}last_pts = frame->pts;av_frame_unref(frame);}}av_packet_unref(pkt);}cleanup:// 12. 清理资源av_frame_free(&frame);av_frame_free(&rgb_frame);av_packet_free(&pkt);sws_freeContext(sws_ctx);avcodec_free_context(&codec_ctx);avformat_close_input(&fmt_ctx);avformat_network_deinit();return ret < 0 ? 1 : 0;
}