本文是我的《FFMPEG Tips》系列的第二篇文章,上篇文章《FFMPEG Tips (1) 如何打印日志》主要分享了如何利用 ffmpeg 库打印日志,而本文则主要分享一下如何利用 ffmpeg 库拿到码流的一些基本信息。


1. 码流中的哪些信息值得关注 ?


[ ] 是否包含:音频、视频

[ ] 码流的封装格式

[ ] 视频的编码格式

[ ] 音频的编码格式

[ ] 视频的分辨率、帧率、码率

[ ] 音频的采样率、位宽、通道数

[ ] 码流的总时长

[ ] 其他 Metadata 信息,如作者、日期等


2. 为什么需要拿到这些信息 ?


[ ] 码流的封装格式 -> 解封装

[ ] 音频、视频的编码格式 -> 初始化×××

[ ] 视频的分辨率、帧率、码率 -> 视频的渲染

[ ] 音频的采样率、位宽、通道数 -> 初始化音频播放器

[ ] 码流的总时长 -> 展示、拖动

[ ] 其他 Metadata 信息 -> 展示


3. 这些关键信息都藏在哪 ?


这些关键的媒体信息,被称作 “metadata”,常常记录在整个码流的开头或者结尾处,例如:wav 格式主要由 wav header 头来记录音频的采样率、通道数、位宽等关键信息;mp4 格式,则存放在 moov box 结构中;而 FLV 格式则记录在 onMetaData 中等等。


我们可以看看 FLV 格式的 onMetaData 记录的信息包含有哪些内容:



当然,并不是所有的码流都能简单地通过 "metadata" 解析出这些媒体信息,有些码流还需要通过试读、解码等一系列复杂的操作判断之后,才能准确地判断真实的媒体信息,在 ffmpeg 中,函数 avformat_find_stream_info 就是干这事的。


4. 如何从 ffmpeg 取出这些信息 ?


(1)首先打开码流,并解析“metadata”


播放器要完成的第一件事,就是 “打开码流”,然后再“ 解析码流信息”,在 ffmpeg 中,这两步任务主要通过 `avformat_open_input` 和 `avformat_find_stream_info` 函数来完成,前者负责服务器的连接和码流头部信息的拉取,后者则主要负责媒体信息的探测和分析工作,这两步的示例代码如下:


AVFormatContext*ic=avformat_alloc_context();if(avformat_open_input(&ic,url,NULL,NULL)<0){LOGE("couldnotopensource%s",url);return-1;}if(avformat_find_stream_info(ic,NULL)<0){LOGE("couldnotfindstreaminformation");return-1;}


当这两步执行成功后,媒体信息就已经成功保存在了 ffmpeg 相关的结构体成员变量中了,下一步我们看看如何拿到这些信息,为我所用。


(2)利用 ffmpeg 系统函数 dump 码流信息


ffmpeg 提供了一个函数直接帮助你打印出解析到的媒体信息,用法如下:


av_dump_format(ic,0,ic->filename,0);


例如,打印 “rtmp://live.hkstv.hk.lxdns.com/live/hks” 的结果如下:



不过,这样打印的信息还不够,我们希望能通过代码取到每一个关键的媒体信息。因此,下面我们看看如何直接从 AVFormatContext 上下文结构体中提取这些信息。


(3)手动从 ffmpeg 的上下文结构体中提取


首先,我们看看 AVFormatContext 变量有哪些跟媒体信息有关的成员变量:


-structAVInputFormat*iformat;//记录了封装格式信息-unsignedintnb_streams;//记录了该URL中包含有几路流-AVStream**streams;//一个结构体数组,每个对象记录了一路流的详细信息-int64_tstart_time;//第一帧的时间戳-int64_tduration;//码流的总时长-intbit_rate;//码流的总码率,bps-AVDictionary*metadata;//一些文件信息头,key/value字符串


由此可见,封装格式、总时长和总码率可以拿到了。另外,由于 AVStream **streams 还详细记录了每一路流的媒体信息,可以进一步挖一挖,看看它有哪些成员变量。


我们通过 av_find_best_stream 函数来取出指向特定指定路数的 AVStream 对象,比如视频流的 AVStream 和 音频流的 AVStream 对象分别通过如下方法来取到:


intvideo_stream_idx=av_find_best_stream(ic,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);AVStreamvideo_stream=ic->streams[video_stream_idx];intaudio_stream_idx=av_find_best_stream(ic,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);AVStreamaudio_stream=ic->streams[audio_stream_idx];


