iOS AVFoundation视频播放探究
2022-03-31白亚辉
白亚辉
(中国人民武装警察部队河北省总队 河北省石家庄市 050000)
我们开发中很多地方会用到视频播放功能,对于iOS 平台视频播放大致分为两大类:使用苹果的官方API 实现视频播放功能。(AVKit,AVFoundation)和使用集成ffmpeg 框架的第三方库。(ijkplayer,kxmovie 等)。本文现只针对第一种方式做简单的探究。
播放一段本地的视频我们貌似可以通过以下几种方法实现。一是通过在APP 中嵌套WebView(UIWebView,WKWebView)加载Html5 标签Video 来实现视频播放的功能。二是通过AVKit 框架实现视频播放。三是通过底层AVFoundation 框架实现视频播放。实际上,无论哪种方式归根结底仍然离不开底层AVFoundation 的身影。
1 Html5 video标签播放视频
特殊说明:基于mediaPlayer 类库的MPMediaPlayer Controller(iOS9 后遭到废弃,被AVPlayerViewController所替代)iOS8 之后苹果推荐使用WKWebView 替代UIWebView,其主要的优点有:
(1)WKWebView 更多的支持HTML5 的特性;
(2)WKWebView 更快,占用内存可能只有UIWebView的1/3 ~ 1/4;
(3)WKWebView 高达60fps 的滚动刷新率和丰富的内置手势;
(4)WKWebView 具有Safari 相同的JavaScript 引擎;
(5)WKWebView 增加了加载进度属性将;
(6)UIWebViewDelegate 和UIWebView 重构成了14个类与3 个协议。
1.1 使用方式
通过webView 嵌套html 的video 标签实现视频播放,如下:
需要我们做的只是简单的在我们的视图中添加UIWebView(WKWebView)然后调用加载方法去加载html 文件即可。代码如下:
webView = WKWebView(frame:self.view.frame);
let path = Bundle.main.path(forResource:"movieHtml",ofType:"html");
let request = URLRequest.init(url:URL(fileURLWithPath:path!)) webView?.load(request);
self.view .addSubview(webView!);
该例当中加载了一个本地html 文件播放本地视频文件,实际当中也可以加载自己服务器端的html 文件播放服务端视频文件。
1.2 原理分析
首先我们要了解一个概念WebKit,WebKit 是一个开源的浏览器引擎,我们在浏览器中能够看到各种各样的网页就是因为WebKit 帮助我们解析html 代码呈现给我们。很多浏览器包括safar,Chrome 就是一款基于WebKit 的浏览器,在我们的app 中无论原有的UIWebView 还是现有的WKWebView 其内核也是基于WebKit 的。有一点我们应当知道,由于各个平台软硬件的不同,不同平台下WebKit 也有不同的WebKit port。下面是不同WebKit por 的异同:
WebKit port 共同之处:
(1)DOM、winow、document;
(2)CSS 对象模型;
(3)CSS 解析,键盘事件处理;
(4)HTML 解析和DOM 构建;
(5)所有的布局和定位;
(6)Chrome 开发工具和WebKit 检查器的UI 与检查器;
(7)contenteditable、pushState、文件API、大多数SVG、CSS Transform math、Web Audio API、localStorage 等功能;
(8)很多其他功能与特性。
WebKit port 不同之处:
(1)GPU 相关技术;
(2)3D 转换;
(3)WebGL;
(4)视频解码;
(5)将2D 图像绘制到屏幕;
(6)解析方式;
(7)SVG 和CSS 渐变绘制;
(8)文字绘制和断字;
(9)网络层(SPDY、预渲染、WebSocket 传输);
(10)JavaScript 引擎;
(11)JavaScriptCore 在WebKit repo 中。V8 和JavaScript Core 被绑定在WebKit 中;
(12)表单控制器的渲染;
(13)
(14)图像解码;
(15)页面导航 前进/后退;
(16)pushState()的导航部分;
(17)SSL 功能,比如Strict Transport Security 和Public Key Pins。
从上面可以得知WebKit 在不同平台下其
通过instrument 可以大致了解其调用堆栈情况,如图1,可以看到其实质上最后还是调用了AVKit 框架下的AVplayerViewController 来实现视频的播放。综上,这种用html 方式播放视频其实本质上是通过webview 内核WebKit实现的视频标签video 解析然后传递到底层去进行视频播放,这种播放过程交给AVKit 框架来实现。
图1:AVKit 框架
2 AVKit播放视频
关于AVKit 苹果官方给的解释是“Create view-level services for media playback,complete with user controls,chapter navigation,and support for subtitles and closed captioning.The AVKit framework provides a high-level interface for playing video content.”大致意思为:为媒体播放创建视图层级的服务,包括用户控制,章节导航,并且支持副标题,隐藏字幕。AVKit framework 为播放视频能容提供了一个高级的接口。
AVKit 框架是Apple 为我们提供的一个视频播放高级框架,iOS8 以后可以使用,基于AVFoundation 实现。AVKit高度封装,可以简化我们的播放视频的过程,当然也会带来一些的弊端,一些高度定制化的功能通过AVKit 无法实现,例如视频编辑等。
那么AVKit 为我们提供了哪些类,这些类能帮助我们做什么呢?通过查看其引用关系我们应该能大致了解其功能。如iOS 中的AVKit 框架引用关系如图2(iOS 10.3 其中带小旗子的部分为@class 引入方式)。
图2:AVKit 框架引用关系
可以看到AVKit 框架下涉及到的类并不多。主要的只有两个AVPictureInPictureController 和AVPlayerViewController,其中AVPictureInPictureController 用于画中画的相关实现。AVPlayerViewController 用于视频播放,AVPlayerView Controller 为我们提供了一个带简单操作条的视频界面。
2.1 使用方法
通过播放一段本地视频的方法如下:
let path = Bundle.main.path(forResource:"movie",ofType:"mp4");
let player = AVPlayer.init(url:URL.init(fileURLWithPath:path!)) ;
let playerVC = BAVPlayerViewController();playerVC.player = player;
self.present(playerVC,animated:true) { };
其中BAVPlayerViewController 继承自AVPlayerView Controller 之所以这样做是因为后面我们会在这个类中做一些视频控制界面的改变(这些改变必须在视图加载后,比如contentOverlayView 要在viewDidLoad 之后才能获取到)。如果不想做任何修改直接用AVPlayerViewController 即可。
在iphone 与iPad 上运行会有些许不同,ipad 上比iPhone上右下角会多处一个画中画的操作按钮。点击后可进入画中画模式。画中画模式是iOS9 添加的一个功能,可以通过AVPlayerViewController 的allowsPictureInPicturePlayback 属性进行控制,默认为true。
2.2 修改视频界面
可以看到AVPlayerViewController 为我们提供的播放界面并不是那么美观。在实际应用也很可能会与我们设计的app 主题不符,影响用户体验。所以通常情况下我们会对播放界面做相应的修改。修改之前我们先了解下下AVPlayerViewController 的几个重要属性。
(1)player:用户播放视频的主要控件。需要初始化后丢入到AVPlayerViewController。
(2)showsPlaybackControls:用于控制是否显示系统默认的控制条。
(3)videoGravity:定义了视频应该怎样在AVPlayer Layer 中显示的字符串,包括AVLayerVideoGravityResize Aspect( 默认)、AVLayerVideoGravityResizeAspectFill、AVLayerVideoGravityResize 三种。
(4)contentOverlayView :一个处于控制视图和视频视图中间的view,用来添加额外定义的视图。
(5)allowsPictureInPicturePlayback:是否允许画中画模式。
我们可能会有一个思路是通过隐藏系统的控制条,然后在contentOverlayView 添加自己的视图来自定义控制条。事实上这样可能并不能很好的解决我们的问题。contentOverlayView 可以显示我们需要添加的控件,但是它并不能响应事件。通常这种情况会有下面几种可能:
(1)view 本身设置isUserInteractionEnabled = false;
(2)view 父控件设置isUserInteractionEnabled = false;
(3)view 前方有其他控件遮挡。
所以我们设置self.contentOverlayView?.isUserInteraction Enabled = true;self.contentOverlayView?.superview?.isUser InteractionEnabled = true;验证后仍然无妨响应事件。为了验证遮盖问题我们有必要了解下播放视频时的view 层次图,如图3(基于iOS10.3)。
图3:View 层次图
从图3 中可以看到我们使用的contentOverlayView 前面确实会存在两个view,一个AVTouchIgnoringView 从字面意思理解不处理触摸事件的view(透明的UIView),也就是这个view 会将事件传递的其后面的view(根据进度条view可以响应事件也可以推断AVTouchIgnoringView 并不会拦截我们的事件)。另一个用于显示系统进度条的UIView,这个view 可以响应用户事件,我们contentOverlayView 无法响应事件应该是这个view 响应了用户事件,导致响应链无法向下传递,也就无法传递到后面的contentOverlayView。那么我们是不是可以在这个view 上做一些自定义控件呢,很遗憾我们并不能get 到这个view。使用 playerViewController.view.subviews[0].subviews[0].subviews[1]这种方式获取某个view 并不是很好的方式,因为随着sdk 的更新这个层次机构并不能保证会一成不变。
我们虽然无法使用contentOverlayView 达到我们想要的效果,但是contentOverlayView 并不是一个毫无用处的view。实际中我们仍然能用它呈现一些无需与用户交互的界面。比如直播过程的字幕,送礼礼物动画等(暂且不论其好坏),我们再回头品味Apple 对contentOverlayView 解读:“Use the content overlay view to add additional custom views between the video content and the controls.”其中between the video content and the controls,或许苹果也不并希望我们将控制放到contentOverlayView 当中。
修改界面的另一个思路:在我们AVPlayerViewController初始化的过程中,系统会为我们创建了一个AVPlayerView,然后将这个view 添加到self.view 当中。如果我们无法在AVPlayerView 当中去修改界面,那么我们只能在AVPlayerView 上面在添加一个控制层了。当然这样这个控制层就遮盖了原来的控制层view,其上的双击放大,单击隐藏进度条功能就会消失。这一部分得靠自己去实现了。
事实上AVPlayerView 是苹果为我们封装的一个播放界面,我们完全可以不用AVPlayerView,去实现自定义界面,这就更接近底层了,我们会在AVFoundation 播放视频时介绍。
在我们自定义界面的时候不免会设计到对视频的控制,比如暂停、开始、跳转。或是一些视频数据的现实问题,这些一般通过AVPlayerViewController 为我们提供player 可以做到。
通过前面的了解,我们知道AVKit 框架播放视频其事只是做了两件事:一提供了画中画相关功能。二提供一个viewController,自带并不是很美观的视频播放界面。(修改界面可以做到,但实现方式却比较low)。真正做到视频播放的其实是AVPlayer,而AVPlayer 是AVFoundation 框架的主要角色之一,所以AVKit 视频播放其实也是AVFoundation视频播放过程。
3 AVFoundation框架播放视频
首先看一下苹果官方网站AVFoundation 的定义如下:AVFoundation is one of several frameworks that you can use to play and create time-based audiovisual media.It provides an Objective-C interface you use to work on a detailed level with time-based audiovisual data.For example,you can use it to examine,create,edit,or reencode media files.You can also get input streams from devices and manipulate video during realtime capture and playback.大致意思为:AVFoundation 是几款你可以用来播放和创建基于时间视听媒体的框架之一。它提供了一个用来处理视听媒体数据Objective-C 接口。比如你可以用它来检查、创建、编辑或着重编码媒体文件,你也可以用它从设备获得输入流,在实时拍摄、播放录像时操作视频。AVFoundation 在iOS 中所处的位置如图4所示。
图4:AVFoundation 在iOS 中所处的位置
可以看出AVFoundation 框架处于一个比较低级(相对UIKit)位置。建立在Core Audio、Core Media 、Core Animation 之上。
3.1 使用方法
最简单的方式只需要
(1)生成AVplayer;
(2)生成AVplayerLayer;
(3)将AVplayerLayer 添加到view 的layer 上;
(4)AVplayer 调用play 方法。
就像下面这样就像下面这样
let vc = UIViewController();
//player 的初始化可以通过playerItem 生成
let path = Bundle.main.path(forResource:"movie",ofType:"mp4");
let player = AVPlayer(url:URL.init(string:path!)!);
let layer = AVPlayerLayer.init(layer:player);
vc.view.layer.addSublayer(layer);
self.present(vc,animated:true) { player.play();}
3.2 自定义界面可能遇到的问题
上述这样就能实现视频的播放了,但是没有控制条。这就需要我们自己去实现了。具体细节不再详细赘述,实现过程中可能会遇到的问题:问题一:屏幕旋转后视频界面不能自适应问题。问题二:视频进度视频总时间显示问题。因为layer 不支持autolayout 所以针对问题一比较简单的方法是:自定义一个view 改变这个view 的classLayer为AVPlayerLayer,让我们的player 的layer 为该view 的layer。然后我们对这个view 进行autolayout 这样就能自动适应屏幕了。
代码如下:
关于屏幕旋转的问题在iOS6 以后屏幕旋转做了调整,尤其是在项目中用到navgationcontrler,tabbarcontroller 时问题会变得比较繁琐。这里我们不在详细展开。如果我们的项目中只有一个或者很少界面横屏显示视频,建议采用modal形式显示viewcontroller。
3.3 深入理解播放过程
通过前面关于html5 方式以及AVKit 方式的介绍我们已经知道播放过程最终都会给到AVFoundation 层。那么AVFoundation 是怎么样播放的呢?
AVFoundation视频播放功能集中到下面几个类:AVAsset:用于获取多媒体的相关信息,包括获取多媒体的画面、声音等信息。AVPlayerItem:媒体资源管理对象,管理视频的一些基本信息和状态。AVPlayer:用于控制视频的播放暂停快进等。AVPlayerLayer:视频呈现的图层,用于将AVPlayer 播放的视频显示出来。
初始化过程比较简单,如果按照下面方式初始化AVPlayer。
playerItem = AVPlayerItem(url:URL.init(fileURLWithPath:videoPath!))
player = AVPlayer.init(playerItem:playerItem);
对应初始化过程如图5。
图5:AVPlayer 对应初始化过程
AVPlayer 会通过AVPlayerItem 去初始化,AVPlayerItem通 过AVAsset 初 始 化,AVAsset 通 过URL 初 始 化(AVFoundation 为我们封装了一些方法可以直接通过URL初始化AVPlayerItem 或者AVPlayer)。AVPlayer 初始化完毕后,初始化用于显示视频的AVPlayerLayer,AVPlayer 会作为参数传递进去。这样视频就能播放了。
视频播放过程会比较复杂,要想了解视频是怎样播放出来的我们首先要熟悉下视频播放流程。一般而言视频播放需要经过几个步骤:解协议,解封装,解码视音频,视音频同步,解协议是播放网络流媒体时的步骤,如果播放本地视频之需要后面三个步骤即可。
解协议:将流媒体协议的数据,解析为标准的相应的封装格式数据。视频数据在网络上传递时会根据不同的流媒体协议标准传播数据,这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用RTMP 协议传输的数据,经过解协议操作后,输出FLV 格式的数据。
解封装:将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如MP4,MKV,RMVB,TS,FLV,AVI 等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV 格式的数据,经过解封装操作后,输出H.264 编码的视频码流和AAC 编码的音频码流。
解码:将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC,MP3,AC-3 等等,视频的压缩编码标准则包含H.264,MPEG2,VC-1 等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P,RGB 等等;压缩编码的音频 数据输出成为非压缩的音频抽样数据,例如PCM 数据。
视频解码方式分为硬解和软解。硬解:用硬件来进行解码,通过显卡的视频加速功能对高清视频进行解码,依靠显卡GPU 的。优点是低功耗、发热少、效率高,缺点是视频兼容性差、支持度低;软解:用软件进行解码,但是实际最终还是要硬件来支持的,这个硬件就是CPU。优点是兼容强、全解码、效果好,缺点是对CPU 要求高、效率低、发热大。注意:AVFoundation 框架也使用硬件对视频进行硬编码和解码,编码后直接写入文件,解码后直接显示。苹果在iOS 8.0系统之前,没有开放系统的硬件编码解码功能,不过Mac OS 系统一直有。在iOS 8.0 后,苹果将该框架引入iOS 系统。用户可以直接使用Video ToolBox 的框架来处理硬件的编码和解码。
视音频同步:根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据。同步完毕后并将视频音频数据送至系统的显卡和声卡播放出来。
有了对上述步骤的理解,我们总结iOS 中视频播放的流程,首先对AVPlayer 初始化,完毕后开始播放视频,如果我们播放的是网络上的视频AVPlayer 首先会有一个解协议的过程,将网络上的流媒体数据解协议成视频封装数据。如果播放本地的视频文件则直接将文件解封装成音视频文件,随后分别对音视频文件进行解码,最后进行音视频同步呈现出来。
对于视频播放模块框架AVFoundation 怎样实现,通过什么代码实现以上几个步骤,苹果没有开放这其过程,所以对其探究也很难深入进去,不过我们仍然可以通过调用函数调用栈大致窥见一二,如图6所示,当开始播放视频的时候启动线程调用了VideoToolBox 框架的相关内容,当然不只是VideoToolBox ,我们在其他线程中还能看到MedioToolBox 框架。再往底层还有绘图相关的框架。
图6:VideoToolBox 框架的相关内容