使用FFMPEG和VideoToolbox在iOS上加速H264解码

在LIVEOP,我们致力于以简洁的方式为急救人员提供最相关的信息,同时又不影响我们的无缝用户体验。 当我们与全球领先的无线(随身)摄像头系统提供商Zepcam合作时,我们希望确保我们提供的体验符合我们的高标准,而不影响性能或效率。

Zepcam托管的摄像机流有几种不同的格式,最重要的是,HTTP Live Streaming(HLS)是iOS生态系统中的一流公民,内置了AVFoundation和RTSP(实时流协议)的内置支持。 HLS流通常用于直播电视和新闻广播。 它专注于为观看者带来无缝体验:不允许丢帧,不允许无序播放帧,并保留少量即将出现的帧缓冲区以确保流畅的播放体验。 Zepcam流被激活的情况通常会危及生命。 警员可能在试图装防暴动时从佩戴在身上的摄像机进行实时直播,或者在顶部装有摄像机的梯形引擎可能会鸟瞰大建筑物的火灾,包括消防员在地面上的位置。 我们对无缝用户体验的定义与HTTP Live Streaming所规定的不同:在我们的案例中,向用户显示的帧尽可能实时是很重要的。 它们可以无序到达,并且可以丢弃几帧,只要这有利于流的实时性。 加上我们的要求,我们开始在UDP上使用RTSP。

Apple不在任何高级框架中提供对RTSP流回放的支持。 用于回放视频流的所有高级系统类MPMoviePlayerControllerAVPlayerItemAVPlayer不支持RTSP流。 幸运的是,瑞士的音频/视频处理军刀FFMPEG配备了正确的工具来处理和解码RTSP流。 FFMPEG在开源社区已有17年的历史了,自那时起,它就已经成为VLC,Google Chrome和Chromium¹等各种最终用户应用程序背后的可靠力量。

设置FFMPEG

Zepcam提供的RTSP流使用H264编解码器进行编码。 为了防止最终iOS应用程序文件(.ipa)的二进制文件大量增加,我们选择从头开始编译最新版本的FFMPEG(v4.0.1),仅启用我们希望使用的那些功能。 我们使用此处找到的出色的构建脚本,并进行了一些调整:

  • FF_VERSION变量更改为4.0.1
  • DEPLOYMENT_TARGET更改为iOS应用程序的部署目标
  • 更改CONFIGURE_FLAGS以启用位码,并禁用除流所需的所有功能之外的所有功能:
  CONFIGURE_FLAGS =“-启用交叉编译--disable-debug --disable-programs --disable-doc --extra-cflags = -fembed-bitcode --extra-cxxflags = -fembed-bitcode --disable-ffmpeg --disable-ffprobe --disable-avdevice --disable-avfilter --disable-encoders --disable-parsers --disable-decoders --disable-protocols --disable-filters --disable-muxers --disable-bsfs --disable-indevs --disable-outdevs --disable-demuxers --enable-protocol = file --enable-protocol = tcp --enable-protocol = udp --enable-decoder = mjpeg --enable-decoder = h264 --enable-parser = mjpeg --enable-parser = h264 --enable-parser = aac --enable-demuxer = rtsp --enable-videotoolbox“ 

此外,如此处所述,需要对FFMPEG源文件libswresample/arm/audio_convert_neon.S进行小的更改。 现在,编译应该会成功,从而产生几个不同的库。 将库拖动到Xcode项目中,并确保将它们与应用程序目标链接(“构建阶段”>“使用库链接二进制文件”)。

通过FFMPEG实现视频播放所需的全局设置非常简单。 使用avformat_open_input打开指向RTSP流的输入URL,使用avformat_open_input从输入中查找流,使用avcodec_alloc_context3avcodec_parameters_to_context分配编解码器上下文,最后使用avcodec_open2打开编解码器。 对所有这些方法实施正确的错误处理和内存清理非常重要,因为根据情况它们都可能失败。 在我们的应用程序中,我们还选择实现中断回调,以便在某些情况下尽早退出阻塞方法,例如SCNetworkReachability API所指示的互联网连接不足或自定义超时计时器到期后。 特别是与可达性API的结合,使我们能够规避内置FFMPEG超时并在未检测到Internet连接时尽早失败。

