视频播放方式
我们开发中很多地方会用到视频播放功能,对于iOS平台视频播放大致分为两大类。
- 使用苹果的api实现视频播放功能。(AVKit,AVFoundation)
- 使用集成ffmpeg框架的第三方库。(ijkplayer ,kxmovie等)
本文现只针对第一种方式做简单的探究。
播放一段本地的视频我们 貌似 可以通过以下几种方法实现。
- 通过在app中嵌套webView(UIWebView,WKWebView)加载html5标签video来实现视频播放的功能。
- 通过AVKit框架实现视频播放。
- 通过底层AVFoundation框架实现视频播放。
- ……
实际上,无论哪种方式归根结底仍然离不开底层AVFoundation的身影。
html5 video标签播放视频
特殊说明:
基于mediaPlayer类库的MPMediaPlayerController(iOS9后遭到废弃,被AVPlayerViewController所替代)
iOS8之后苹果推荐使用WKWebView替代UIWebView,其主要的优点有:
- WKWebView更多的支持HTML5的特性
- WKWebView更快,占用内存可能只有UIWebView的1/3 ~ 1/4
- WKWebView高达60fps的滚动刷新率和丰富的内置手势
- WKWebView具有Safari相同的JavaScript引擎
- WKWebView增加了加载进度属性
- 将UIWebViewDelegate和UIWebView重构成了14个类与3个协议官方链接
使用方式
通过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文件播放服务端视频文件。
原理分析
首先我们要了解一个概念WebKit。WebKit 是一个开源的浏览器引擎,我们在浏览器中能够看到各种各样的网页就是因为WebKit帮助我们解析html代码呈现给我们。很多浏览器包括safar,Chrome就是一款基于WebKit的浏览器,在我们的app中无论原有的UIWebView 还是现有的WKWebView 其内核也是基于WebKit的。
关于WebKit更多信息可以参考文章:
WebKit for Developers 中文翻译版 开发者需要了解的WebKit
有一点我们应当知道,由于各个平台软硬件的不同,不同平台下WebKit也有不同的WebKit port。下面是不同WebKit por的异同:
WebKit port共同之处
- DOM、winow、document
- CSS对象模型
- CSS解析,键盘事件处理
- HTML解析和DOM构建
- 所有的布局和定位
- Chrome开发工具和WebKit检查器的UI与检查器
- contenteditable、pushState、文件API、大多数SVG、CSS Transform math、Web Audio API、localStorage等功能
- 很多其他功能与特性
WebKit port不同之处:
- GPU相关技术
- 3D转换
- WebGL
- 视频解码
- 将2D图像绘制到屏幕
- 解析方式
- SVG和CSS渐变绘制
- 文字绘制和断字
- 网络层(SPDY、预渲染、WebSocket传输)
- JavaScript引擎
- JavaScriptCore 在WebKit repo中。V8和JavaScriptCore被绑定在WebKit中。
- 表单控制器的渲染
- 图像解码
- 页面导航 前进/后退
- pushState()的导航部分
- SSL功能,比如Strict Transport Security和Public Key Pins
从上面可以得知WebKit在不同平台下其
下面是程序调用堆栈
通过instrument 可以大致了解其调用堆栈情况,可以看到其实质上最后还是调用了AVKit框架下的 AVplayerViewController来实现视频的播放。
注: 测试环境 xcode 8.3.2 虚拟机 iOS 10.3
综上,这种用html方式播放视频其实本质上是通过webview内核WebKit实现的视频标签video解析然后传递到底层去进行视频播放,这种播放过程交给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框架引用关系如图(iOS 10.3 其中带小旗子的部分为@class引入方式)
可以看到AVKit框架下涉及到的类并不多。主要的只有两个AVPictureInPictureController 和AVPlayerViewController, 其中AVPictureInPictureController用于画中画的相关实现。AVPlayerViewController用于视频播放,AVPlayerViewController为我们提供了一个带简单操作条的视频界面。
使用方法
通过播放一段本地视频的方法如下
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继承自AVPlayerViewController之所以这样做是因为后面我们会在这个类中做一些视频控制界面的改变(这些改变必须在视图加载后,比如contentOverlayView要在viewDidLoad之后才能获取到)。如果不想做任何修改直接用AVPlayerViewController即可。
运行效果如下
在iphone与iPad上运行会有些许不同,ipad上比iPhone上右下角会多处一个画中画的操作按钮。点击后可进入画中画模式。画中画模式是iOS9添加的一个功能,可以通过AVPlayerViewController的allowsPictureInPicturePlayback属性进行控制,默认为true。
修改视频界面
可以看到AVPlayerViewController为我们提供的播放界面并不是那么美观。在实际应用也很可能会与我们设计的app主题不符,影响用户体验。所以通常情况下我们会对播放界面做相应的修改。
修改之前我们先看下AVPlayerViewController的内部结构。
重要的几个属性
- player: 用户播放视频的主要控件。需要初始化后丢入到AVPlayerViewController。
- showsPlaybackControls:用于控制是否显示系统默认的控制条。
- videoGravity:定义了视频应该怎样在AVPlayerLayer中显示的字符串,包括AVLayerVideoGravityResizeAspect(默认)、AVLayerVideoGravityResizeAspectFill、AVLayerVideoGravityResize三种
- contentOverlayView :一个处于控制视图和视频视图中间的view,用来添加额外定义的视图。
- allowsPictureInPicturePlayback:是否允许画中画模式。
修改界面的一个思路
我们可能会有一个思路是通过隐藏系统的控制条,然后在contentOverlayView添加自己的视图来自定义控制条。事实上这样可能并不能很好的解决我们的问题。contentOverlayView可以显示我们需要添加的控件,但是它并不能响应事件。通常这种情况会有下面几种可能:
- view本身设置isUserInteractionEnabled = false;
- view父控件设置isUserInteractionEnabled = false;
- view前方有其他控件遮挡。
所以我们设置
self.contentOverlayView?.isUserInteractionEnabled = true;
self.contentOverlayView?.superview?.isUserInteractionEnabled = true;
验证后仍然无妨响应事件。
为了验证遮盖问题我们有必要了解下播放视频时的view层次图,如下(基于iOS10.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视频播放过程。
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中所处的位置
可以看出AVFoundation框架处于一个比较低级(相对UIKit)位置。建立在Core Audio、Core Media 、Core Animation之上。
使用方法
最简单的方式只需要
- 生成AVplayer
- 生成AVplayerLayer
- 将AVplayerLayer 添加到view的layer上。
- AVplayer 调用play方法。
就像下面这样
let vc = UIViewController();
let path = Bundle.main.path(forResource: "movie", ofType: "mp4");
//player的初始化可以通过playerItem生成
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();
}
使用可以参考这篇文章:AVPlayer 本地、网络视频播放相关
自定义界面可能遇到的问题
上述这样就能实现视频的播放了,但是没有控制条。这就需要我们自己去实现了。具体细节不再详细赘述,实现过程中可能会遇到的问题:
- 屏幕旋转后视频界面不能自适应问题。
- 视频进度视频总时间显示问题。
因为layer不支持autolayout所以针对问题1比较简单的方法是:自定义一个view改变这个view的classLayer为AVPlayerLayer,让我们的player 的layer为该view的layer。 然后我们对这个view进行autolayout这样就能自动适应屏幕了
代码如下:
import UIKit
import AVFoundation
class BVideoPlayLayerView: UIView {
var videoPreviewLayer: AVPlayerLayer {
guard let layer = layer as? AVPlayerLayer else {
fatalError("Expected `AVPlayerLayer` type for layer. Check PreviewView.layerClass implementation.")
}
return layer
}
//设置一个player属性 这个player属性与自身的layer绑定,使用时view.player = ...
var player: AVPlayer? {
get {
return videoPreviewLayer.player;
}
set {
videoPreviewLayer.player = newValue;
}
}
/// 改变view layer 用于播放视频 默认CALayer
override class var layerClass: AnyClass {
return AVPlayerLayer.self;
}
}
}
关于屏幕旋转的问题在iOS6以后屏幕旋转做了调整,尤其是在项目中用到navgationcontrler,tabbarcontroller时问题会变得比较繁琐。这里我们不在详细展开。如果我们的项目中只有一个或者很少界面横屏显示视频,建议采用modal形式显示viewcontroller。
视频进度显示要通过playeritem获取时间,获取时间格式为CMTime。需要进行格式转换成我们需要的分秒形式。apple为我们提供了几个方法来操作CMTime常用的如下:
//通过CMTime获取秒
public func CMTimeGetSeconds(_ time: CMTime) -> Float64
//初始化CMTime
public func CMTimeMake(_ value: Int64, _ timescale: Int32) -> CMTime
//根据秒数初始化CMTime
public func CMTimeMakeWithSeconds(_ seconds: Float64, _ preferredTimescale: Int32) -> CMTime
//CMTime相加
public func CMTimeAdd(_ addend1: CMTime, _ addend2: CMTime) -> CMTime
//CMTime相减
public func CMTimeSubtract(_ minuend: CMTime, _ subtrahend: CMTime) -> CMTime
播放过程中要改变进度条的value可以通过player提供给我们的方法实现
open func addPeriodicTimeObserver(forInterval interval: CMTime, queue: DispatchQueue?, using block: @escaping (CMTime) -> Swift.Void) -> Any
例如:
player?.addPeriodicTimeObserver(forInterval: CMTimeMake(1, 30), queue: DispatchQueue.main, using: { (time) in
let currentTime = CMTimeGetSeconds((self.playerItem?.currentTime())!);
let totleStr = self.playerItem?.duration.toMinStr();
//toMinStr是CMTime的拓展方法 用于将CMTime转化成00:00格式字符串
let currentStr = self.playerItem?.currentTime().toMinStr();
self.timeLable?.text = currentStr! + "/" + totleStr!;
self.slider?.setValue(Float(currentTime), animated: true)
})
深入理解播放过程
通过前面关于html5方式以及AVKit方式的介绍我们已经知道播放过程最终都会给到AVFoundation层。那么AVFoundation是怎么样播放的呢?
AVFoundation视频播放功能集中到下面几个类:
AVAsset:用于获取多媒体的相关信息,包括获取多媒体的画面、声音等信息。
AVPlayerItem:媒体资源管理对象,管理视频的一些基本信息和状态。
AVPlayer:用于控制视频的播放暂停快进等。
AVPlayerLayer: 视频呈现的图层,用于将AVPlayer播放的视频显示出来。
初始化过程比较简单,如果按照下面方式初始化AVPlayer。
playerItem = AVPlayerItem(url:URL.init(fileURLWithPath: videoPath!))
player = AVPlayer.init(playerItem: playerItem);
对应初始化过程如下
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的框架来处理硬件的编码和解码。
更多解码信息可以参考:
[总结]视音频编解码技术零基础学习方法
iOS8系统H264视频硬件编解码说明
视音频同步
根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据。同步完毕后并将视频音频数据送至系统的显卡和声卡播放出来。
流程总结
AVPlayer初始化完毕后,我们开始播放视频,如果我们播放的是网络上的视频AVPlayer首先会有一个解协议的过程,将网络上的流媒体数据解协议成视频封装数据。如果播放本地的视频文件则直接将文件解封装成音视频文件,随后分别对音视频文件进行解码,最后进行音视频同步呈现出来。
对于视频播放模块框架AVFoundation怎样实现,通过什么代码实现以上几个步骤,苹果没有开放这其过程,所以对其探究也很难深入进去,不过我们仍然可以通过调用函数调用栈大致窥见一二,如下,当开始播放视频的时候启动线程掉用了VideoToolBox框架的相关内通,当然不只是VideoToolBox ,我们在其他线程中还能看到MedioToolBox 框架。再往底层还有绘图相关的框架。
一个有趣的实验
如果对三种方式播放视频的cup占用率进行对比,结果却可能跟我们相像的并不一致。
可以看到html的占用率明显的比两者要低得多。苹果可能在这方面做了些优化。具体为什么会这样希望对这方面有什么了解的人士能够帮忙解惑。