利用FFmpeg解析音视频流,音视频流能够来自一个标准的RTMP的URL或者是一个文件. 经过解析获得音视频流,进一步就能够解码, 而后视频渲染在屏幕上,音频经过扬声器输出.ios
利用FFmpeg框架中libavformat模块能够经过函数av_read_frame
解析出音视频流的音视频数据,若是直接使用FFmpeg硬解,仅须要解析到AVPacket便可传给解码模块使用,若是使用VideoToolbox中的硬解, 对于视频数据,还须要获取其NALU Header中的(vps)sps, pps以便后续使用.git
使用流程github
- (instancetype)initWithPath:(NSString *)path;
startParseWithCompletionHandler
startParseWithCompletionHandler
方法中的Block获取解析后的音视频数据.FFmpeg parse流程数组
avformat_alloc_context
avformat_open_input
avformat_find_stream_info
formatContext->streams[i]->codecpar->codec_type == (isVideoStream ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO)
m_formatContext->streams[m_audioStreamIndex]
av_read_frame
av_bitstream_filter_filter
下面的连接中包含搭建iOS须要的FFmpeg环境的详细步骤,须要的能够提早阅读.bash
iOS编译FFmpeg数据结构
导入FFmpeg框架后,首先须要将用到FFmpeg的文件更名为.mm
, 由于涉及C,C++混编,因此须要更改文件名app
而后在头文件中导入FFmpeg头文件.框架
// FFmpeg Header File
#ifdef __cplusplus
extern "C" {
#endif
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavutil/opt.h"
#ifdef __cplusplus
};
#endif
复制代码
注意: FFmpeg是一个广为流传的框架,其结构复杂,通常导入都按照如上格式,以文件夹名为根目录进行导入,具体设置,请参考上文连接.ide
void av_register_all(void);
初始化libavformat并注册全部muxers,demuxers与协议。若是不调用此功能,则能够选择一个特定想要支持的格式。通常在程序中的main函数或是主程序启动的代理方法- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
中初始化FFmpeg,执行一次便可.函数
av_register_all();
复制代码
avformat_alloc_context()
: 初始化avformat上下文对象.int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options)
函数
fmt
: 若是非空表示强制指定一个输入流的格式, 设置为空会自动选择.int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
:读取媒体文件的数据包以获取流信息- (AVFormatContext *)createFormatContextbyFilePath:(NSString *)filePath {
if (filePath == nil) {
log4cplus_error(kModuleName, "%s: file path is NULL",__func__);
return NULL;
}
AVFormatContext *formatContext = NULL;
AVDictionary *opts = NULL;
av_dict_set(&opts, "timeout", "1000000", 0);//设置超时1秒
formatContext = avformat_alloc_context();
BOOL isSuccess = avformat_open_input(&formatContext, [filePath cStringUsingEncoding:NSUTF8StringEncoding], NULL, &opts) < 0 ? NO : YES;
av_dict_free(&opts);
if (!isSuccess) {
if (formatContext) {
avformat_free_context(formatContext);
}
return NULL;
}
if (avformat_find_stream_info(formatContext, NULL) < 0) {
avformat_close_input(&formatContext);
return NULL;
}
return formatContext;
}
复制代码
经过遍历format context对象能够从nb_streams
数组中找到音频或视频流索引,以便后续使用.
注意: 后面代码中仅须要知道音频,视频的索引就能够快速读取到format context对象中对应流的信息.
- (int)getAVStreamIndexWithFormatContext:(AVFormatContext *)formatContext isVideoStream:(BOOL)isVideoStream {
int avStreamIndex = -1;
for (int i = 0; i < formatContext->nb_streams; i++) {
if ((isVideoStream ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO) == formatContext->streams[i]->codecpar->codec_type) {
avStreamIndex = i;
}
}
if (avStreamIndex == -1) {
log4cplus_error(kModuleName, "%s: Not find video stream",__func__);
return NULL;
}else {
return avStreamIndex;
}
}
复制代码
目前视频仅支持H264, H265编码的格式.实际过程当中,解码获得视频的旋转角度多是不一样的,以及不一样机型能够支持的解码文件格式也是不一样的,因此能够用这个方法手动过滤一些不支持的状况.具体请下载代码观看,这里仅列出实战中测试出支持的列表.
/*
各机型支持的最高分辨率和FPS组合:
iPhone 6S: 60fps -> 720P
30fps -> 4K
iPhone 7P: 60fps -> 1080p
30fps -> 4K
iPhone 8: 60fps -> 1080p
30fps -> 4K
iPhone 8P: 60fps -> 1080p
30fps -> 4K
iPhone X: 60fps -> 1080p
30fps -> 4K
iPhone XS: 60fps -> 1080p
30fps -> 4K
*/
复制代码
音频本例中仅支持AAC格式.其余格式可根据需求自行更改.
使用AVPacket这个结构体来存储压缩数据.对于视频而言, 它一般包含一个压缩帧,对音频而言,可能包含多个压缩帧,该结构体类型经过av_malloc()
函数分配内存,经过av_packet_ref()
函数拷贝,经过av_packet_unref().
函数释放内存.
AVPacket packet;
av_init_packet(&packet);
复制代码
解析数据
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
: 此函数返回存储在文件中的内容,而且不验证解码器的有效帧是什么。它会将存储在文件中的内容分红帧,并为每次调用返回一个。它不会在有效帧之间省略无效数据,以便为解码器提供解码时可能的最大信息。
int size = av_read_frame(formatContext, &packet);
if (size < 0 || packet.size < 0) {
handler(YES, YES, NULL, NULL);
log4cplus_error(kModuleName, "%s: Parse finish",__func__);
break;
}
复制代码
获取sps, pps等NALU Header信息
经过调用av_bitstream_filter_filter
能够从码流中过滤获得sps, pps等NALU Header信息.
av_bitstream_filter_init: 经过给定的比特流过滤器名词建立并初始化一个比特流过滤器上下文.
av_bitstream_filter_filter: 此函数经过过滤buf
参数中的数据,将过滤后的数据放在poutbuf
参数中.输出的buffer必须被调用者释放.
此函数使用buf_size大小过滤缓冲区buf,并将过滤后的缓冲区放在poutbuf指向的缓冲区中。
attribute_deprecated int av_bitstream_filter_filter ( AVBitStreamFilterContext * bsfc,
AVCodecContext * avctx,
const char * args, // filter 配置参数
uint8_t ** poutbuf, // 过滤后的数据
int * poutbuf_size, // 过滤后的数据大小
const uint8_t * buf,// 提供给过滤器的原始数据
int buf_size, // 提供给过滤器的原始数据大小
int keyframe // 若是要过滤的buffer对应于关键帧分组数据,则设置为非零
)
复制代码
注意: 下面使用
new_packet
是为了解决av_bitstream_filter_filter
会产生内存泄漏的问题.每次使用完后将用new_packet
释放便可.
if (packet.stream_index == videoStreamIndex) {
static char filter_name[32];
if (formatContext->streams[videoStreamIndex]->codecpar->codec_id == AV_CODEC_ID_H264) {
strncpy(filter_name, "h264_mp4toannexb", 32);
videoInfo.videoFormat = XDXH264EncodeFormat;
} else if (formatContext->streams[videoStreamIndex]->codecpar->codec_id == AV_CODEC_ID_HEVC) {
strncpy(filter_name, "hevc_mp4toannexb", 32);
videoInfo.videoFormat = XDXH265EncodeFormat;
} else {
break;
}
AVPacket new_packet = packet;
if (self->m_bitFilterContext == NULL) {
self->m_bitFilterContext = av_bitstream_filter_init(filter_name);
}
av_bitstream_filter_filter(self->m_bitFilterContext, formatContext->streams[videoStreamIndex]->codec, NULL, &new_packet.data, &new_packet.size, packet.data, packet.size, 0);
}
复制代码
能够根据本身的需求自定义时间戳生成规则.这里使用当前系统时间戳加上数据包中的自带的pts/dts生成了时间戳.
CMSampleTimingInfo timingInfo;
CMTime presentationTimeStamp = kCMTimeInvalid;
presentationTimeStamp = CMTimeMakeWithSeconds(current_timestamp + packet.pts * av_q2d(formatContext->streams[videoStreamIndex]->time_base), fps);
timingInfo.presentationTimeStamp = presentationTimeStamp;
timingInfo.decodeTimeStamp = CMTimeMakeWithSeconds(current_timestamp + av_rescale_q(packet.dts, formatContext->streams[videoStreamIndex]->time_base, input_base), fps);
复制代码
本例将获取到的数据放在自定义的结构体中,而后经过block回调传给方法的调用者,调用者能够在回调函数中处理parse到的视频数据.
struct XDXParseVideoDataInfo {
uint8_t *data;
int dataSize;
uint8_t *extraData;
int extraDataSize;
Float64 pts;
Float64 time_base;
int videoRotate;
int fps;
CMSampleTimingInfo timingInfo;
XDXVideoEncodeFormat videoFormat;
};
...
videoInfo.data = video_data;
videoInfo.dataSize = video_size;
videoInfo.extraDataSize = formatContext->streams[videoStreamIndex]->codec->extradata_size;
videoInfo.extraData = (uint8_t *)malloc(videoInfo.extraDataSize);
videoInfo.timingInfo = timingInfo;
videoInfo.pts = packet.pts * av_q2d(formatContext->streams[videoStreamIndex]->time_base);
videoInfo.fps = fps;
memcpy(videoInfo.extraData, formatContext->streams[videoStreamIndex]->codec->extradata, videoInfo.extraDataSize);
av_free(new_packet.data);
// send videoInfo
if (handler) {
handler(YES, NO, &videoInfo, NULL);
}
free(videoInfo.extraData);
free(videoInfo.data);
复制代码
struct XDXParseAudioDataInfo {
uint8_t *data;
int dataSize;
int channel;
int sampleRate;
Float64 pts;
};
...
if (packet.stream_index == audioStreamIndex) {
XDXParseAudioDataInfo audioInfo = {0};
audioInfo.data = (uint8_t *)malloc(packet.size);
memcpy(audioInfo.data, packet.data, packet.size);
audioInfo.dataSize = packet.size;
audioInfo.channel = formatContext->streams[audioStreamIndex]->codecpar->channels;
audioInfo.sampleRate = formatContext->streams[audioStreamIndex]->codecpar->sample_rate;
audioInfo.pts = packet.pts * av_q2d(formatContext->streams[audioStreamIndex]->time_base);
// send audio info
if (handler) {
handler(NO, NO, NULL, &audioInfo);
}
free(audioInfo.data);
}
复制代码
由于咱们已经将packet中的关键数据拷贝到自定义的结构体中,因此使用完后须要释放packet.
av_packet_unref(&packet);
复制代码
- (void)freeAllResources {
if (m_formatContext) {
avformat_close_input(&m_formatContext);
m_formatContext = NULL;
}
if (m_bitFilterContext) {
av_bitstream_filter_close(m_bitFilterContext);
m_bitFilterContext = NULL;
}
}
复制代码
注意: 若是使用FFmpeg硬解,则仅仅须要获取到AVPacket数据结构便可.不须要再将数据封装到自定义的结构体中
上面操做执行完后,便可经过以下block获取解析后的数据,通常须要继续对音视频进行解码操做.后面文章会讲到,请持续关注.
XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
[parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) {
if (isFinish) {
// parse finish
...
return;
}
if (isVideoFrame) {
// decode video
...
}else {
// decode audio
...
}
}];
复制代码