拿到了 video_stream 和 audio_stream ,我们就可以把 AVStream 结构体中的信息提取出来了,其关键的成员变量如下:


-AVCodecContext*codec;//记录了该码流的编码信息-int64_tstart_time;//第一帧的时间戳-int64_tduration;//该码流的时长-int64_tnb_frames;//该码流的总帧数-AVDictionary*metadata;//一些文件信息头,key/value字符串-AVRationalavg_frame_rate;//平均帧率


到这里,我们拿到了平均的帧率,其中,AVCodecContext 详细记录了每一路流的具体的编码信息,我们再进一步挖一挖,看看 AVCodecContext 有哪些成员变量。


-conststructAVCodec*codec;//编码的详细信息-enumAVCodecIDcodec_id;//编码类型-intbit_rate;//平均码率/*videoonly*/-intwidth,height;//图像的宽高尺寸,码流中不一定存在该信息,会由解码后覆盖-enumAVPixelFormatpix_fmt;//原始图像的格式,码流中不一定存在该信息,会由解码后覆盖/*audioonly*/-intsample_rate;//音频的采样率-intchannels;//音频的通道数-enumAVSampleFormatsample_fmt;//音频的格式,位宽-intframe_size;//每个音频帧的sample个数


原来我们最关心的编码类型、图片的宽高、音频的参数藏在这里了!经过层层解析后,我们想要的媒体信息,基本上在这些结构体变量中都找到了。


5. 代码示例


我们可以尝试手动把我们找到的媒体信息都打印出来看看,代码示例如下(你也可以到我的 Github 查看源代码: https://github.com/Jhuster/clib):


#include<libavutil/log.h>#defineLOGD(format,...)av_log(NULL,AV_LOG_DEBUG,format,##__VA_ARGS__);intff_dump_stream_info(char*url){AVFormatContext*ic=avformat_alloc_context();if(avformat_open_input(&ic,url,NULL,NULL)<0){LOGD("couldnotopensource%s",url);return-1;}if(avformat_find_stream_info(ic,NULL)<0){LOGD("couldnotfindstreaminformation");return-1;}LOGD("----------dumpingstreaminfo----------");LOGD("inputformat:%s",ic->iformat->name);LOGD("nb_streams:%d",ic->nb_streams);int64_tstart_time=ic->start_time/AV_TIME_BASE;LOGD("start_time:%lld",start_time);int64_tduration=ic->duration/AV_TIME_BASE;LOGD("duration:%llds",duration);intvideo_stream_idx=av_find_best_stream(ic,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);if(video_stream_idx>=0){AVStream*video_stream=ic->streams[video_stream_idx];LOGD("videonb_frames:%lld",video_stream->nb_frames);LOGD("videocodec_id:%d",video_stream->codec->codec_id);LOGD("videocodec_name:%s",avcodec_get_name(video_stream->codec->codec_id));LOGD("videowidthxheight:%dx%d",video_stream->codec->width,video_stream->codec->height);LOGD("videopix_fmt:%d",video_stream->codec->pix_fmt);LOGD("videobitrate%lldkb/s",(int64_t)video_stream->codec->bit_rate/1000);LOGD("videoavg_frame_rate:%dfps",video_stream->avg_frame_rate.num/video_stream->avg_frame_rate.den);}intaudio_stream_idx=av_find_best_stream(ic,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);if(audio_stream_idx>=0){AVStream*audio_stream=ic->streams[audio_stream_idx];LOGD("audiocodec_id:%d",audio_stream->codec->codec_id);LOGD("audiocodec_name:%s",avcodec_get_name(audio_stream->codec->codec_id));LOGD("audiosample_rate:%d",audio_stream->codec->sample_rate);LOGD("audiochannels:%d",audio_stream->codec->channels);LOGD("audiosample_fmt:%d",audio_stream->codec->sample_fmt);LOGD("audioframe_size:%d",audio_stream->codec->frame_size);LOGD("audionb_frames:%lld",audio_stream->nb_frames);LOGD("audiobitrate%lldkb/s",(int64_t)audio_stream->codec->bit_rate/1000);}LOGD("----------dumpingstreaminfo----------");avformat_close_input(&ic);}


6. 小结


关于如何使用 FFMPEG 如何提取码流的基本信息就介绍到这儿了,文章中有不清楚的地方欢迎留言或者来信 lujun.hust@gmail.com 交流,关注我的新浪微博 @卢_俊 或者 微信公众号 @Jhuster 获取最新的文章和资讯。