白亞輝
(中國人民武裝警察部隊河北省總隊 河北省石家莊市 050000)
我們開發(fā)中很多地方會用到視頻播放功能,對于iOS 平臺視頻播放大致分為兩大類:使用蘋果的官方API 實現(xiàn)視頻播放功能。(AVKit,AVFoundation)和使用集成ffmpeg 框架的第三方庫。(ijkplayer,kxmovie 等)。本文現(xiàn)只針對第一種方式做簡單的探究。
播放一段本地的視頻我們貌似可以通過以下幾種方法實現(xiàn)。一是通過在APP 中嵌套WebView(UIWebView,WKWebView)加載Html5 標簽Video 來實現(xiàn)視頻播放的功能。二是通過AVKit 框架實現(xiàn)視頻播放。三是通過底層AVFoundation 框架實現(xiàn)視頻播放。實際上,無論哪種方式歸根結底仍然離不開底層AVFoundation 的身影。
特殊說明:基于mediaPlayer 類庫的MPMediaPlayer Controller(iOS9 后遭到廢棄,被AVPlayerViewController所替代)iOS8 之后蘋果推薦使用WKWebView 替代UIWebView,其主要的優(yōu)點有:
(1)WKWebView 更多的支持HTML5 的特性;
(2)WKWebView 更快,占用內(nèi)存可能只有UIWebView的1/3 ~ 1/4;
(3)WKWebView 高達60fps 的滾動刷新率和豐富的內(nèi)置手勢;
(4)WKWebView 具有Safari 相同的JavaScript 引擎;
(5)WKWebView 增加了加載進度屬性將;
(6)UIWebViewDelegate 和UIWebView 重構成了14個類與3 個協(xié)議。
通過webView 嵌套html 的video 標簽實現(xiàn)視頻播放,如下:
需要我們做的只是簡單的在我們的視圖中添加UIWebView(WKWebView)然后調(diào)用加載方法去加載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 是一個開源的瀏覽器引擎,我們在瀏覽器中能夠看到各種各樣的網(wǎng)頁就是因為WebKit 幫助我們解析html 代碼呈現(xiàn)給我們。很多瀏覽器包括safar,Chrome 就是一款基于WebKit 的瀏覽器,在我們的app 中無論原有的UIWebView 還是現(xiàn)有的WKWebView 其內(nèi)核也是基于WebKit 的。有一點我們應當知道,由于各個平臺軟硬件的不同,不同平臺下WebKit 也有不同的WebKit port。下面是不同WebKit por 的異同:
WebKit port 共同之處:
(1)DOM、winow、document;
(2)CSS 對象模型;
(3)CSS 解析,鍵盤事件處理;
(4)HTML 解析和DOM 構建;
(5)所有的布局和定位;
(6)Chrome 開發(fā)工具和WebKit 檢查器的UI 與檢查器;
(7)contenteditable、pushState、文件API、大多數(shù)SVG、CSS Transform math、Web Audio API、localStorage 等功能;
(8)很多其他功能與特性。
WebKit port 不同之處:
(1)GPU 相關技術;
(2)3D 轉(zhuǎn)換;
(3)WebGL;
(4)視頻解碼;
(5)將2D 圖像繪制到屏幕;
(6)解析方式;
(7)SVG 和CSS 漸變繪制;
(8)文字繪制和斷字;
(9)網(wǎng)絡層(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 可以大致了解其調(diào)用堆棧情況,如圖1,可以看到其實質(zhì)上最后還是調(diào)用了AVKit 框架下的AVplayerViewController 來實現(xiàn)視頻的播放。綜上,這種用html 方式播放視頻其實本質(zhì)上是通過webview 內(nèi)核WebKit實現(xiàn)的視頻標簽video 解析然后傳遞到底層去進行視頻播放,這種播放過程交給AVKit 框架來實現(xiàn)。
圖1: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.”大致意思為:為媒體播放創(chuàng)建視圖層級的服務,包括用戶控制,章節(jié)導航,并且支持副標題,隱藏字幕。AVKit framework 為播放視頻能容提供了一個高級的接口。
AVKit 框架是Apple 為我們提供的一個視頻播放高級框架,iOS8 以后可以使用,基于AVFoundation 實現(xiàn)。AVKit高度封裝,可以簡化我們的播放視頻的過程,當然也會帶來一些的弊端,一些高度定制化的功能通過AVKit 無法實現(xiàn),例如視頻編輯等。
那么AVKit 為我們提供了哪些類,這些類能幫助我們做什么呢?通過查看其引用關系我們應該能大致了解其功能。如iOS 中的AVKit 框架引用關系如圖2(iOS 10.3 其中帶小旗子的部分為@class 引入方式)。
圖2:AVKit 框架引用關系
可以看到AVKit 框架下涉及到的類并不多。主要的只有兩個AVPictureInPictureController 和AVPlayerViewController,其中AVPictureInPictureController 用于畫中畫的相關實現(xiàn)。AVPlayerViewController 用于視頻播放,AVPlayerView Controller 為我們提供了一個帶簡單操作條的視頻界面。
通過播放一段本地視頻的方法如下:
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。
可以看到AVPlayerViewController 為我們提供的播放界面并不是那么美觀。在實際應用也很可能會與我們設計的app 主題不符,影響用戶體驗。所以通常情況下我們會對播放界面做相應的修改。修改之前我們先了解下下AVPlayerViewController 的幾個重要屬性。
(1)player:用戶播放視頻的主要控件。需要初始化后丟入到AVPlayerViewController。
(2)showsPlaybackControls:用于控制是否顯示系統(tǒng)默認的控制條。
(3)videoGravity:定義了視頻應該怎樣在AVPlayer Layer 中顯示的字符串,包括AVLayerVideoGravityResize Aspect( 默認)、AVLayerVideoGravityResizeAspectFill、AVLayerVideoGravityResize 三種。
(4)contentOverlayView :一個處于控制視圖和視頻視圖中間的view,用來添加額外定義的視圖。
(5)allowsPictureInPicturePlayback:是否允許畫中畫模式。
我們可能會有一個思路是通過隱藏系統(tǒng)的控制條,然后在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(根據(jù)進度條view可以響應事件也可以推斷AVTouchIgnoringView 并不會攔截我們的事件)。另一個用于顯示系統(tǒng)進度條的UIView,這個view 可以響應用戶事件,我們contentOverlayView 無法響應事件應該是這個view 響應了用戶事件,導致響應鏈無法向下傳遞,也就無法傳遞到后面的contentOverlayView。那么我們是不是可以在這個view 上做一些自定義控件呢,很遺憾我們并不能get 到這個view。使用 playerViewController.view.subviews[0].subviews[0].subviews[1]這種方式獲取某個view 并不是很好的方式,因為隨著sdk 的更新這個層次機構并不能保證會一成不變。
我們雖然無法使用contentOverlayView 達到我們想要的效果,但是contentOverlayView 并不是一個毫無用處的view。實際中我們?nèi)匀荒苡盟尸F(xiàn)一些無需與用戶交互的界面。比如直播過程的字幕,送禮禮物動畫等(暫且不論其好壞),我們再回頭品味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初始化的過程中,系統(tǒng)會為我們創(chuàng)建了一個AVPlayerView,然后將這個view 添加到self.view 當中。如果我們無法在AVPlayerView 當中去修改界面,那么我們只能在AVPlayerView 上面在添加一個控制層了。當然這樣這個控制層就遮蓋了原來的控制層view,其上的雙擊放大,單擊隱藏進度條功能就會消失。這一部分得靠自己去實現(xiàn)了。
事實上AVPlayerView 是蘋果為我們封裝的一個播放界面,我們完全可以不用AVPlayerView,去實現(xiàn)自定義界面,這就更接近底層了,我們會在AVFoundation 播放視頻時介紹。
在我們自定義界面的時候不免會設計到對視頻的控制,比如暫停、開始、跳轉(zhuǎn)。或是一些視頻數(shù)據(jù)的現(xiàn)實問題,這些一般通過AVPlayerViewController 為我們提供player 可以做到。
通過前面的了解,我們知道AVKit 框架播放視頻其事只是做了兩件事:一提供了畫中畫相關功能。二提供一個viewController,自帶并不是很美觀的視頻播放界面。(修改界面可以做到,但實現(xiàn)方式卻比較low)。真正做到視頻播放的其實是AVPlayer,而AVPlayer 是AVFoundation 框架的主要角色之一,所以AVKit 視頻播放其實也是AVFoundation視頻播放過程。
首先看一下蘋果官方網(wǎng)站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 是幾款你可以用來播放和創(chuàng)建基于時間視聽媒體的框架之一。它提供了一個用來處理視聽媒體數(shù)據(jù)Objective-C 接口。比如你可以用它來檢查、創(chuàng)建、編輯或著重編碼媒體文件,你也可以用它從設備獲得輸入流,在實時拍攝、播放錄像時操作視頻。AVFoundation 在iOS 中所處的位置如圖4所示。
圖4:AVFoundation 在iOS 中所處的位置
可以看出AVFoundation 框架處于一個比較低級(相對UIKit)位置。建立在Core Audio、Core Media 、Core Animation 之上。
最簡單的方式只需要
(1)生成AVplayer;
(2)生成AVplayerLayer;
(3)將AVplayerLayer 添加到view 的layer 上;
(4)AVplayer 調(diào)用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();}
上述這樣就能實現(xiàn)視頻的播放了,但是沒有控制條。這就需要我們自己去實現(xiàn)了。具體細節(jié)不再詳細贅述,實現(xiàn)過程中可能會遇到的問題:問題一:屏幕旋轉(zhuǎn)后視頻界面不能自適應問題。問題二:視頻進度視頻總時間顯示問題。因為layer 不支持autolayout 所以針對問題一比較簡單的方法是:自定義一個view 改變這個view 的classLayer為AVPlayerLayer,讓我們的player 的layer 為該view 的layer。然后我們對這個view 進行autolayout 這樣就能自動適應屏幕了。
代碼如下:
關于屏幕旋轉(zhuǎn)的問題在iOS6 以后屏幕旋轉(zhuǎn)做了調(diào)整,尤其是在項目中用到navgationcontrler,tabbarcontroller 時問題會變得比較繁瑣。這里我們不在詳細展開。如果我們的項目中只有一個或者很少界面橫屏顯示視頻,建議采用modal形式顯示viewcontroller。
通過前面關于html5 方式以及AVKit 方式的介紹我們已經(jīng)知道播放過程最終都會給到AVFoundation 層。那么AVFoundation 是怎么樣播放的呢?
AVFoundation視頻播放功能集中到下面幾個類:AVAsset:用于獲取多媒體的相關信息,包括獲取多媒體的畫面、聲音等信息。AVPlayerItem:媒體資源管理對象,管理視頻的一些基本信息和狀態(tài)。AVPlayer:用于控制視頻的播放暫停快進等。AVPlayerLayer:視頻呈現(xiàn)的圖層,用于將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 會作為參數(shù)傳遞進去。這樣視頻就能播放了。
視頻播放過程會比較復雜,要想了解視頻是怎樣播放出來的我們首先要熟悉下視頻播放流程。一般而言視頻播放需要經(jīng)過幾個步驟:解協(xié)議,解封裝,解碼視音頻,視音頻同步,解協(xié)議是播放網(wǎng)絡流媒體時的步驟,如果播放本地視頻之需要后面三個步驟即可。
解協(xié)議:將流媒體協(xié)議的數(shù)據(jù),解析為標準的相應的封裝格式數(shù)據(jù)。視頻數(shù)據(jù)在網(wǎng)絡上傳遞時會根據(jù)不同的流媒體協(xié)議標準傳播數(shù)據(jù),這些協(xié)議在傳輸視音頻數(shù)據(jù)的同時,也會傳輸一些信令數(shù)據(jù)。這些信令數(shù)據(jù)包括對播放的控制(播放,暫停,停止),或者對網(wǎng)絡狀態(tài)的描述等。解協(xié)議的過程中會去除掉信令數(shù)據(jù)而只保留視音頻數(shù)據(jù)。例如,采用RTMP 協(xié)議傳輸?shù)臄?shù)據(jù),經(jīng)過解協(xié)議操作后,輸出FLV 格式的數(shù)據(jù)。
解封裝:將輸入的封裝格式的數(shù)據(jù),分離成為音頻流壓縮編碼數(shù)據(jù)和視頻流壓縮編碼數(shù)據(jù)。封裝格式種類很多,例如MP4,MKV,RMVB,TS,F(xiàn)LV,AVI 等等,它的作用就是將已經(jīng)壓縮編碼的視頻數(shù)據(jù)和音頻數(shù)據(jù)按照一定的格式放到一起。例如,F(xiàn)LV 格式的數(shù)據(jù),經(jīng)過解封裝操作后,輸出H.264 編碼的視頻碼流和AAC 編碼的音頻碼流。
解碼:將視頻/音頻壓縮編碼數(shù)據(jù),解碼成為非壓縮的視頻/音頻原始數(shù)據(jù)。音頻的壓縮編碼標準包含AAC,MP3,AC-3 等等,視頻的壓縮編碼標準則包含H.264,MPEG2,VC-1 等等。解碼是整個系統(tǒng)中最重要也是最復雜的一個環(huán)節(jié)。通過解碼,壓縮編碼的視頻數(shù)據(jù)輸出成為非壓縮的顏色數(shù)據(jù),例如YUV420P,RGB 等等;壓縮編碼的音頻 數(shù)據(jù)輸出成為非壓縮的音頻抽樣數(shù)據(jù),例如PCM 數(shù)據(jù)。
視頻解碼方式分為硬解和軟解。硬解:用硬件來進行解碼,通過顯卡的視頻加速功能對高清視頻進行解碼,依靠顯卡GPU 的。優(yōu)點是低功耗、發(fā)熱少、效率高,缺點是視頻兼容性差、支持度低;軟解:用軟件進行解碼,但是實際最終還是要硬件來支持的,這個硬件就是CPU。優(yōu)點是兼容強、全解碼、效果好,缺點是對CPU 要求高、效率低、發(fā)熱大。注意:AVFoundation 框架也使用硬件對視頻進行硬編碼和解碼,編碼后直接寫入文件,解碼后直接顯示。蘋果在iOS 8.0系統(tǒng)之前,沒有開放系統(tǒng)的硬件編碼解碼功能,不過Mac OS 系統(tǒng)一直有。在iOS 8.0 后,蘋果將該框架引入iOS 系統(tǒng)。用戶可以直接使用Video ToolBox 的框架來處理硬件的編碼和解碼。
視音頻同步:根據(jù)解封裝模塊處理過程中獲取到的參數(shù)信息,同步解碼出來的視頻和音頻數(shù)據(jù)。同步完畢后并將視頻音頻數(shù)據(jù)送至系統(tǒng)的顯卡和聲卡播放出來。
有了對上述步驟的理解,我們總結iOS 中視頻播放的流程,首先對AVPlayer 初始化,完畢后開始播放視頻,如果我們播放的是網(wǎng)絡上的視頻AVPlayer 首先會有一個解協(xié)議的過程,將網(wǎng)絡上的流媒體數(shù)據(jù)解協(xié)議成視頻封裝數(shù)據(jù)。如果播放本地的視頻文件則直接將文件解封裝成音視頻文件,隨后分別對音視頻文件進行解碼,最后進行音視頻同步呈現(xiàn)出來。
對于視頻播放模塊框架AVFoundation 怎樣實現(xiàn),通過什么代碼實現(xiàn)以上幾個步驟,蘋果沒有開放這其過程,所以對其探究也很難深入進去,不過我們?nèi)匀豢梢酝ㄟ^調(diào)用函數(shù)調(diào)用棧大致窺見一二,如圖6所示,當開始播放視頻的時候啟動線程調(diào)用了VideoToolBox 框架的相關內(nèi)容,當然不只是VideoToolBox ,我們在其他線程中還能看到MedioToolBox 框架。再往底層還有繪圖相關的框架。
圖6:VideoToolBox 框架的相關內(nèi)容