iOS平台FFmpeg开发(二)音/视频编解码

通过上一篇文章iOS平台FFmpeg开发(一)初识FFmpeg的学习,我相信你已经了解了视音频的基础知识,并且把FFmepg编译成功并成功导入到工程中了。从这一篇文章开始,我们开始真正地使用FFmpeg。

对视频的解码,我们需要使用libavformatlibavcodec这两个库。libavformat库主要负责输入输出、封装和解封装,libavcodec库主要负责编解码,所以要使用相应功能之前要先导入头文件avformat.havcodec.h

初始化

首先我们需要对FFmepg各个库进行初始化,这个初始化工作在囊个app生命周期只执行一次即可,所以你的代码应该是这样的:

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
   av_register_all();
   avformat_network_init();
   avcodec_register_all();
});

其中av_register_all()会初始化所有的muxerdemuxer和代码。muxer代码音视频复用器,它会把编码好的视频数据和音频数据合并到一个封装格式数据(比如mp4)中去,同理demuxer是解封装。

avformat_network_init()会初始化所有的网络模块。

avcodec_register_all()会注册所有类型的解码器,如果只用特定格式的解码器,可以单独注册。

文件操作

首先要创建AVFormatContext,用以管理文件的输入输出:

_format_context = avformat_alloc_context();

然后是打开输入,这个输入可以是本地视频文件地址,也可以是视频流地址。如果文件打开失败,要调用avformat_free_context()及时释放掉AVFormatContext。如果打开成功,后面不再需要输入文件的操作,要调用avformat_close_input(&_format_context)来关闭输入。

result = avformat_open_input(&_format_context, self.filePath.UTF8String, NULL, NULL);
if (result < 0) {
   NSLog(@"Failed to open input");
   if (_format_context) {
      avformat_free_context(_format_context);
 }
   return;
}

接着需要将视音频流的信息读取到AVFormatContextAVFormatContext中有信息,才能进行查找视频流、音频流及相应的解码器的操作:

result = avformat_find_stream_info(_format_context, NULL);
if (result) {
   NSLog(@"Failed to find stream info!");
   if (_format_context) {
       avformat_close_input(&_format_context);
   }
   return;
}

如果上面的方法成功了,就可以直接打印整个视频文件的信息了:

av_dump_format(_format_context, 0, _filePath.UTF8String, 0);

至此,对于视频文件基本信息的读取操作已经完成了。

初始化音/视频解码器

接下来需要初始化视音频的AVCodec(解码器)和AVCodecContext(解码器上下文)。注意,这里音频的AVCodecAVCodecContext和视频的是分开的,但是它们的流程是一模一样的,所以这部分可以单独抽一个方法出来。

首先根据类型找到音频或视频的序号,并在同时匹配到最适合的解码器。注:在之前的版本中会使用for循环来手动查找视频流或者音频流,并且要在后面单独进行解码器的查找操作,比较麻烦,现在一个方法就搞定,方便得多。

AVCodec *codec;
int streamIndex = av_find_best_stream(_format_context, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0); // 以查找视频流为例,

这样通过序号就能找到视频流或者音频流了:

AVStream *stream = _format_context->streams[streamIndex];

接下来通过匹配到的解码器创建AVCodecContext(解码器上下文)并把视/音频流里的参数传到视/音频解码器中:

AVCodecContext *codecContext = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(codecContext, stream->codecpar);
av_codec_set_pkt_timebase(codecContext, stream->time_base);

这里的codecpar表示包含解码器的各种参数的结构体。
time_base则是一个代表分数的结构体,num 为分数,den为分母,它表示时间的刻度。时间量乘以刻度就可以得到时间。
如果是(1, 25),那么时间刻度就是1/25。这里要注意的是AVStreamtime_baseAVCodecContexttime_base是不同的,上面的方法就涉及到time_base的转换,所以要换算得到时间就要选取相应的time_base

如果要得到double形式的time_base,可以调用av_q2d()函数,这个操作在这种分数结构体中会经常用到:

timeBase = av_q2d(codecContext->time_base);

接下来就可以打开解码器上下文准备进行解码操作了:

int result = avcodec_open2(codecContext, codec, NULL);
if (result) {
   NSLog(@"Failed to open avcodec!");
   avcodec_free_context(&codecContext);
   return;
}

解码

在进行解码之前,要先了解两个基本的结构体:AVPacketAVFrame

AVPacket

AVPacket表示编码(即压缩)后的数据,这种格式的音视频数据可以直接通过muxer封装成类似MKV的封装格式。如果AVPacket存的是视频数据,通常一个AVPacket只存放一桢数据(对应一个AVFrame),如果AVPacket存的是音频数据,那么一个AVPacekt里就可能存放多个桢的数据(对应多个AVFrame)。

