学习 swift 有段时间了, 原来写过一个基于 swift 3.0 的视频播放, 后来有同学联系我说, 在音频锁屏的情况下, 无法用控制面板拖动进度条调节播放进度, 所以又将原来的代码拿过来重新整理了下也顺便更新到了 4.0 版本.在把原来的代码拿来的时候发现原来有好多地方都是错误的,原来在 OC 项目里面已经写过一遍关于视频播放的东西所以就按照原来的逻辑写了 swift 版本,其实里面很多代码我也是通过查找资料和看文档拼凑出来的,对于 swift 的语句也是一知半解,希望各位看官多多包涵.
先来看一下实现的效果,一图胜千言(第一张是 iOS 10 系统,第二张是 iOS 11 系统).
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 喜欢按钮,类似网易云音乐的那个锁屏,如果设置了 likeCommand 则 dislikeCommand 是上一首响应事件,
previousTrackCommand
上一首,外部拖动进度条是
changePlaybackPositionCommand
,系统有一个专门的方法来出来远程拖动进度条响应事件:
open func addTarget(handler: @escaping (MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus) -> Any
大概控制面板能用到的这些信息差不多也就这么多,如果想了解更多的可以看一下文档或者查阅资料.
屏幕旋转问题
一个视频播放实现起来并不困难,只要处理好 player 与 platitem 就行了.最难的就是,如果手机屏幕旋转,怎么能让视频跟着屏幕自适应呢,我在工程里面通过 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 中自己去查,我也是一个初学者里面很多东西都是查资料得来的并不能保证其内容的正确性.
来源: https://juejin.im/post/5a670ff1518825733707cd15