解码帧

AVCodecContext结构公开了一个AVCodecContext字段,该字段使我们可以从可用格式列表中为解码器提供的视频帧选择输出AVPixelFormat 。 如果我们将此字段留空,则视频帧将被格式化为AV_PIX_FMT_YUV420P ,这是解码器根据基础流自动检测到的格式。 iOS上的图像采用RGB(A)格式( AV_PIX_FMT_RGB24 )格式,因此在显示之前需要额外的步骤将帧从AV_PIX_FMT_YUV420P转换为AV_PIX_FMT_RGB24libswscale提供了一个功能sws_scale来实现此功能,但是不幸的是,它没有在GPU上实现,这意味着在执行从YUV420P到RGB24的额外转换步骤时,我们会受到性能影响。

在通过get_format函数收到的可用像素格式列表中,有一种需要特别注意: AV_PIX_FMT_VIDEOTOOLBOX 。 尽管记录不充分,但是这种格式告诉解码器将传入的帧传递给Apple的VideoToolbox.framework 。 它将解码GPU上的每个传入帧,并返回一个CVPixelBufferRef保存解码后的数据。 这比默认实现要好得多,默认实现需要在CPU上从YUV420到RGB24的额外转换。 现在,传递给AVCodecContext字段的函数句柄如下所示:

 静态枚举AVPixelFormat negotiation_pixel_format(struct AVCodecContext * s,const枚举AVPixelFormat * fmt){ 
 而(* fmt!= AV_PIX_FMT_NONE){ 
如果(* fmt == AV_PIX_FMT_VIDEOTOOLBOX){
如果(s-> hwaccel_context == NULL){
int结果= av_videotoolbox_default_init(s);
如果(结果<0){
返回s-> pix_fmt;
}
}
返回* fmt;
}
++ fmt;
}
返回s-> pix_fmt;
}

在尝试使用VideoToolbox格式之前,请确保它可用。 如果它不可用,或者初始化videotoolbox集成失败,我们将退回到解码器最初发现的格式。 请注意, AV_PIX_FMT_VIDEOTOOLBOX在iOS模拟器上不可用。 在视频播放器类的拆解方法中,我们检查编解码器上下文的hwaccell_context是否不为NULL,如果是这种情况,则调用av_videotoolbox_default_free

单独的视频帧通过avcodec_receive_frame接收。 返回此函数后, AVFrame输出参数将填充编码单个视频帧的信息,具体取决于所使用的像素格式。 如果成功使用了VideoToolbox格式,则可以在AVFrame.data[3]找到保存帧数据的AVFrame.data[3] 。 尽管没有明确记录, pixfmt.h中其他硬件加速格式的定义后面的注释中可以明显pixfmt.h 。 无法立即显示CVPixelBufferRef 。 我们首先使用+[CIImage imageWithCVPixelBuffer:]将其转换为CIImage ,然后使用+[UIImage imageWithCIImage:]CIImage转换为UIImage 。 最终的UIImage描述了一个视频帧,现在可以在您的视频播放器中显示,例如,实现为普通的UIImageView

在使用默认像素格式(包括向RGB24的额外转换步骤)和VideoToolbox格式之间的测试中,我们注意到了显着的性能差异。 通过CPU解码,我们体验了整体缓慢的播放和相对大量的帧丢失。 通过GPU解码,几乎丢弃了零帧。 尽管没有得到很好的记录,但是深入了解FFMPEG的内部内容以在GPU上实现iOS上视频流的GPU加速解码绝对是值得的。

我们迫不及待地希望看到我们的Zepcam集成将如何改善每天工作的男人和女人的工作流程,以确保我们的社会安全。

  1. https://trac.ffmpeg.org/wiki/项目