AVFrame

AVFrame表示解码后的音/视频数据,它在使用之前必须进行初始化av_frame_alloc()。通常它只需要初始化一次就可以了,在解码过程中它可以作为一个容器被反复利用。

解码流程

在了解上面两个基本概念后,现在可以开始真正的解码了。

首先调用av_read_frame()将音/视频一小段一小段读取出来(视频是每次读取一桢,音频每次读取多桢),封装到AVPacket中,然后通过音/视频流的编号确定是音频数据还是视频数据并进行分别的解码操作。这里音/视频AVPacket的解码分别抽出了单独的方法。

- (void)readPacket {
    
    AVPacket packet;
    while (YES) {
        int result = av_read_frame(_format_context, &packet);
        if (result < 0) {
            NSLog(@"Finish to read frame!");
            break;
        }
        if (self.videoEnable && packet.stream_index == _videoStreamIndex) {
            if (![self decodeVideoPacket:packet]) {
                NSLog(@"Failed to decode audio packet");
                continue;
            }
        } else if (self.audioEnable && packet.stream_index == audioStreamIndex) {
            if (![self decodeAudioPacket:packet]) {
                NSLog(@"Failed to decode audio packet");
                continue;
            }
        }
    }
}

解码音/视频需要使用一对函数avcodec_send_packet()avcodec_receive_frame(),第一个函数发送未解码的包,第二个函数接收已解码的AVFrame。如果所有的AVFrame都接收完成则表示文件全部解码完成。相应的,编码也是一对函数avcodec_send_frame()avcodec_receive_packet()

  • avcodec_send_packet() 发送未解码数据
  • avcodec_receive_frame() 接收解码后的数据
  • avcodec_send_frame() 发送未编码的数据
  • avcodec_receive_packet() 接收编码后的数据

在这4个函数中的返回值中,都会有两个错误AVERROR(EAGAIN)AVERROR_EOF

如果是发送函数报AVERROR(EAGAIN)的错,表示已发送的AVPacket还没有被接收,不允许发送新的AVPacket。如果是接收函数报这个错,表示没有新的AVPacket可以接收,需要先发送AVPacket才能执行这个函数。

而如果报AVERROR_EOF的错,在以上4个函数中都表示编解码器处于flushed状态,无法进行发送和接收操作。

解码视频时每次发送的AVPacket通常是一桢视频,所以发送一次接收一次:

- (BOOL)decodeVideoPacket:(AVPacket)packet {
   int result = avcodec_send_packet(_codec_context, &packet);
   if (result < 0 && result != AVERROR(EAGAIN) && result != AVERROR_EOF) {
      NSLog(@"Failed to send packet!");
      return NO;
   }
   result = avcodec_receive_frame(_codec_context, _temp_frame);
   if (result < 0 && result != AVERROR(EAGAIN) && result != AVERROR_EOF) {
       NSLog(@"Failed to receive frame: %d", result);
      return NO;
   }
 
   // 对_temp_frame进行操作
   av_packet_unref(&packet);
}

解码音频时每次发送的AVPacket通常会转换成多个AVFrame,所以在接收的时候需要使用while循环保证所有的AVFrame都被接收到:

- (BOOL)decodeAudioPacket:(AVPacket)packet {
   int result = avcodec_send_packet(_codec_context, &packet);
   if (result < 0 && result != AVERROR(EAGAIN) && result != AVERROR_EOF) {
       NSLog(@"Failed to send packet!");
       return NO;
   }
   while (result >= 0) {
       result = avcodec_receive_frame(_codec_context, _temp_frame);
       if (result < 0) {
       if (result != AVERROR(EAGAIN) && result != AVERROR_EOF) {
           NSLog(@"Failed to receive frame: %d", result);
           return NO;
       }
       break;
       }
      // 对_temp_frame进行操作
      }
      av_packet_unref(&packet);
}

至此,音/视频的编解码就全部完成了,后续可以利用解码后的AVFrame进行音/视频的播放。

总结

音/视频编解码中最重要的是两个上下文结构体:AVFormatContextAVCodecContextAVFormatContext主要负责对原始音/视频文件或音/视频流进行操作,获取原始音/视频数据的信息。而AVCodecContext主要是用于存储编解码需要的信息,提供相应的解码器进行解码。加深对这两个上下文的理解,音/视频的编解码就会更得心应手。

在下一篇文章中,我会讲解如何播放解码后的视频数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容