基于swift4.0实现视频播放、屏幕旋转、倍速播放、手势调节,锁屏面板等功能

3,894 阅读7分钟

学习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喜欢按钮,类似网易云音乐的那个锁屏,如果设置了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中自己去查,我也是一个初学者里面很多东西都是查资料得来的并不能保证其内容的正确性。