本文分析avformat_open_input函数,该函数在libavformat包下。
/**
* Open an input stream and read the header.
* The codecs are not opened.
* The stream must be closed with avformat_close_input().
*
* @param ps Pointer to user-supplied AVFormatContext.
* @param options A dictionary filled with AVFormatContext and demuxer-private options.
*
* @param url URL of the stream to open.
*
* @param fmt If non-NULL, this parameter forces a specific input format.
* Otherwise the format is autodetected.
*
* @return 0 on success, a negative AVERROR on failure.
* @note If you want to use custom IO, preallocate the format context and set its pb field.
*/
int avformat_open_input(AVFormatContext **ps, const char *url,
ff_const59 AVInputFormat *fmt, AVDictionary **options);
通读avformat_open_input,主要功能是:
根据传入的url确定了要使用的协议URLProtocol,比如http的或是file类型的协议;
然后按该协议打开文件或建立连接,循环从2048byte大小2的幂次递增开始读取数据,
然后再遍历所有的AVInputFormat,确定iformat,即确定是个什么格式的数据,flv啊还是mp4啥的;
调用AVInputFormat#read_header读取该格式下的头部数据,在此可能会创建AVStream,否则会在调用avformat_find_stream_info时候通过read_frame_internal创建流;
avformat_open_input函数流程图如下:
下面是各个分析各个函数。
utils.c#avformat_open_input
主要工作:
Open an input stream and read the header. The codecs are not opened. The stream must be closed with avformat_close_input().
return 0 on success, a negative AVERROR on failure.
If you want to use custom IO, preallocate the format context and set its pb field.
打开流并根据格式信息读取数据头创建相应的AVStream,但不打开解码器,返回0表示成功。
int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options)
{
AVFormatContext *s = *ps;
int i, ret = 0;
AVDictionary *tmp = NULL;
// 没赋值就赋值
if (!s && !(s = avformat_alloc_context()))
return AVERROR(ENOMEM);
// 有format就设置
if (fmt)
s->iformat = fmt;
// 在堆上分配,赋值s->url
if (!(s->url = av_strdup(filename ? filename : ""))) {
ret = AVERROR(ENOMEM);
goto fail;
}
// init_input找到文件格式format
if ((ret = init_input(s, filename, &tmp)) < 0)
goto fail;
s->probe_score = ret;
s->duration = s->start_time = AV_NOPTS_VALUE;
// 读取多媒体数据文件头,根据音视频频流创建相应的AVStream。
if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
if ((ret = s->iformat->read_header(s)) < 0)
goto fail;
for (i = 0; i < s->nb_streams; i++)
s->streams[i]->internal->orig_codec_id = s->streams[i]->codecpar->codec_id;
if (options) {
av_dict_free(options);
*options = tmp;
}
*ps = s;
return 0;
fail:
ff_id3v2_free_extra_meta(&id3v2_extra_meta);
av_dict_free(&tmp);
avformat_free_context(s);
*ps = NULL;
return ret;
}
Open input file and probe the format if necessary.
如果没有指定iformat的话,则打开文件并探测格式。
主要工作:
调用其它函数,找到iformat;
先根据filename探测一波iformat;
没找到的话打开文件读取数据进行探测;
还没找到的话再探测一波;
在任意一步找到iformat即返回给init_input,init_input会调用iformat的read_head创建相应的AVStream。
#define AVPROBE_SCORE_MAX 100
#define AVPROBE_SCORE_RETRY (AVPROBE_SCORE_MAX/4)
static int init_input(AVFormatContext *s, const char *filename,
AVDictionary **options)
{
int ret;
// 只将filename传入
AVProbeData pd = { filename, NULL, 0 };
// 初始化25分的得分
int score = AVPROBE_SCORE_RETRY;
/*
* 自定义了IO的话
* 如果没指定iformat,就调用av_probe_input_buffer2推测iformat
* 如果指定了iformat,直接返回
*/
if (s->pb) {
s->flags |= AVFMT_FLAG_CUSTOM_IO;
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)
av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and "
"will be ignored with AVFMT_NOFILE format.\n");
return 0;
}
/*
* 1. 如果指定了iformat,并且该格式是不需要读取数据的,就直接返回
* 如果没指定iformat,则通过av_probe_input_format2来推测iformat,推测成功则返回
*/
if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
(!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))
return score;
/*
* 2. 第一步失败,则通过io_open打开文件;返回0则表示打开成功,<0则失败,open_input流程结束
*/
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
return ret;
// 第二步根据URLProtocol打开了文件,此处如果外部指定了iformat则返回;
// 即iformat即使指定了,也要打开文件去读写;也说明了文件格式iformat和传输协议urlProtocol是两码事
if (s->iformat)
return 0;
/*
* 3. 第二步没找到iformat的话,再调用av_probe_input_buffer2推测
*/
return av_probe_input_buffer2(s->pb, &s->iformat, filename,
s, 0, s->format_probesize);
}
第一步:format.c#av_probe_input_format2
Guess the file format.
推测文件格式;只有推测出来的得分值 > score_max,才将该format返回。
主要工作:
调用av_probe_input_format3找到format,大于score_max则探测成功,否则探测失败。
在init_input调用中
score_max为初始值25,is_opened为false,pd该结构体只有filename。
因为该文件没有被打开过,所以只有filename这一个有效信息。
typedef struct AVProbeData {
const char *filename;
unsigned char *buf; /**< Buffer must have AVPROBE_PADDING_SIZE of extra allocated bytes filled with zero. */
int buf_size; /**< Size of buf except extra allocated bytes */
const char *mime_type; /**< mime_type, when known. */
} AVProbeData;
ff_const59 AVInputFormat *av_probe_input_format2(ff_const59 AVProbeData *pd, int is_opened, int *score_max)
{
int score_ret;
ff_const59 AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret);
if (score_ret > *score_max) {
*score_max = score_ret;
return fmt;
} else
return NULL;
}
本文福利, 免费领取C++音视频学习资料包、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
format.c#av_probe_input_format3
Guess the file format,推荐iformat,是av_probe_input_format2的具体实现。
参数score_ret是推测的值,是要和score_max对比的。
主要工作:
第一次进来时,文件没有打开,pd.buf是NULL,赋值为zerobuffer,空转?
第二遍io_open填充buf后,走到这里遍历所有格式,根据read_probe、扩展名、mime_type进行探测,如果有得分值 > 传入的score_max的话,则找到了该format; 如果没大于,则返回null。
#define AVPROBE_SCORE_EXTENSION 50 ///< score for file extension
#define AVPROBE_SCORE_MIME 75 ///< score for file mime type
#define AVPROBE_SCORE_MAX 100 ///< maximum score
ff_const59 AVInputFormat *av_probe_input_format3(ff_const59 AVProbeData *pd, int is_opened, int *score_ret)
{
AVProbeData lpd = *pd;
const AVInputFormat *fmt1 = NULL;
ff_const59 AVInputFormat *fmt = NULL;
int score, score_max = 0;
void *i = 0;
const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE]; // 局部静态
enum nodat {
NO_ID3,
ID3_ALMOST_GREATER_PROBE,
ID3_GREATER_PROBE,
ID3_GREATER_MAX_PROBE,
} nodat = NO_ID3;
if (!lpd.buf) {
// 初始化buf,32字节,内容全是0,局部静态呀
lpd.buf = (unsigned char *) zerobuffer;
}
// 判断是否是ID3v2格式,ID3是一种metadata容器,多应用于MP3格式的音频文件中。
if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) {
int id3len = ff_id3v2_tag_len(lpd.buf);
if (lpd.buf_size > id3len + 16) {
if (lpd.buf_size < 2LL*id3len + 16)
nodat = ID3_ALMOST_GREATER_PROBE;
lpd.buf += id3len;
lpd.buf_size -= id3len;
} else if (id3len >= PROBE_BUF_MAX) {
nodat = ID3_GREATER_MAX_PROBE;
} else
nodat = ID3_GREATER_PROBE;
}
/*
* demuxer_list是编译出来的;
* 遍历所有格式,根据read_probe、扩展名、mime_type进行探测,如果有得分值 > 传入的score_max的话,则找到了该format; 如果没大于,则返回null。
*
* 1. 如果有read_probe方法,调用read_probe方法进行判断,返回该format的得分,如果文件扩展名是该格式的话,再进行switch判断;
* 没有read_probe方法,扩展名匹配的话则得分是AVPROBE_SCORE_EXTENSION 50
* 2. mime_type匹配的话,则得分max(AVPROBE_SCORE_MIME75, score)
* 3. 比较score_max得分值,超过则找到,没超过则返回null。
*/
while ((fmt1 = av_demuxer_iterate(&i))) {
// 如果一个文件格式如果是AVFMT_NOFILE,对应init_input中的第一次探测的时候才能向下走,第二次文件打开了则continue;
if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2"))
continue;
score = 0;
if (fmt1->read_probe) {
score = fmt1->read_probe(&lpd);
if (score)
av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%d\n", fmt1->name, score, lpd.buf_size);
if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) {
switch (nodat) {
case NO_ID3:
score = FFMAX(score, 1);
break;
case ID3_GREATER_PROBE:
case ID3_ALMOST_GREATER_PROBE:
score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1);
break;
case ID3_GREATER_MAX_PROBE:
score = FFMAX(score, AVPROBE_SCORE_EXTENSION);
break;
}
}
} else if (fmt1->extensions) {
if (av_match_ext(lpd.filename, fmt1->extensions))
score = AVPROBE_SCORE_EXTENSION;
}
if (av_match_name(lpd.mime_type, fmt1->mime_type)) {
if (AVPROBE_SCORE_MIME > score) {
av_log(NULL, AV_LOG_DEBUG, "Probing %s score:%d increased to %d due to MIME type\n", fmt1->name, score, AVPROBE_SCORE_MIME);
score = AVPROBE_SCORE_MIME;
}
}
if (score > score_max) {
score_max = score;
fmt = (AVInputFormat*)fmt1;
} else if (score == score_max)
fmt = NULL;
}
if (nodat == ID3_GREATER_PROBE)
score_max = FFMIN(AVPROBE_SCORE_EXTENSION / 2 - 1, score_max);
*score_ret = score_max;
return fmt;
}
遍历demuxer_list,调用该格式的read_probe。
只有在打开文件后调用这才有意义,即调过io_open后才给pb.buf赋了值,否则在空转。
可以参考文件格式进行分析。
第二步:s->io_open,options.c#io_open_default
通过av_probe_input_format3遍历所有格式依然没有找到iformat时,则打开该文件读取数据再次进行探测。
AVFormatContext->io_open函数指针,赋值是在初始化时options.c#avformat_alloc_context中。
主要工作:
根据filename确定要使用的协议,然后初始化URLContext;
url_open2/url_open打开文件或建立连接,准备读取数据;
然后初始化AVIOContext;
// options.c
static int io_open_default(AVFormatContext *s, AVIOContext **pb, const char *url, int flags, AVDictionary **options)
{
av_log(s, loglevel, "Opening \'%s\' for %s\n", url, flags & AVIO_FLAG_WRITE ? "writing" : "reading");
return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options, s->protocol_whitelist, s->protocol_blacklist);
}
// aviobuf.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;
// 初始化URLContext
ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL);
// 初始化AVIOContext,会初始化读取的buffer大小,默认32768字节长
ffio_fdopen(s, h);
return 0;
}
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) {
ffurl_alloc(puc, filename, flags, int_cb);
ffurl_connect(*puc, options);
return 0;
}
int ffurl_alloc(URLContext **puc, const char *filename, int flags, const AVIOInterruptCB *int_cb) {
const URLProtocol *p = NULL;
p = url_find_protocol(filename);
if (p)
// 初始化URLContext,进行赋值
return url_alloc_for_protocol(puc, p, filename, flags, int_cb);
}
/*
* 根据相应的协议,调用协议的open函数,创建连接或者打开文件;
* 可以以http.c为例梳理,其中会绕会到tcp.c在打开socket后读取http response
*/
int ffurl_connect(URLContext *uc, AVDictionary **options) {
int err =
uc->prot->url_open2 ? uc->prot->url_open2(uc, uc->filename, uc->flags, options) :
uc->prot->url_open(uc, uc->filename, uc->flags);
if (err)
return err;
uc->is_connected = 1;
// 无法seek则设置is_streamed为true,表示no seek possible
if ((uc->flags & AVIO_FLAG_WRITE) || !strcmp(uc->prot->name, "file"))
if (!uc->is_streamed && ffurl_seek(uc, 0, SEEK_SET) < 0)
uc->is_streamed = 1;
return 0;
}
static const struct URLProtocol *url_find_protocol(const char *filename)
{
const URLProtocol **protocols;
char proto_str[128], proto_nested[128], *ptr;
// 查找第一个非字母数字的字符下标
size_t proto_len = strspn(filename, URL_SCHEME_CHARS);
if (filename[proto_len] != ':' &&
(strncmp(filename, "subfile,", 8) || !strchr(filename + proto_len + 1, ':')) ||
is_dos_path(filename)){
/*
* 文件路径,比如/storage/emulated/0/Android/data/com.baiiu.example/cache/test.mp4
* 则proto_str是file
*/
strcpy(proto_str, "file");
} else {
/*
* http://,rtmp:// 这样的,则proto_str是http、rtmp
*/
av_strlcpy(proto_str, filename,
FFMIN(proto_len + 1, sizeof(proto_str)));
}
// 把proto_str赋值给proto_nested,嵌套的如hls+http:// m3u8格式的
av_strlcpy(proto_nested, proto_str, sizeof(proto_nested));
if ((ptr = strchr(proto_nested, '+')))
*ptr = '\0';
// 获取所有支持的protocols
protocols = ffurl_get_protocols(NULL, NULL);
if (!protocols)
return NULL;
int i;
for (i = 0; protocols[i]; i++) {
const URLProtocol *up = protocols[i];
// 协议相同
if (!strcmp(proto_str, up->name)) {
av_freep(&protocols);
return up;
}
// nested协议相同
if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME &&
!strcmp(proto_nested, up->name)) {
av_freep(&protocols);
return up;
}
}
av_freep(&protocols);
if (av_strstart(filename, "https:", NULL) || av_strstart(filename, "tls:", NULL))
av_log(NULL, AV_LOG_WARNING, "https protocol not found, recompile FFmpeg with "
"openssl, gnutls or securetransport enabled.\n");
return NULL;
}
// 差不多这些协议吧,通过extern定义,是在别的文件内定义的
// https协议相关,从http->tls->tcp
extern const URLProtocol ff_http_protocol;
extern const URLProtocol ff_https_protocol;
extern const URLProtocol ff_tls_openssl_protocol;
extern const URLProtocol ff_tcp_protocol;
extern const URLProtocol ff_file_protocol;
extern const URLProtocol ff_ftp_protocol;
extern const URLProtocol ff_hls_protocol;
extern const URLProtocol ff_httpproxy_protocol;
extern const URLProtocol ff_rtp_protocol;
extern const URLProtocol ff_tls_protocol;
extern const URLProtocol ff_udp_protocol;
extern const URLProtocol ff_udplite_protocol;
extern const URLProtocol ff_librtmp_protocol;
extern const URLProtocol ff_librtmpe_protocol;
extern const URLProtocol ff_librtmps_protocol;
extern const URLProtocol ff_librtmpt_protocol;
extern const URLProtocol ff_librtmpte_protocol;
const URLProtocol **ffurl_get_protocols(const char *whitelist, const char *blacklist) {
const URLProtocol **ret;
int i, ret_idx = 0;
ret = av_mallocz_array(FF_ARRAY_ELEMS(url_protocols), sizeof(*ret));
if (!ret)
return NULL;
for (i = 0; url_protocols[i]; i++) {
const URLProtocol *up = url_protocols[i];
// 黑白名单过滤...
ret[ret_idx++] = up;
}
return ret;
}
初始化AVIOContext。
retry_transfer_wrapper是对ffurl_read、ffurl_write、ffurl_seek方法的封装,读取size长度的流数据。
int ffio_fdopen(AVIOContext **s, URLContext *h) {
*s = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE, h,
(int (*)(void *, uint8_t *, int)) ffurl_read,
(int (*)(void *, uint8_t *, int)) ffurl_write,
(int64_t (*)(void *, int64_t, int))ffurl_seek);
return 0;
}
// avio.c
int ffurl_read(URLContext *h, unsigned char *buf, int size)
{
if (!(h->flags & AVIO_FLAG_READ))
return AVERROR(EIO);
return retry_transfer_wrapper(h, buf, size, 1, h->prot->url_read);
}
int ffurl_read_complete(URLContext *h, unsigned char *buf, int size)
{
if (!(h->flags & AVIO_FLAG_READ))
return AVERROR(EIO);
return retry_transfer_wrapper(h, buf, size, size, h->prot->url_read);
}
int ffurl_write(URLContext *h, const unsigned char *buf, int size)
{
if (!(h->flags & AVIO_FLAG_WRITE))
return AVERROR(EIO);
/* avoid sending too big packets */
if (h->max_packet_size && size > h->max_packet_size)
return AVERROR(EIO);
return retry_transfer_wrapper(h, (unsigned char *)buf, size, size,
(int (*)(struct URLContext *, uint8_t *, int))
h->prot->url_write);
}
static inline int retry_transfer_wrapper(URLContext *h, uint8_t *buf,
int size, int size_min,
int (*transfer_func)(URLContext *h,
uint8_t *buf,
int size))
{
int ret, len;
int fast_retries = 5;
int64_t wait_since = 0;
len = 0;
while (len < size_min) {
if (ff_check_interrupt(&h->interrupt_callback))
return AVERROR_EXIT;
ret = transfer_func(h, buf + len, size - len);
if (ret == AVERROR(EINTR))
continue;
if (h->flags & AVIO_FLAG_NONBLOCK)
return ret;
if (ret == AVERROR(EAGAIN)) {
ret = 0;
if (fast_retries) {
fast_retries--;
} else {
if (h->rw_timeout) {
if (!wait_since)
wait_since = av_gettime_relative();
else if (av_gettime_relative() > wait_since + h->rw_timeout)
return AVERROR(EIO);
}
av_usleep(1000);
}
} else if (ret == AVERROR_EOF)
return (len > 0) ? len : AVERROR_EOF;
else if (ret < 0)
return ret;
if (ret) {
fast_retries = FFMAX(fast_retries, 2);
wait_since = 0;
}
len += ret;
}
return len;
}
主要工作:
通过io_open打开文件后,循环按该协议进行读取数据,从最小2048byte开始读取,不断扩充buf,直到探测到格式或达到1M为止。
填充pb.buf后,调用av_probe_input_format2继续探测iformat。此时buf的数据已经有值,便可以按照各个Inputformat确定score。
int av_probe_input_buffer2(AVIOContext *pb, ff_const59 AVInputFormat **fmt,
const char *filename, void *logctx,
unsigned int offset, unsigned int max_probe_size)
{
AVProbeData pd = { filename ? filename : "" };
uint8_t *buf = NULL;
int ret = 0, probe_size, buf_offset = 0;
int score = 0;
int ret2;
// 默认的PROBE_BUF_MAX,1<<20 byte,1M
if (!max_probe_size) {
max_probe_size = PROBE_BUF_MAX;
} else if (max_probe_size < PROBE_BUF_MIN) {
return AVERROR(EINVAL);
}
if (offset >= max_probe_size)
return AVERROR(EINVAL);
/*
* 从PROBE_BUF_MIN 2048 byte开始,逐渐 *2扩大到max_probe_size,最大到1M+1 byte,
* 直到探测出iformat为止、或者直到最大也没探测出来,即探测失败;
*/
for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt;
probe_size = FFMIN(probe_size << 1,
FFMAX(max_probe_size, probe_size + 1))) {
score = probe_size < max_probe_size ? AVPROBE_SCORE_RETRY : 0;
/*
* Read probe data.
* 1. 重新分配buf大小,当前的probe_size + AVPROBE_PADDING_SIZE
* 2. 根据io_open确定的protocol协议进行读取数据
* 用offset来控制读了多少,从哪开始读,不用重新读取
*/
if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0)
goto fail;
if ((ret = avio_read(pb, buf + buf_offset, probe_size - buf_offset)) < 0) {
if (ret != AVERROR_EOF)
goto fail;
// 重置
score = 0;
ret = 0; /* error was end of file, nothing read */
}
buf_offset += ret; // 增加offset
if (buf_offset < offset)
continue;
pd.buf_size = buf_offset - offset;
pd.buf = &buf[offset]; // 赋值pb.buf
memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE);
/*
* Guess file format. 再次探测格式,is_opened为1,文件已经打开了
*/
*fmt = av_probe_input_format2(&pd, 1, &score);
}
if (!*fmt)
ret = AVERROR_INVALIDDATA;
fail:
/* Rewind. Reuse probe buffer to avoid seeking. */
ret2 = ffio_rewind_with_probe_data(pb, &buf, buf_offset);
if (ret >= 0)
ret = ret2;
av_freep(&pd.mime_type);
return ret < 0 ? ret : score;
}
主要工作
avio_read通过AVIOContext#read_packet读取数据,循环读取到size大小后返回。
AVIOContext#read_packet,是在ffio_fdopen函数中对AVIOContext进行初始化。
实际调用在avio.c#ffurl_read。
ffurl_read方法实际上在调用相应prototol的url_read。
参考上面ffurl_read解释,最终会调到protocol里的url_read调用相应的protocol进行数据读取。
// aviobuf.c
int avio_read(AVIOContext *s, unsigned char *buf, int size)
{
int len, size1;
size1 = size;
while (size > 0) {
len = FFMIN(s->buf_end - s->buf_ptr, size);
if (len == 0 || s->write_flag) {
if((s->direct || size > s->buffer_size) && !s->update_checksum) {
len = read_packet_wrapper(s, buf, size);
if (len == AVERROR_EOF) {
s->eof_reached = 1;
break;
} else if (len < 0) {
s->eof_reached = 1;
s->error= len;
break;
} else {
s->pos += len;
s->bytes_read += len;
size -= len;
buf += len;
// reset the buffer
s->buf_ptr = s->buffer;
s->buf_end = s->buffer/* + len*/;
}
} else {
fill_buffer(s);
len = s->buf_end - s->buf_ptr;
if (len == 0)
break;
}
} else {
memcpy(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;
}
static int read_packet_wrapper(AVIOContext *s, uint8_t *buf, int size) {
return s->read_packet(s->opaque, buf, size);
}
本文福利, 免费领取C++音视频学习资料包、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