首页 > 代码库 > ffmpeg实战系列——003
ffmpeg实战系列——003
Talk is cheap,Show me the code!
示例2、demuxing_decoding.c
以下例子并不完整,只列出核心数据结构和代码
static AVFormatContext *fmt_ctx = NULL;
static AVCodecContext *video_dec_ctx = NULL, *audio_dec_ctx;
static AVStream *video_stream = NULL, *audio_stream = NULL;
static int video_stream_idx = -1, audio_stream_idx = -1; static AVFrame *frame = NULL; static AVPacket pkt;
int main (int argc, char **argv) { av_register_all();
/* open input file, and allocate format context */ if (avformat_open_input(&fmt_ctx, src_filename, NULL, NULL) < 0) { }
/* retrieve stream information */ if (avformat_find_stream_info(fmt_ctx, NULL) < 0) { }
if (open_codec_context(&video_stream_idx, &video_dec_ctx, fmt_ctx, AVMEDIA_TYPE_VIDEO) >= 0) { video_stream = fmt_ctx->streams[video_stream_idx]; } }
if (open_codec_context(&audio_stream_idx, &audio_dec_ctx, fmt_ctx, AVMEDIA_TYPE_AUDIO) >= 0) { audio_stream = fmt_ctx->streams[audio_stream_idx]; }
frame = av_frame_alloc();
av_init_packet(&pkt); while (av_read_frame(fmt_ctx, &pkt) >= 0) { AVPacket orig_pkt = pkt; do { ret = decode_packet(&got_frame, 0); } while (pkt.size > 0); av_packet_unref(&orig_pkt); }
end: avcodec_free_context(&video_dec_ctx); avcodec_free_context(&audio_dec_ctx); avformat_close_input(&fmt_ctx); if (video_dst_file) fclose(video_dst_file); if (audio_dst_file) fclose(audio_dst_file); av_frame_free(&frame); av_free(video_dst_data[0]);
return ret < 0; }
static int open_codec_context(int *stream_idx, AVCodecContext **dec_ctx, AVFormatContext *fmt_ctx, enum AVMediaType type) { int ret, stream_index; AVStream *st; AVCodec *dec = NULL; AVDictionary *opts = NULL;
ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0); //返回值是一个index,通过index可以拿到对应的avstream if (ret < 0) { } else { stream_index = ret; st = fmt_ctx->streams[stream_index];
/* find decoder for the stream */ dec = avcodec_find_decoder(st->codecpar->codec_id);
/* Allocate a codec context for the decoder */ *dec_ctx = avcodec_alloc_context3(dec);
/* Copy codec parameters from input stream to output codec context */ if ((ret = avcodec_parameters_to_context(*dec_ctx, st->codecpar)) < 0) { fprintf(stderr, "Failed to copy %s codec parameters to decoder context\n", av_get_media_type_string(type)); return ret; }
/* Init the decoders, with or without reference counting */ av_dict_set(&opts, "refcounted_frames", refcount ? "1" : "0", 0); if ((ret = avcodec_open2(*dec_ctx, dec, &opts)) < 0) { fprintf(stderr, "Failed to open %s codec\n", av_get_media_type_string(type)); return ret; } *stream_idx = stream_index; }
return 0; } |
下面详细分析下这个过程:
1、avformat_open_input(&fmt_ctx, src_filename, NULL)
avformat_open_input(&fmt_ctx, src_filename, NULL, NULL) int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options) { AVFormatContext *s = *ps; int i, ret = 0; AVDictionary *tmp = NULL; ID3v2ExtraMeta *id3v2_extra_meta = NULL;
if (!s && !(s = avformat_alloc_context())) return AVERROR(ENOMEM); if ((ret = init_input(s, filename, &tmp)) < 0) goto fail; s->probe_score = ret;
avio_skip(s->pb, s->skip_initial_bytes);
/* Allocate private data. */ if (s->iformat->priv_data_size > 0) { if (!(s->priv_data = http://www.mamicode.com/av_mallocz(s->iformat->priv_data_size))) { ret = AVERROR(ENOMEM); goto fail; } if (s->iformat->priv_class) { *(const AVClass **) s->priv_data = http://www.mamicode.com/s->iformat->priv_class; av_opt_set_defaults(s->priv_data); if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0) goto fail; } }
if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header) if ((ret = s->iformat->read_header(s)) < 0) goto fail;
s->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE;
update_stream_avctx(s);
for (i = 0; i < s->nb_streams; i++) s->streams[i]->internal->orig_codec_id = s->streams[i]->codecpar->codec_id; } |
在分析之前先了解几个重要的数据结构:
——AVFormatContext
typedef struct AVFormatContext { const AVClass *av_class;
struct AVInputFormat *iformat;
struct AVOutputFormat *oformat;
/** * - muxing: set by avformat_write_header() * - demuxing: set by avformat_open_input() */ void *priv_data; //最重要的成员
/** * - demuxing: either set by the user before avformat_open_input() (then * the user must close it manually) or set by avformat_open_input(). * - muxing: set by the user before avformat_write_header(). T */ AVIOContext *pb; //最重要的成员 unsigned int nb_streams; /** * A list of all streams in the file. New streams are created with * avformat_new_stream(). * * - demuxing: streams are created by libavformat in avformat_open_input(). * If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams may also * appear in av_read_frame(). * - muxing: streams are created by the user before avformat_write_header(). * * Freed by libavformat in avformat_free_context(). */ AVStream **streams;
/** * input or output filename * * - demuxing: set by avformat_open_input() * - muxing: may be set by the caller before avformat_write_header() */ char filename[1024]; int64_t duration; int64_t bit_rate;
unsigned int packet_size; int max_delay; int64_t probesize; int64_t max_analyze_duration; const uint8_t *key; int keylen; unsigned int nb_programs; AVProgram **programs; enum AVCodecID video_codec_id; enum AVCodecID audio_codec_id; enum AVCodecID subtitle_codec_id; unsigned int max_index_size; unsigned int max_picture_buffer; unsigned int nb_chapters; AVChapter **chapters;
AVDictionary *metadata; int64_t start_time_realtime;
int fps_probe_size; int error_recognition; AVIOInterruptCB interrupt_callback; int64_t max_interleave_delta; int strict_std_compliance; int event_flags; int max_ts_probe;
int avoid_negative_ts;
int audio_preload;
int max_chunk_duration; int max_chunk_size;
int use_wallclock_as_timestamps;
int avio_flags; enum AVDurationEstimationMethod duration_estimation_method;
int64_t skip_initial_bytes; unsigned int correct_ts_overflow;
int seek2any;
int flush_packets;
int probe_score;
int format_probesize; char *codec_whitelist;
char *format_whitelist;
AVFormatInternal *internal;
int io_repositioned;
AVCodec *video_codec;
AVCodec *audio_codec;
AVCodec *subtitle_codec;
AVCodec *data_codec; int metadata_header_padding; void *opaque; av_format_control_message control_message_cb;
int64_t output_ts_offset;
uint8_t *dump_separator;
/** * Forced Data codec_id. * Demuxing: Set by user. */ enum AVCodecID data_codec_id;
char *protocol_whitelist; int (*io_open)(struct AVFormatContext *s, AVIOContext **pb, const char *url, int flags, AVDictionary **options); void (*io_close)(struct AVFormatContext *s, AVIOContext *pb); char *protocol_blacklist; int max_streams; } AVFormatContext;
我们主要关注下面几个成员: struct AVInputFormat *iformat; void *priv_data; AVIOContext *pb; AVStream **streams;
|
——if ((ret = init_input(s, filename, &tmp)) < 0)
if ((ret = init_input(s, filename, &tmp)) < 0)
/* Open input file and probe the format if necessary. */ static int init_input(AVFormatContext *s, const char *filename, AVDictionary **options) { int ret; AVProbeData pd = { filename, NULL, 0 }; int score = AVPROBE_SCORE_RETRY;
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; }
if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) || (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score)))) return score;
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0) return ret;
if (s->iformat) return 0; return av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize); }
AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max) { int score_ret; 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; }
AVInputFormat *av_probe_input_format3(AVProbeData *pd, int is_opened, int *score_ret) { AVProbeData lpd = *pd; AVInputFormat *fmt1 = NULL, *fmt; int score, score_max = 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) lpd.buf = (unsigned char *) zerobuffer;
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; }
fmt = NULL; while ((fmt1 = av_iformat_next(fmt1))) { 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 = 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; } |
——AVInputFormat
typedef struct AVInputFormat { const char *name; const char *long_name; int flags;
const char *extensions;
const struct AVCodecTag * const *codec_tag;
const AVClass *priv_class; ///< AVClass for the private context
const char *mime_type;
struct AVInputFormat *next;
int raw_codec_id;
int priv_data_size;
int (*read_probe)(AVProbeData *);
int (*read_header)(struct AVFormatContext *);
int (*read_packet)(struct AVFormatContext *, AVPacket *pkt);
int (*read_close)(struct AVFormatContext *);
int (*read_seek)(struct AVFormatContext *, int stream_index, int64_t timestamp, int flags);
int64_t (*read_timestamp)(struct AVFormatContext *s, int stream_index, int64_t *pos, int64_t pos_limit);
int (*read_play)(struct AVFormatContext *);
int (*read_pause)(struct AVFormatContext *);
int (*read_seek2)(struct AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags);
int (*get_device_list)(struct AVFormatContext *s, struct AVDeviceInfoList *device_list);
int (*create_device_capabilities)(struct AVFormatContext *s, struct AVDeviceCapabilitiesQuery *caps);
int (*free_device_capabilities)(struct AVFormatContext *s, struct AVDeviceCapabilitiesQuery *caps); } AVInputFormat;
下面来看一种具体的AVInputFormat: AVInputFormat ff_mov_demuxer = { .name = "mov,mp4,m4a,3gp,3g2,mj2", .long_name = NULL_IF_CONFIG_SMALL("QuickTime / MOV"), .priv_class = &mov_class, .priv_data_size = sizeof(MOVContext), .extensions = "mov,mp4,m4a,3gp,3g2,mj2", .read_probe = mov_probe, .read_header = mov_read_header, .read_packet = mov_read_packet, .read_close = mov_read_close, .read_seek = mov_read_seek, .flags = AVFMT_NO_BYTE_SEEK, }; 重点关注:MOVContext这个结构体,在mov_read_header时被初始化。只要你要用ff_mov_demuxer干活,必须要有MOVContext实例,所有的mov上下文信息都来自于MOVContext。 而实际上:AVFormatContext的priv_data成员就是一个MOVContext /* Allocate private data. */ if (s->iformat->priv_data_size > 0) { if (!(s->priv_data = av_mallocz(s->iformat->priv_data_size))) { ret = AVERROR(ENOMEM); goto fail; } if (s->iformat->priv_class) { *(const AVClass **) s->priv_data = http://www.mamicode.com/s->iformat->priv_class; av_opt_set_defaults(s->priv_data); if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0) goto fail; } } |
——如果没有识别到具体的AVInputFormat,那么就需要打开这个文件读取一些数据进行进一步分析:一般都是走这个flow:
ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options))
先来看看这个接口是在哪里赋值的: AVFormatContext *avformat_alloc_context(void) { AVFormatContext *ic; ic = av_malloc(sizeof(AVFormatContext)); if (!ic) return ic; avformat_get_context_defaults(ic);
ic->internal = av_mallocz(sizeof(*ic->internal)); if (!ic->internal) { avformat_free_context(ic); return NULL; } ic->internal->offset = AV_NOPTS_VALUE; ic->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE; ic->internal->shortest_end = AV_NOPTS_VALUE;
return ic; } static void avformat_get_context_defaults(AVFormatContext *s) { memset(s, 0, sizeof(AVFormatContext));
s->av_class = &av_format_context_class;
s->io_open = io_open_default; s->io_close = io_close_default;
av_opt_set_defaults(s); } static int io_open_default(AVFormatContext *s, AVIOContext **pb, const char *url, int flags, AVDictionary **options) { #if FF_API_OLD_OPEN_CALLBACKS FF_DISABLE_DEPRECATION_WARNINGS if (s->open_cb) return s->open_cb(s, pb, url, flags, &s->interrupt_callback, options); FF_ENABLE_DEPRECATION_WARNINGS #endif
return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options, s->protocol_whitelist, s->protocol_blacklist); }
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; int err;
err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL); if (err < 0) return err; err = ffio_fdopen(s, h); if (err < 0) { ffurl_close(h); return err; } return 0; } 这个两个也非常重要。开始吧: 第一个API:先分配并初始化一个URLContext,这个非常重要 URLContext *h; int err; err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL); 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) { int ret = ffurl_alloc(puc, filename, flags, int_cb);
ret = ffurl_connect(*puc, options); }
//这里就是先通过文件名找到URLProtocol,然后再用URLProtocol去分配和初始化URLContext 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) return url_alloc_for_protocol(puc, p, filename, flags, int_cb);
return AVERROR_PROTOCOL_NOT_FOUND; } 查找的过程没什么花样,全局链表搜索: static const struct URLProtocol *url_find_protocol(const char *filename) { const URLProtocol **protocols; char proto_str[128], proto_nested[128], *ptr; protocols = ffurl_get_protocols(NULL, NULL); //获取到所有的白名单里面的协议//URLProtocol if (!protocols) return NULL; for (i = 0; protocols[i]; i++) { const URLProtocol *up = protocols[i]; if (!strcmp(proto_str, up->name)) { av_freep(&protocols); return up; } if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME && !strcmp(proto_nested, up->name)) { av_freep(&protocols); return up; } } av_freep(&protocols); return NULL; } 我们这里举一个实际的例子: const URLProtocol ff_file_protocol = { .name = "file", .url_open = file_open, .url_read = file_read, .url_write = file_write, .url_seek = file_seek, .url_close = file_close, .url_get_file_handle = file_get_handle, .url_check = file_check, .url_delete = file_delete, .url_move = file_move, .priv_data_size = sizeof(FileContext), //密切关注这个结构体 .priv_data_class = &file_class, .url_open_dir = file_open_dir, .url_read_dir = file_read_dir, .url_close_dir = file_close_dir, .default_whitelist = "file,crypto" }; 如果你需要调用 ff_file_protocol干活,那么FileContext是必须要提供的,所有的信息都来源于FileContext
下面通过找到的协议分配URLContext:主要是做些初始化,这里最重点强调:URLContext有一个成员是FileContext,通过这个成员uc->priv_data,才得以调用ff_file_protocol url_alloc_for_protocol(puc, p, filename, flags, int_cb); static int url_alloc_for_protocol(URLContext **puc, const URLProtocol *up, const char *filename, int flags, const AVIOInterruptCB *int_cb) { URLContext *uc; int err;
#if CONFIG_NETWORK if (up->flags & URL_PROTOCOL_FLAG_NETWORK && !ff_network_init()) return AVERROR(EIO); #endif if ((flags & AVIO_FLAG_READ) && !up->url_read) { av_log(NULL, AV_LOG_ERROR, "Impossible to open the ‘%s‘ protocol for reading\n", up->name); return AVERROR(EIO); } if ((flags & AVIO_FLAG_WRITE) && !up->url_write) { av_log(NULL, AV_LOG_ERROR, "Impossible to open the ‘%s‘ protocol for writing\n", up->name); return AVERROR(EIO); } uc = av_mallocz(sizeof(URLContext) + strlen(filename) + 1); if (!uc) { err = AVERROR(ENOMEM); goto fail; } uc->av_class = &ffurl_context_class; uc->filename = (char *)&uc[1]; strcpy(uc->filename, filename); uc->prot = up; //这里将URLProtocol赋值给URLContext的prot uc->flags = flags; uc->is_streamed = 0; /* default = not streamed */ uc->max_packet_size = 0; /* default: stream file */ if (up->priv_data_size) { uc->priv_data = http://www.mamicode.com/av_mallocz(up->priv_data_size); //最重要的数据结构 if (!uc->priv_data) { err = AVERROR(ENOMEM); goto fail; } if (up->priv_data_class) { int proto_len= strlen(up->name); char *start = strchr(uc->filename, ‘,‘); *(const AVClass **)uc->priv_data = http://www.mamicode.com/up->priv_data_class; av_opt_set_defaults(uc->priv_data); if(!strncmp(up->name, uc->filename, proto_len) && uc->filename + proto_len == start){ int ret= 0; char *p= start; char sep= *++p; char *key, *val; p++;
if (strcmp(up->name, "subfile")) ret = AVERROR(EINVAL);
while(ret >= 0 && (key= strchr(p, sep)) && p<key && (val = strchr(key+1, sep))){ *val= *key= 0; if (strcmp(p, "start") && strcmp(p, "end")) { ret = AVERROR_OPTION_NOT_FOUND; } else ret= av_opt_set(uc->priv_data, p, key+1, 0); if (ret == AVERROR_OPTION_NOT_FOUND) av_log(uc, AV_LOG_ERROR, "Key ‘%s‘ not found.\n", p); *val= *key= sep; p= val+1; } if(ret<0 || p!=key){ av_log(uc, AV_LOG_ERROR, "Error parsing options string %s\n", start); av_freep(&uc->priv_data); av_freep(&uc); err = AVERROR(EINVAL); goto fail; } memmove(start, key+1, strlen(key)); } } } if (int_cb) uc->interrupt_callback = *int_cb;
*puc = uc; return 0; fail: *puc = NULL; if (uc) av_freep(&uc->priv_data); av_freep(&uc); #if CONFIG_NETWORK if (up->flags & URL_PROTOCOL_FLAG_NETWORK) ff_network_close(); #endif return err; }
第二个API分析:err = ffio_fdopen(s, h); 其中:两个参数类型分别是: AVIOContext **s URLContext *h; 这里主要是将AVIOContext和URLContext进行绑定,同时初始化AVIOContext int ffio_fdopen(AVIOContext **s, URLContext *h) { AVIOInternal *internal = NULL; //其实就是一个URLContext,只是内部使用 //这里插一句: typedef struct AVIOInternal { URLContext *h; } AVIOInternal;
uint8_t *buffer = NULL; int buffer_size, max_packet_size;
max_packet_size = h->max_packet_size; if (max_packet_size) { buffer_size = max_packet_size; /* no need to bufferize more than one packet */ } else { buffer_size = IO_BUFFER_SIZE; } buffer = av_malloc(buffer_size); if (!buffer) return AVERROR(ENOMEM);
internal = av_mallocz(sizeof(*internal)); if (!internal) goto fail;
internal->h = h; //将URLContext赋值给AVIOInternal
//分配AVIOContext,传入三个函数指针: *s = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE, internal, io_read_packet, io_write_packet, io_seek); if (!*s) goto fail;
(*s)->protocol_whitelist = av_strdup(h->protocol_whitelist); if (!(*s)->protocol_whitelist && h->protocol_whitelist) { avio_closep(s); goto fail; } (*s)->protocol_blacklist = av_strdup(h->protocol_blacklist); if (!(*s)->protocol_blacklist && h->protocol_blacklist) { avio_closep(s); goto fail; } (*s)->direct = h->flags & AVIO_FLAG_DIRECT;
(*s)->seekable = h->is_streamed ? 0 : AVIO_SEEKABLE_NORMAL; (*s)->max_packet_size = max_packet_size; if(h->prot) { (*s)->read_pause = io_read_pause; (*s)->read_seek = io_read_seek;
if (h->prot->url_read_seek) (*s)->seekable |= AVIO_SEEKABLE_TIME; } (*s)->short_seek_get = io_short_seek; (*s)->av_class = &ff_avio_class; return 0; fail: av_freep(&internal); av_freep(&buffer); return AVERROR(ENOMEM); } 这里先别急,来看看这几个函数指针到底是什么鬼: io_read_packet io_write_packet, io_seek, io_read_pause, io_read_seek, io_short_seek 揭开庐山真面目: static int io_read_packet(void *opaque, uint8_t *buf, int buf_size) { AVIOInternal *internal = opaque; return ffurl_read(internal->h, buf, buf_size); } 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); //好像看出什么来了,莫不是URLProtocol的url_read()函数? } 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); //卧槽,居然真的是URLProtocol的函数 if (ret == AVERROR(EINTR)) continue; if (h->flags & AVIO_FLAG_NONBLOCK) return ret; return len; } 假如是文件类型: const URLProtocol ff_file_protocol = { .name = "file", .url_open = file_open, .url_read = file_read, .url_write = file_write, .url_seek = file_seek, .url_close = file_close, .url_get_file_handle = file_get_handle, .url_check = file_check, .url_delete = file_delete, .url_move = file_move, .priv_data_size = sizeof(FileContext), .priv_data_class = &file_class, .url_open_dir = file_open_dir, .url_read_dir = file_read_dir, .url_close_dir = file_close_dir, .default_whitelist = "file,crypto" }; 那就是最终调到file_read()函数。
下面继续分析:密切关注这三个函数指针的去向哈,还有URLContext的去向 //分配AVIOContext,传入三个函数指针: *s = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE, internal, io_read_packet, io_write_packet, io_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, uint8_t *buf, int buf_size), int64_t (*seek)(void *opaque, int64_t offset, int whence)) { AVIOContext *s = av_mallocz(sizeof(AVIOContext)); if (!s) return NULL; ffio_init_context(s, buffer, buffer_size, write_flag, opaque, read_packet, write_packet, seek); return s; } int ffio_init_context(AVIOContext *s, 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, uint8_t *buf, int buf_size), int64_t (*seek)(void *opaque, int64_t offset, int whence)) { s->buffer = buffer; s->orig_buffer_size = s->buffer_size = buffer_size; s->buf_ptr = buffer; s->opaque = opaque; //URLContext对象到了这里,叼 s->direct = 0;
url_resetbuf(s, write_flag ? AVIO_FLAG_WRITE : AVIO_FLAG_READ);
s->write_packet = write_packet; //三个函数指针到了这里,叼 s->read_packet = read_packet; s->seek = seek; s->pos = 0; s->must_flush = 0; s->eof_reached = 0; s->error = 0; s->seekable = seek ? AVIO_SEEKABLE_NORMAL : 0; s->max_packet_size = 0; s->update_checksum = NULL; s->short_seek_threshold = SHORT_SEEK_THRESHOLD;
if (!read_packet && !write_flag) { s->pos = buffer_size; s->buf_end = s->buffer + buffer_size; } s->read_pause = NULL; s->read_seek = NULL;
s->write_data_type = NULL; s->ignore_boundary_point = 0; s->current_type = AVIO_DATA_MARKER_UNKNOWN; s->last_time = AV_NOPTS_VALUE; s->short_seek_get = NULL;
return 0; } 这里总结一下:AVIOContext里面最重要的两个成员:URLContext:s->opaque 以及一堆函数指针: int (*read_packet)(void *opaque, uint8_t *buf, int buf_size), int (*write_packet)(void *opaque, uint8_t *buf, int buf_size), int64_t (*seek)(void *opaque, int64_t offset, int whence)) 调用这些函数都必须通过URLContext才行,因为URLContext里面有这些函数调用需要的上下文:URLContext-〉priv_data 例如file格式: const URLProtocol ff_file_protocol = { .name = "file", .url_open = file_open, .url_read = file_read, .url_write = file_write, .url_seek = file_seek, .url_close = file_close, .url_get_file_handle = file_get_handle, .url_check = file_check, .url_delete = file_delete, .url_move = file_move, .priv_data_size = sizeof(FileContext), .priv_data_class = &file_class, .url_open_dir = file_open_dir, .url_read_dir = file_read_dir, .url_close_dir = file_close_dir, .default_whitelist = "file,crypto" }; 他就需要上下文: typedef struct FileContext { const AVClass *class; int fd; int trunc; int blocksize; int follow; #if HAVE_DIRENT_H DIR *dir; #endif } FileContext; 到此,AVIO全部分析完毕,简直透彻简单: AVIOContext —— URLContext——URLProcotol——具体某种协议格式的函数指针
至此整个AVIO_open函数分析完毕,核心如下: 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; int err; //根据filename获取URLProtocol,然后根据URLProcol初始化URLContext err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL); if (err < 0) return err; //根据URLContext初始化AVIOContext,之后AVIOContext的所有函数其实都是调URLProtocol的函数干活,实际上AVIOContext是对URLProcol函数的封装,做了错误处理和缓冲 err = ffio_fdopen(s, h); if (err < 0) { ffurl_close(h); return err; } return 0; }
好像前面漏了一点东西,现在补上去:第一个API其实还有connect的动作 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) { int ret = ffurl_alloc(puc, filename, flags, int_cb); ret = ffurl_connect(*puc, options); } 实际上很简单就是调用具体URLProtocol的open函数: int ffurl_connect(URLContext *uc, AVDictionary **options) {
err = uc->prot->url_open2 ? uc->prot->url_open2(uc, uc->filename, uc->flags, options) : uc->prot->url_open(uc, uc->filename, uc->flags); } |
到这里为止,其实init_input并没有走完,我们只是初始化了AVIOContext,但是并没有获得AVInputFormat,这个是具体的某种文件格式的Parser:
/* Open input file and probe the format if necessary. */ static int init_input(AVFormatContext *s, const char *filename, AVDictionary **options) { int ret; AVProbeData pd = { filename, NULL, 0 }; int score = AVPROBE_SCORE_RETRY;
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; }
if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) || (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score)))) return score;
//初始化AVIOContext if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0) return ret;
if (s->iformat) return 0; //初始化AVInputFormat return av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize); }
再来看看AVInputFormat的初始化过程: 其实也是简单,通过probedata去获取得分最高的AVInputFormat: int av_probe_input_buffer2(AVIOContext *pb, AVInputFormat **fmt, const char *filename, void *logctx, unsigned int offset, unsigned int max_probe_size) { 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. */ if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0) if ((ret = avio_read(pb, buf + buf_offset, probe_size - buf_offset)) < 0) {
score = 0; ret = 0; /* error was end of file, nothing read */ } buf_offset += ret; if (buf_offset < offset) continue; pd.buf_size = buf_offset - offset; pd.buf = &buf[offset];
memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE);
/* Guess file format. */ *fmt = av_probe_input_format2(&pd, 1, &score); if (*fmt) {
return ret < 0 ? ret : score; } 假如我们probe到的格式是ts: AVInputFormat ff_mpegts_demuxer = { .name = "mpegts", .long_name = NULL_IF_CONFIG_SMALL("MPEG-TS (MPEG-2 Transport Stream)"), .priv_data_size = sizeof(MpegTSContext), .read_probe = mpegts_probe, .read_header = mpegts_read_header, .read_packet = mpegts_read_packet, .read_close = mpegts_read_close, .read_timestamp = mpegts_get_dts, .flags = AVFMT_SHOW_IDS | AVFMT_TS_DISCONT, .priv_class = &mpegts_class, }; 至此init_input分析完毕: AVFormatContext两个成员都成功初始化:s->pb, &s->iformat, 一个是AVIOContext类型,一个是AVInputFormat类型 |
继续分析:avformat_open_input()函数
——ret = s->iformat->read_header(s):这个read_header()函数非常重要,不但初始化了该格式的上下文结构体如:MpegTSContext,同时还创建了该文件中包含的视频和音频流实体:
AVStream
换ts来分析: AVInputFormat ff_mpegtsraw_demuxer = { .name = "mpegtsraw", .long_name = NULL_IF_CONFIG_SMALL("raw MPEG-TS (MPEG-2 Transport Stream)"), .priv_data_size = sizeof(MpegTSContext), .read_header = mpegts_read_header, .read_packet = mpegts_raw_read_packet, .read_close = mpegts_read_close, .read_timestamp = mpegts_get_dts, .flags = AVFMT_SHOW_IDS | AVFMT_TS_DISCONT, .priv_class = &mpegtsraw_class, };
//这个API超级重要,主要功能是初始化MpegTSContext数据结构,方便后面调用AVInputFormat ff_mpegts_demuxer的函数获取上下文信息 static int mpegts_read_header(AVFormatContext *s) { MpegTSContext *ts = s->priv_data; //AVFormatContext持有MpegTSContext指针,初始化的就是AVFormatContext里面的priv_data成员 AVIOContext *pb = s->pb; uint8_t buf[8 * 1024] = {0}; int len; int64_t pos, probesize = s->probesize;
if (ffio_ensure_seekback(pb, probesize) < 0) av_log(s, AV_LOG_WARNING, "Failed to allocate buffers for seekback\n");
/* read the first 8192 bytes to get packet size */ pos = avio_tell(pb); len = avio_read(pb, buf, sizeof(buf)); ts->raw_packet_size = get_packet_size(buf, len); if (ts->raw_packet_size <= 0) { av_log(s, AV_LOG_WARNING, "Could not detect TS packet size, defaulting to non-FEC/DVHS\n"); ts->raw_packet_size = TS_PACKET_SIZE; } ts->stream = s; ts->auto_guess = 0;
if (s->iformat == &ff_mpegts_demuxer) { /* normal demux */
/* first do a scan to get all the services */ seek_back(s, pb, pos);
//这里会根据PMT创建具体的video、audio AVStream mpegts_open_section_filter(ts, SDT_PID, sdt_cb, ts, 1);
mpegts_open_section_filter(ts, PAT_PID, pat_cb, ts, 1);
handle_packets(ts, probesize / ts->raw_packet_size); /* if could not find service, enable auto_guess */ ts->auto_guess = 1; av_log(ts->stream, AV_LOG_TRACE, "tuning done\n"); s->ctx_flags |= AVFMTCTX_NOHEADER; } seek_back(s, pb, pos); return 0; } 重点讲讲这里:因为这里非常重要。如果媒体格式有header,那么在read_header()创建AVStream,如果没有header,那么在read_packet()创建AVStream 创建AVStream的函数是: AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c) { } 看一下这个函数的说明: /** * Add a new stream to a media file. * * When demuxing, it is called by the demuxer in read_header(). If the * flag AVFMTCTX_NOHEADER is set in s.ctx_flags, then it may also * be called in read_packet(). * * When muxing, should be called by the user before avformat_write_header(). * * User is required to call avcodec_close() and avformat_free_context() to * clean up the allocation by avformat_new_stream(). AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c); ###############################################################################
另外强调一点:AVInputFormat内部持有AVFormatContext指针,所以所有关于读写packet的函数,最终都是调用AVFormatContext的AVIOContext的读写函数,都是调用URLContext的URLProtocol的读写函数。 另外:Aviobuf.c是对Avio.c的读写函数封装,添加了错误检测机制和缓冲机制。 |
继续分析:avformat_open_input()剩余的部分
接下来我们关注一下这个变量:struct AVFormatInternal
在分配AVFormatContext的时候: AVFormatContext *avformat_alloc_context(void) { AVFormatContext *ic; ic = av_malloc(sizeof(AVFormatContext)); if (!ic) return ic; avformat_get_context_defaults(ic);
ic->internal = av_mallocz(sizeof(*ic->internal)); if (!ic->internal) { avformat_free_context(ic); return NULL; } ic->internal->offset = AV_NOPTS_VALUE; ic->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE; ic->internal->shortest_end = AV_NOPTS_VALUE; return ic; } 这个结构体在avformat_open_input内部还会做一些初始化: if (!s->metadata) { s->metadata = http://www.mamicode.com/s->internal->id3v2_meta; s->internal->id3v2_meta = NULL; } else if (s->internal->id3v2_meta) { int level = AV_LOG_WARNING; if (s->error_recognition & AV_EF_COMPLIANT) level = AV_LOG_ERROR; av_log(s, level, "Discarding ID3 tags because more suitable tags were found.\n"); av_dict_free(&s->internal->id3v2_meta); if (s->error_recognition & AV_EF_EXPLODE) return AVERROR_INVALIDDATA; } ff_id3v2_free_extra_meta(&id3v2_extra_meta);
if ((ret = avformat_queue_attached_pictures(s)) < 0) goto fail;
if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->pb && !s->internal->data_offset) s->internal->data_offset = avio_tell(s->pb);
s->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE; 感觉没什么内容,先略过。 |
继续分析:avformat_open_input()函数的最后一部分
update_stream_avctx(s);
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; //个人认为这个函数在第一次avformat_open_input()的时候是不会执行的,因为s->nb_streams是0,以后打开时,s->nb_streams不为0,则会更新avstream的信息 static int update_stream_avctx(AVFormatContext *s) { int i, ret; for (i = 0; i < s->nb_streams; i++) { AVStream *st = s->streams[i];
if (!st->internal->need_context_update) continue;
/* close parser, because it depends on the codec */ if (st->parser && st->internal->avctx->codec_id != st->codecpar->codec_id) { av_parser_close(st->parser); st->parser = NULL; }
/* update internal codec context, for the parser */ ret = avcodec_parameters_to_context(st->internal->avctx, st->codecpar); if (ret < 0) return ret;
#if FF_API_LAVF_AVCTX FF_DISABLE_DEPRECATION_WARNINGS /* update deprecated public codec context */ ret = avcodec_parameters_to_context(st->codec, st->codecpar); if (ret < 0) return ret; FF_ENABLE_DEPRECATION_WARNINGS #endif
st->internal->need_context_update = 0; } return 0; } |
至此:avformat_open_input()函数的全部过程分析完毕,收获很大阿。
2、avformat_find_stream_info(fmt_ctx, NULL)
这个函数貌似也挺厉害的样子,开搞:
主要是对AVFormatContext的所有AVStream成员进行初始化,可以参考雷神的博客。我们这里只重点分析主要成员的初始化:
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options) { AVStream *st; AVCodecContext *avctx;
for (i = 0; i < ic->nb_streams; i++) { const AVCodec *codec; st = ic->streams[i]; avctx = st->internal->avctx; //初始化AVStream的parser成员 st->parser = av_parser_init(st->codecpar->codec_id);
ret = avcodec_parameters_to_context(avctx, st->codecpar);
//初始化AVCodec和AVCodecContext codec = find_probe_decoder(ic, st, st->codecpar->codec_id); // Try to just open decoders, in case this is enough to get parameters. if (!has_codec_parameters(st, NULL) && st->request_probe <= 0) { if (codec && !avctx->codec) if (avcodec_open2(avctx, codec, options ? &options[i] : &thread_opt) < 0) } } //为了初始化AVCodecContext,有时候需要解码一些帧 if (st->info->found_decoder == 1) { do { err = try_decode_frame(ic, st, &empty_pkt, (options && i < orig_nb_streams) ? &options[i] : NULL); } while (err > 0 && !has_codec_parameters(st, NULL));
// close codecs which were opened in try_decode_frame() for (i = 0; i < ic->nb_streams; i++) { st = ic->streams[i]; avcodec_close(st->internal->avctx); }
/* update the stream parameters from the internal codec contexts */ for (i = 0; i < ic->nb_streams; i++) { st = ic->streams[i]; ret = avcodec_parameters_to_context(st->codec, st->codecpar); }
} 至此整个函数分析完毕:主要工作就是初始化AVStream和AVCodecContext |
这里重点看一下AVStream的数据结构:
typedef struct AVStream { int index; /**< stream index in AVFormatContext */ int id; #if FF_API_LAVF_AVCTX /** * @deprecated use the codecpar struct instead */ attribute_deprecated AVCodecContext *codec; #endif void *priv_data;
AVRational time_base;
int64_t start_time;
int64_t duration;
int64_t nb_frames; ///< number of frames in this stream if known or 0
AVRational sample_aspect_ratio;
AVDictionary *metadata; AVRational avg_frame_rate; AVPacketSideData *side_data;
int pts_wrap_bits; /**< number of bits in pts (used for wrapping control) */ int64_t first_dts; int64_t cur_dts; int64_t last_IP_pts; int last_IP_duration;
int codec_info_nb_frames;
/* av_read_frame() support */ enum AVStreamParseType need_parsing; struct AVCodecParserContext *parser;
struct AVPacketList *last_in_packet_buffer; AVProbeData probe_data; int nb_index_entries; unsigned int index_entries_allocated_size; AVRational r_frame_rate; AVStreamInternal *internal; AVCodecParameters *codecpar; } AVStream; |
3、open_codec_context()
这个函数是自己定义的,做得事情主要是初始化AVCodecContext和打开AVCodec
static int open_codec_context(int *stream_idx, AVCodecContext **dec_ctx, AVFormatContext *fmt_ctx, enum AVMediaType type) { int ret, stream_index; AVStream *st; AVCodec *dec = NULL; AVDictionary *opts = NULL;
ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0); if (ret < 0) { fprintf(stderr, "Could not find %s stream in input file ‘%s‘\n", av_get_media_type_string(type), src_filename); return ret; } else { stream_index = ret; //如果有多个streams,则通过av_find_best_stream找到最匹配stream的stream_index st = fmt_ctx->streams[stream_index];
/* find decoder for the stream */ dec = avcodec_find_decoder(st->codecpar->codec_id); if (!dec) { fprintf(stderr, "Failed to find %s codec\n", av_get_media_type_string(type)); return AVERROR(EINVAL); }
/* Allocate a codec context for the decoder */ *dec_ctx = avcodec_alloc_context3(dec); if (!*dec_ctx) { fprintf(stderr, "Failed to allocate the %s codec context\n", av_get_media_type_string(type)); return AVERROR(ENOMEM); }
/* Copy codec parameters from input stream to output codec context */ if ((ret = avcodec_parameters_to_context(*dec_ctx, st->codecpar)) < 0) { fprintf(stderr, "Failed to copy %s codec parameters to decoder context\n", av_get_media_type_string(type)); return ret; }
/* Init the decoders, with or without reference counting */ av_dict_set(&opts, "refcounted_frames", refcount ? "1" : "0", 0); if ((ret = avcodec_open2(*dec_ctx, dec, &opts)) < 0) { fprintf(stderr, "Failed to open %s codec\n", av_get_media_type_string(type)); return ret; } *stream_idx = stream_index; }
return 0; }
|
4、decode_packet()
这个函数也是个自己定义的函数,和之前分析的差不多:
static int decode_packet(int *got_frame, int cached) { int ret = 0; int decoded = pkt.size;
*got_frame = 0;
if (pkt.stream_index == video_stream_idx) { /* decode video frame */ ret = avcodec_decode_video2(video_dec_ctx, frame, got_frame, &pkt); if (ret < 0) { fprintf(stderr, "Error decoding video frame (%s)\n", av_err2str(ret)); return ret; } } 很简单,这里就不重复分析了。 |
5、资源回收
最后看一下资源释放:
avcodec_free_context(&video_dec_ctx); // AVCodecContext释放 avcodec_free_context(&audio_dec_ctx); // AVCodecContext释放 avformat_close_input(&fmt_ctx); // AVFormatContext释放 av_frame_free(&frame); // AVFrame释放 |
至此整个demuxing再解码的详细过程分析完毕:demuxing_decoding.c
附录:
新版API只有以下几种格式能用:
int (*send_frame)(AVCodecContext *avctx, const AVFrame *frame); int (*send_packet)(AVCodecContext *avctx, const AVPacket *avpkt); int (*receive_frame)(AVCodecContext *avctx, AVFrame *frame); int (*receive_packet)(AVCodecContext *avctx, AVPacket *avpkt);
#define DEFINE_CRYSTALHD_DECODER(x, X) \ static const AVClass x##_crystalhd_class = { \ .class_name = #x "_crystalhd", \ .item_name = av_default_item_name, \ .option = options, \ .version = LIBAVUTIL_VERSION_INT, \ }; \ AVCodec ff_##x##_crystalhd_decoder = { \ .name = #x "_crystalhd", \ .long_name = NULL_IF_CONFIG_SMALL("CrystalHD " #X " decoder"), \ .type = AVMEDIA_TYPE_VIDEO, \ .id = AV_CODEC_ID_##X, \ .priv_data_size = sizeof(CHDContext), \ .priv_class = &x##_crystalhd_class, \ .init = init, \ .close = uninit, \ .send_packet = crystalhd_decode_packet, \ .receive_frame = crystalhd_receive_frame, \ .flush = flush, \ .capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_AVOID_PROBING, \ .pix_fmts = (const enum AVPixelFormat[]){AV_PIX_FMT_YUYV422, AV_PIX_FMT_NONE}, \ };
#if CONFIG_H264_CRYSTALHD_DECODER DEFINE_CRYSTALHD_DECODER(h264, H264) #endif
#if CONFIG_MPEG2_CRYSTALHD_DECODER DEFINE_CRYSTALHD_DECODER(mpeg2, MPEG2VIDEO) #endif
#if CONFIG_MPEG4_CRYSTALHD_DECODER DEFINE_CRYSTALHD_DECODER(mpeg4, MPEG4) #endif
#if CONFIG_MSMPEG4_CRYSTALHD_DECODER DEFINE_CRYSTALHD_DECODER(msmpeg4, MSMPEG4V3) #endif
#if CONFIG_VC1_CRYSTALHD_DECODER DEFINE_CRYSTALHD_DECODER(vc1, VC1) #endif
#if CONFIG_WMV3_CRYSTALHD_DECODER DEFINE_CRYSTALHD_DECODER(wmv3, WMV3) #endif |
ffmpeg实战系列——003