影片播放器 [TOC]
利用MVC架構製作簡易的影片播放器以及常用的控制功能。
APP截圖
橫式影片播放器 將螢幕轉向或點擊最右下角全螢幕按鈕可入橫式播放器,再次點擊則返回 直式。
播放器快轉/倒退特效 點擊按鈕或快速點兩下可快進或倒轉
螢幕左方區域雙擊為倒轉,右方區域為快轉
程式碼 下方範例只挑幾個重點部分做說明,完整的範例在這裡
範例總共有幾大部分:
資料格式 由於此範例不是經由API讀取資料,且資料格式為固定的,所以使用列舉來製作資料來源的格式以便影片切換。
創一個列舉並定義值 定義名稱可自行決定,這裡使用影片名稱來命名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 enum VideoSourceType :Int , CaseIterable { case bigBuckBunny case elephantDream case forBiggerBlazes case forBiggerEscape case forBiggerFun case forBiggerJoyrides case forBiggerMeltdowns case sintel case subaruOutbackOnStreetAndDirt case tearsOfSteel case volkswagenGTIReview case weAreGoingOnBullrun case whatCareCanYouGetForAGrand } class VideoSource : NSObject { override init () { super .init () } }
遵循CaseIterable協議可以讓enum向陣列一樣使用。
定義一個協議並列出我們影片需要的幾個參數。
1 2 3 4 5 6 7 protocol VideoDetail { var description:String { get } var sources:String { get } var subtitle:String { get } var title:String { get } var fullImagePath:String { get } }
使用extension並讓VideoSourceType遵循這個協議。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 extension VideoSourceType : VideoDetail { var description:String { } var sources:String { } var subtitle:String { } var title:String { } var fullImagePath:String { } }
在description 、sources 、subtitle 、title 做相同的判斷,依照列舉的項目回傳對應得資訊。
以下為title的範例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 var title:String { switch self { case .bigBuckBunny: return "Big Buck Bunny" case .elephantDream: return "Elephant Dream" case .forBiggerBlazes: return "For Bigger Blazes" case .forBiggerEscape: return "For Bigger Escape" case .forBiggerFun: return "For Bigger Fun" case .forBiggerJoyrides: return "For Bigger Joyrides" case .forBiggerMeltdowns: return "For Bigger Meltdowns" case .sintel: return "Sintel" case .subaruOutbackOnStreetAndDirt: return "Subaru Outback On Street And Dirt" case .tearsOfSteel: return "Tears of Steel" case .volkswagenGTIReview: return "Volkswagen GTI Review" case .weAreGoingOnBullrun: return "We Are Going On Bullrun" case .whatCareCanYouGetForAGrand: return "What care can you get for a grand?" } }
若有來自不同網域的圖片可利用switch判斷並加上對應的路徑。
範例的路徑都一樣所以回傳同一個網域即可。
1 2 3 var fullImagePath:String { return "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/" + thumb }
由於有遵循CaseIterable協議,若有需求可以用if else來判斷並處理例外的網域。
範例:
1 2 3 4 5 6 7 8 var fullImagePath:String { if self == .bigBuckBunny { return "https://aaa.com/" + thumb } else { return "https://bbb.com/" + thumb } }
定義model 列表資料結構
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct Categories :Codable { var name:String ? var videos:[Video ] = [] var count:Int { return videos.count } } extension Categories { func getVideo (index:Int) -> Video ? { if index < count { return videos[index] } return nil } }
Codable協議:使資料類型可編碼( encodable )和可解碼 ( decodable ),用於與外部表示( 例如:JSON )相容。 例:
1 2 3 4 5 6 do { let data = try? JSONEncoder ().encode(item) } catch { }
如果沒有遵循Codable協議就不能使用JSONEncoder
影片的資料結構
1 2 3 4 5 6 7 struct Video :Codable { var description:String ? var sources:String ? var subtitle:String ? var thumb:String ? var title:String ? }
在VideoSource建立一個陣列來存放影片資料,設定期存取權限為get only
存取權限(Access control) private 只有當前類別可以使用,private(set)能使其他檔案使用此參數,但無法改變其參數內容,若需要改變內容需要透過setter。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class VideoSource : NSObject { private(set) var videos:[Video ] = [] override init () { super .init () for sourceType in VideoSourceType .allCases { let video = Video .init (description: sourceType.description, sources: sourceType.sources, subtitle: sourceType.subtitle, thumb: sourceType.fullImagePath, title: sourceType.title) videos.append(video) } } func getVideos () -> Categories { return Categories .init (name: "Movies" , videos: videos) } }
在大部份時候蘋果鼓勵使用 Struct,直到需要利用到 Class 才有的特性,像是需要使用到 Swift 的 Framework、Foundation 或是 UIkit,亦或是需要不斷的在各處繼承同一個父類別。
structure 的使用定偏向儲存資料。 class 則是著重於流程控制,程式碼功能導向。
影片列表 創建一個 ViewController 並繼承 BaseViewController 來實作影片列表。
個人寫作習慣是會將專案內的 ViewController 繼承一個父類,裡面會有加入許多通用的函式,例如跳出警告視窗,設定上方導覽列等功能,可自行增加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class MovieListViewController : BaseViewController { override func viewDidLoad () { super .viewDidLoad() } } extension MovieListViewController : UICollectionViewDelegate , UICollectionViewDataSource { func collectionView (_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { } func collectionView (_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { } func collectionView (_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { } }
由於 UICollecitonView 為較基礎應用,故不在此篇重點,暫不贅述,完整專案請參考最下方程式碼連結。
播放器 實作一個AVPlayer的物件
1 2 3 4 5 6 7 8 9 10 11 12 class AVPlayerView : UIView { required init? (coder aDecoder: NSCoder ) { super .init (coder: aDecoder) } override init (frame: CGRect ) { super .init (frame: frame) } }
制定幾個協議,將對其的操作透過代理模式傳給 PlayerViewController
基礎幾個參數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class AVPlayerView : UIView { weak var delegate:AVPlayerViewDelegate ? var player : AVPlayer ? var avPlayerLayer : AVPlayerLayer ! var playerItem:AVPlayerItem ? var isPlaying:Bool = false var timeObserverToken: Any ? var currentSecond:Float64 ? { if let time = player? .currentItem? .currentTime() { return CMTimeGetSeconds (time) } return nil } required init? (coder aDecoder: NSCoder ) { super .init (coder: aDecoder) } override init (frame: CGRect ) { super .init (frame: frame) } }
在AVPlayerView內 製作一個列舉,方便監聽播放器狀態時使用。
1 2 3 4 5 6 7 8 9 10 11 12 class AVPlayerView : UIView { enum PlayerObserverKey :String { case status case loadedTimeRanges case playbackBufferEmpty case playbackLikelyToKeepUp } }
接下來開始初始化播放器,AVPlayerView 只吃一個外部參數,就是影片路徑,需要重複使用時,只需要代入不同的影片路徑。
AVPlayerItem 設定影片路徑 -> AVPlayerItem 影片播放元素給 AVPlayer 初始化 -> 交給 AVPLayerLayer 設定播放畫面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func setPlayer (urlSting:String) { let videoURL = URL (string: urlSting)! playerItem = AVPlayerItem (url: videoURL) player = AVPlayer (playerItem: playerItem) avPlayerLayer = AVPlayerLayer (player: player) avPlayerLayer.frame = self .bounds self .layer.addSublayer(avPlayerLayer) }
播放器有幾個需要監聽的事件,監聽後才能對狀態進行判斷並加以控制
1. 監聽播放結束
1 2 3 4 5 6 NotificationCenter .default.addObserver(self , selector: #selector (playerDidFinishPlaying), name: NSNotification .Name .AVPlayerItemDidPlayToEndTime , object: nil )
selector 對應的函式
1 2 3 4 5 6 7 8 9 @objc func playerDidFinishPlaying () { delegate? .playerDidFinishPlaying(self ) }
2. 監聽播放器狀態
1 2 3 4 5 6 player? .currentItem? .addObserver(self , forKeyPath: PlayerObserverKey .status.rawValue, options: .new, context: nil ) }
3. 緩衝,可用來獲取快取存了多少
1 2 3 4 5 player? .currentItem? .addObserver(self , forKeyPath: PlayerObserverKey .loadedTimeRanges.rawValue, options: .new, context: nil )
4. 緩衝不夠,停止播放
1 2 3 4 5 6 player? .currentItem? .addObserver(self , forKeyPath: PlayerObserverKey .playbackBufferEmpty.rawValue, options: .new, context: nil )
5. 緩衝足夠,繼續播放
1 2 3 4 5 player? .currentItem? .addObserver(self , forKeyPath: PlayerObserverKey .playbackLikelyToKeepUp.rawValue, options: .new, context: nil )
5. 監聽影片目前播放進度並傳給代理,提供給ViewController使用
1 2 3 4 5 6 7 8 9 10 11 func addVideoObserver () { timeObserverToken = player? .addPeriodicTimeObserver(forInterval: CMTimeMake (value: 1 , timescale: 1 ), queue: .main, using: { (CMTime ) in if self .player! .currentItem? .status == .readyToPlay { let currentTime = CMTimeGetSeconds (self .player! .currentTime()) self .delegate? .didUpdatePlayerCurrentTime(self , time: currentTime) } }) }
在關閉播放器的時候要記得移除 timeObserverToken,否則聲音還會持續在背景播放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 deinit { if let timeObserverToken = timeObserverToken { player? .removeTimeObserver(timeObserverToken) self .timeObserverToken = nil } player = nil if avPlayerLayer != nil { avPlayerLayer.removeFromSuperlayer() avPlayerLayer = nil } }