播放页面效果:
play.png实现了控件的自定义,手势快进,但是没有添加音量的改变的操作。
播放是使用了ijkplayer
没有使用AVPlayer
,了解ijkplayer
的封装成framework
可以去看ijkplayer封装
进入正题
文章只是提供了一种实现自定义的思路,有错误的地方和有更好的实现方案欢迎大家提出。
视频的读取是从iTunes
导入到Documents
文件夹下。
ijkplayer
播放视频只需要设置
var playerView : IJKMediaPlayback!
self.playerView = IJKFFMoviePlayerController.init(contentURL: URL.init(string: self.selectedFilms[self.currentUrl].fileUrl)!, with: nil)
self.view.insertSubview(self.playerView.view, at: 0)
播放功能就这样的实现啦!
定时器
观看进度需要实时的改变,观看进度也需要改变,所以需要一个定时器
self.timer = Timer.init(timeInterval: 1, target: self, selector: #selector(MXPlayViewController.nextSecond), userInfo: nil, repeats: true)
//添加到主循环
RunLoop.main.add(self.timer, forMode: RunLoopMode.defaultRunLoopMode)
但是什么时候启动最好呢?启动太早和太晚可能时间会不匹配。
player
有一个状态监听,它可能出现的状态为:
IJKMPMoviePlaybackStateStopped, 停止
IJKMPMoviePlaybackStatePlaying, 正在播放
IJKMPMoviePlaybackStatePaused, 暂停
IJKMPMoviePlaybackStateInterrupted, 打断
IJKMPMoviePlaybackStateSeekingForward, 快进
IJKMPMoviePlaybackStateSeekingBackward 快退
在开始播放的时候启动定时器是正好的:
switch self.playerView.playbackState {
case .stopped:
//播放完成
//置空
self.timer.invalidate()
self.timer = nil
//设置播放完成
self.isComplete = true
self.isSeeking = false
//自动播放下一个资源
self.slideNextSource()
break
case .paused:
//暂停
//停止计时器
self.isComplete = false
self.isSeeking = false
break
case .interrupted:
//中断,资源问题
break
case .seekingBackward:
//后退
self.isComplete = false
self.isSeeking = true
self.playAndSettingBottomView()
break
case .seekingForward:
//快进
self.isComplete = false
self.isSeeking = true
self.playAndSettingBottomView()
break
default:
//playing
self.isComplete = false
self.isSeeking = false
self.playAndSettingBottomView()
//设置进度
if self.isFirstEnter && !self.isBecomeActive{
//必须在加载完毕之后才能设置
//设定播放位置
//指定位置开始播放
if self.currentTime != 0 {
//从头开始,如果看到最后一秒
if self.currentTime >= Int.init(self.playerView.duration) {
self.currentTime = 0
self.selectedFilms[self.currentUrl].seeingTime = 0
}
}
self.isFirstEnter = false
}
break
}
滑动手势监听,然后根据移动的距离转换时间的秒数:
(translation / self.playerView.frame.size.width ) * 总时间
添加手势:
//滑动手势self.playerView.view.addGestureRecognizer(UIPanGestureRecognizer.init(target: self, action: #selector(MXPlayViewController.translationToSeekTime(gesture:))))
//获取位移
let translation = gesture.translation(in: self.playerView.view)
//前进/后退的秒数,有正负之分
let forwardSecond = translation.x
//转换算法
let scale = forwardSecond / self.playerView.view.frame.size.width
let destinationSecond = Double(scale) * self.playerView.duration
//时间
var time = self.currentTime + Int(destinationSecond)
//判断时间是否过线
if time < 0 {
time = 0
}else if time > Int(self.playerView.duration){
time = Int(self.playerView.duration)
}
let destinationTime = MXFilmViewModel.durationToTime(time: time)
self.operationProgressLabel.text = String.init(format: "%@/%@", destinationTime,MXFilmViewModel.durationToTime(time: Int(self.playerView.duration)))
有一个需要注意的是,滑动的时候应该停止定时器的运行,所以在手势的回调判断状态
switch gesture.state {
case .began:
//快进不需要停止
//停止定时器
//self.timer.invalidate()
//self.timer = nil
//显示进度提示1
self.operationProgressLabel.alpha = 0.6
//当前时间
let currentTimeString = MXFilmViewModel.durationToTime(time: self.currentTime)
self.operationProgressLabel.text = String.init(format: "%@/%@", currentTimeString,currentTimeString)
break
case .cancelled:
break
case .changed:
//改变值
//获取位移
let translation = gesture.translation(in: self.playerView.view)
//前进/后退的秒数,有正负之分
let forwardSecond = translation.x
//转换算法
let scale = forwardSecond / self.playerView.view.frame.size.width
let destinationSecond = Double(scale) * self.playerView.duration
//时间
var time = self.currentTime + Int(destinationSecond)
//判断时间是否过线
if time < 0 {
time = 0
}else if time > Int(self.playerView.duration){
time = Int(self.playerView.duration)
}
let destinationTime = MXFilmViewModel.durationToTime(time: time)
self.operationProgressLabel.text = String.init(format: "%@/%@", destinationTime,MXFilmViewModel.durationToTime(time: Int(self.playerView.duration)))
break
case .ended:
//停止显示快进/快退提示
self.operationProgressLabel.alpha = 0
if self.playerView.isPlaying() {
//判断是左移还是右移
//获取位移
let translation = gesture.translation(in: self.playerView.view)
//前进/后退的秒数,有正负之分
let forwardSecond = translation.x
//转换算法
let scale = forwardSecond / self.playerView.view.frame.size.width
var destinationSecond = Double(scale) * self.playerView.duration
//判断时间是否符合
if destinationSecond + self.playerView.currentPlaybackTime > self.playerView.duration {
destinationSecond = self.playerView.duration - self.playerView.currentPlaybackTime - 1
}else if destinationSecond + self.playerView.currentPlaybackTime < 0 {
destinationSecond = -self.playerView.currentPlaybackTime
}
//执行快进
self.playerView.currentPlaybackTime += destinationSecond
//同时设置相关属性
self.selectedFilms[self.currentUrl].seeingTime += Int(destinationSecond)
self.currentTime += Int(destinationSecond)
//要通知BottomView进行进度条移动,快进了多少秒
//改变ToalSecond和mutile约束改变倍率
self.bottomView.totalSecond -= Int(destinationSecond)
self.bottomView.multiple = CGFloat(destinationSecond)
//
}
break
case .failed:
break
default:
break
}
控制视图在一定时间内会隐藏,然后监听屏幕的点击事件进行隐藏或者显示。
self.playerView.view.addGestureRecognizer(UITapGestureRecognizer.init(target: self, action: #selector(MXPlayViewController.tapInPlayView)))
func tapInPlayView(){
if self.isShowsControlTool {
//隐藏
self.hideAnimation()
}else{
//显示
self.showsAnimation()
//启动一个延时操作,如果没有是手动取消
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: {
//隐藏
if self.isShowsControlTool {
//隐藏
self.hideAnimation()
}
})
}
self.isShowsControlTool = !self.isShowsControlTool
}
因为有了滑动快进之后,所以下面的bottomView
左右按钮点击我设置成了source
切换。
topView
,bottomView
分别创建两个View去控制,然后通过代理回调。
创建一个枚举,设置可能的操作类型
enum MXPlayBottomOperationType {
case left
case right
case start
case pause
case seekTime
}
//前进
func goNext(){
//回退
//通知代理切换位置
if self.delegate != nil {
self.delegate.operation(with: self, type: .right, seekTime: nil, completion: {
//刷新UI
})
}
}
//后退
func backFormer(){
//回退
//通知代理切换位置
if self.delegate != nil {
self.delegate.operation(with: self, type: .left, seekTime: nil, completion: {
//刷新UI
})
}
}
//开始/暂停
func startOrpause(){
if self.isPlaying {
//停止
//设置按钮
self.playOrPauseButton.setBackgroundImage(UIImage.init(named: "pause"), for: UIControlState.normal)
//通知代理切换位置
if self.delegate != nil {
self.delegate.operation(with: self, type: .pause, seekTime: nil, completion: {
//刷新UI
})
}
}else{
//开始
//设置按钮
self.playOrPauseButton.setBackgroundImage(UIImage.init(named: "start"), for: UIControlState.normal)
//通知代理切换位置
if self.delegate != nil {
self.delegate.operation(with: self, type: .start, seekTime: nil, completion: {
//刷新UI
})
}
}
self.isPlaying = !self.isPlaying
}
切换资源的时候应该保存进度到数据库
//slide source
func slideNextSource(){
//只有一个电影的时候
guard self.selectedFilms.count != 1 else {
SVProgressHUD.showError(withStatus: "没有更多电影了")
self.settingTimer()
return
}
//先停止播放
self.playerView.shutdown()
//刷新并保存
self.updateAndSave()
//加载下一个资源
self.currentUrl += 1
if self.currentUrl >= self.selectedFilms.count {
//提示信息
SVProgressHUD.showError(withStatus: "没有更多电影了")
self.currentUrl = 0
}
//改变属性
self.isFirstEnter = true
self.isNewSource = true
//加载
self.playMovie()
//修改状态栏
self.topView.titleLable.text = self.selectedFilms[self.currentUrl].name
//当前进度什么都需要更改
//播放
if !self.playerView.isPlaying() {
self.playerView.prepareToPlay()
}
}
func slideFormerSource(){
//只有一个电影的时候
guard self.selectedFilms.count != 1 else {
SVProgressHUD.showError(withStatus: "没有更多电影了")
self.settingTimer()
return
}
//先停止播放
self.playerView.shutdown()
//刷新并保存
self.updateAndSave()
//加载下一个资源
self.currentUrl -= 1
if self.currentUrl < 0{
//提示信息
SVProgressHUD.showError(withStatus: "没有更多电影了,重新播放")
self.currentUrl = self.selectedFilms.count - 1
}
self.isFirstEnter = true
self.isNewSource = true
//加载
self.playMovie()
//修改状态栏
self.topView.titleLable.text = self.selectedFilms[self.currentUrl].name
//播放
if !self.playerView.isPlaying() {
self.playerView.prepareToPlay()
}
}
保存到数据库的方法为:
func updateAndSave(){
//添加到数据库
if self.isComplete {
self.selectedFilms[self.currentUrl].seeingTime = 0
}
//就刷新
self.selectedFilms[self.currentUrl].isSeeing = true
operationDataBase(film: self.selectedFilms[self.currentUrl], type: .updateSeeingTime)
}
这就是播放的核心代码了,完整代码上传到了GitHub上,大家可以下载看看。
完整的项目包括了播放分类,主要是区分了未播放,历史播放,
加密的栏目,自定义转场动画,布局是使用纯代码布局的,没有使用storyboard。
已经发布在了App Store,
地址为(https://itunes.apple.com/us/app/mxplayer/id1211708601?l=zh&ls=1&mt=8)。
new.png
history.png
security.png
网友评论