基於swift4.0實現視訊播放、螢幕旋轉、倍速播放、手勢調節,鎖屏皮膚等功能

ControlM發表於2018-01-24

學習swift有段時間了,原來寫過一個基於 swift 3.0 的視訊播放,後來有同學聯絡我說,在音訊鎖屏的情況下,無法用控制皮膚拖動進度條調節播放進度,所以又將原來的程式碼拿過來重新整理了下也順便更新到了4.0版本。在把原來的程式碼拿來的時候發現原來有好多地方都是錯誤的,原來在 OC 專案裡面已經寫過一遍關於視訊播放的東西所以就按照原來的邏輯寫了 swift 版本,其實裡面很多程式碼我也是通過查詢資料和看文件拼湊出來的,對於 swift 的語句也是一知半解,希望各位看官多多包涵。

先來看一下實現的效果,一圖勝千言(第一張是 iOS 10系統,第二張是 iOS 11系統)。

基於swift4.0實現視訊播放、螢幕旋轉、倍速播放、手勢調節,鎖屏皮膚等功能
基於swift4.0實現視訊播放、螢幕旋轉、倍速播放、手勢調節,鎖屏皮膚等功能
demo下載地址

工程介紹

簡單說一下工程結構,所有關於佈局都是在Player資料夾下的MPlayerViewModel檔案中,考慮到耦合度的原因,所以將視訊播放的所有 UI 佈局全部抽離出來,在播放器 view 裡將會頻繁看到一個叫viewModel的物件,它既 UI 佈局也是佈局控制元件的所有者。視訊播放的佈局是基於SnapKit三方庫來佈局了,因為在OC裡用慣了Masonry所以工程裡依然沿用這個庫。主要程式碼是放到MPlayerView這個檔案中的,其中還有一個由 OC 寫的DeviceTool檔案主要用來做頁面強制旋轉用的,強制旋轉這一部分我現在還沒有更好的解決辦法只能橋接 OC 裡的方法。

初始化播放器方法

視訊播放介面我用的是一個單例實現的,剛開始不是用單例實現,但是為了把程式碼拆出來放到各自的功能區所以用單例實現是最好的方法。由於swift放棄了OC裡的dispatch_once實現單例方法,swift3.0以後的單例寫法:

/// 建立播放器單例
static let shared = MPlayerView()
private override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
複製程式碼

在swift3.0之後重寫init方法必須實現required init方法,這麼做也是為了安全,因為在OC裡init方法並不能保證子類完成初始化,增加required“這是由初始化方法的完備性需求所決定的,以保證型別的安全。在建立視訊播放檢視有兩種建立方式:1.用單利建立。2.init 初始化 ,這兩種方法都可以達到視訊播放的效果。

1.單利初始化
self.playerView = MPlayerView.shared.initWithFrame(frame: self.view.frame, videoUrl: videoUrl, type: "VIDEO")
2.init 初始化
self.playerView = MPlayerView().initWithFrame(frame: CGRect.init(x: 0, y: 0, width: Screen_width, height: Screen_width * 9/16), videoUrl: videoUrl, type: "VIDEO")
複製程式碼

手勢滑動及注意事項

由於swift裡面有嚴格的型別檢查,就比如在做手勢滑動的時候,手勢剛開始滑動的時候肯定需要記錄一下當前播放器的位置我在專案中是定義的sumTime屬性是一個CMTime型別,如果在OC裡大可不必這樣,來看一下swift與OC程式碼的區別

swift寫法

/// 給sumTime初值
let time = self.player?.currentTime()
self.sumTime = CMTimeMake((time?.value)!, (time?.timescale)!)
複製程式碼

OC寫法

// 給sumTime初值
CMTime time = self.player.currentTime;
self.sumTime = time.value/time.timescale;
複製程式碼

滑動的距離是一個Double型別,而self.sumTime是CMTime型別,倆者肯定不能想加算出結束滑動的距離,所以將double型別轉換成CMTime型別用以下方法:

CMTime.init(seconds: Double.init(value/200), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
複製程式碼

如果是OC的話直接括號強轉型別即可實現。

知道滑動的距離和記錄滑動前的距離倆者想加即是當前位置,轉化成CMTime型別:

self.sumTime = CMTimeAdd(self.sumTime!, addend)
複製程式碼

手勢是滑動了,但是進度條也是要跟著一起滑動的,有人說我把進度條重新整理放到player的代理裡面,手勢滑動完只需要把時間傳給播放器,播放器根據當前時間和總時間去更新進度條,這樣做也對,但是有一點就是,如果網速不好,手勢已經滑動到5分鐘了,而進度條還停留在1分鐘的地方,播放器快取完畢了,進度條會瞬間跳到5分鐘,從而造成卡頓的假象體驗也不是很好,所以解決這個方法是手勢滑動的時候也更新進度條,但是手勢滑動的時候都是CMTime型別,怎麼轉成Float型別,因為slider?.value是float型別。可以這樣:通過CMTimeGetSeconds方法得到一個Float64再通過Float.init方法得到一個float型別,看一下實現:

let sliderTime = CMTimeGetSeconds(self.sumTime!)/CMTimeGetSeconds(totalMovieDuration)
self.slider?.value = Float.init(sliderTime)
複製程式碼

想檢視整個過程可以看播放器手勢新增與建立這一塊,我已經用MARK:標記起來了。

設定控制皮膚資訊

在視訊播放過程中,對視訊的監聽是必不可少的,監聽播放器狀態,播放器快取...等,由於播放器比較簡單,功能較少,剛開始我只監聽了status屬性,後來我加上來loadedTimeRanges快取狀態,快取這部分的快取進度計算我已經實現了,但是沒有用到只是簡單的列印了一下。

在對播放器status屬性監聽中加入了控制皮膚資訊,是由MPNowPlayingInfoCenter來實現的,通過改變nowPlayingInfo裡面對應的資訊來更新皮膚資訊,裡面有好多屬性,比如MPMediaItemPropertyTitle設定音訊標題,MPMediaItemPropertyArtist作者、MPNowPlayingInfoPropertyElapsedPlaybackTime當前播放過的時間、MPMediaItemPropertyPlaybackDuration播放總時間等等。剛開始做的時候因為鎖屏要更新時間,而nowPlayingInfo又是一個字典型別的再加上需要更新介面佈局的時間和進度條,直接將播放器時間強制轉換成 string 型別,所以將這一部分放到了時間觀察裡面,因為時間觀察會一直進行所以鎖屏介面資訊也會一直更新,這樣帶來一個問題就是鎖屏介面的圖片如果是網路圖片,每1秒就要請求一下圖片而且要不斷的更新這樣帶來的結果可想而知。後來才知道,將MPNowPlayingInfoPropertyElapsedPlaybackTime屬性設定成self.player!.currentTime()播放器當前時間就會自動更新控制皮膚資訊,呼叫的地方也很關鍵,必須放在播放器已經播放的監聽裡面。

配置遠端控制顯示的資訊

響應遠端控制是由MPRemoteCommandCenter來實現的,裡面有很多屬性,比如:playCommand播放響應事件、pauseCommand 暫停響應事件、nextTrackCommand下一曲響應事件、likeCommand喜歡按鈕,類似網易雲音樂的那個鎖屏,如果設定了likeCommanddislikeCommand是上一首響應事件、previousTrackCommand上一首,外部拖動進度條是changePlaybackPositionCommand,系統有一個專門的方法來出來遠端拖動進度條響應事件:

open func addTarget(handler: @escaping (MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus) -> Any
複製程式碼

大概控制皮膚能用到的這些資訊差不多也就這麼多,如果想了解更多的可以看一下文件或者查閱資料。

螢幕旋轉問題

一個視訊播放實現起來並不困難,只要處理好playerplatitem就行了。最難的就是,如果手機螢幕旋轉,怎麼能讓視訊跟著螢幕自適應呢,我在工程裡面通過UIDevice變化新增的是螢幕旋轉監聽:

/**
* 監聽裝置旋轉通知
*/
private func listeningRotating() {
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
NotificationCenter.default.addObserver(self, selector: #selector(onDeviceOrientationChange), name:NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
複製程式碼

如果使用者把螢幕旋轉關掉,就是控制中心那個開關,使用者旋轉螢幕,怎麼能讓畫面跟著跑呢,我百度的很多資料,試了也很多方法,但是都不理想,用的還是OC的程式碼,因為swift裡面移除了NSInvocation屬性,用的依然是OC的螢幕強制旋轉,只能使用橋接檔案:

//這個方法是在網上找的
+ (void)interfaceOrientation:(UIInterfaceOrientation)orientation{
if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
SEL selector = NSSelectorFromString(@"setOrientation:");
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:[UIDevice currentDevice]];
int val = orientation;
// 從2開始是因為0 1 兩個引數已經被selector和target佔用
[invocation setArgument:&val atIndex:2];
[invocation invoke];
    }
}
複製程式碼

因為做的是視訊播放,所以進入後臺後視訊會暫停,這個屬於正常現象,如果在視訊模式下,進入後臺利用控制皮膚是無法將視訊播放的,如果在音訊模式下,進入後臺利用控制皮膚是可以讓視訊播放的。大概就介紹這麼多,一言半句也說得不是很明白,如果還有不明白的知識點可以去demo中自己去查,我也是一個初學者裡面很多東西都是查資料得來的並不能保證其內容的正確性。

相關文章