AVPlayer是AVFoundation的核心类之一,主要的功能是用于做多媒体的播放控制,支持大部分的音频、视频文件格式,AVPlayer是用于视频播放的不二人选,本文不对AVPlayer的基础知识做介绍,直接进入我们的开发实战中来,涉及到UI的搭建,播放器的控制,帧图片的获取,媒体资源的加载进度等
注:本文代码块均使用Swift进行编写
1、控制器的实现
新建一个项目,取名:HJWealthT,再新建一个Swift文件取名:HJVideoViewController,本文继承于HJBaseViewController,HJBaseViewController继承于UIViewController,是本项目所有控制器的根控制器
然后我们给定一个场景:在cell上进行视频的播放。那么我们需要在控制器中添加一个UITableView,再准备一些播放视频的URL数组。使用懒加载实现两个内部属性:contentView,videoArray,并遵守协议:UITableViewDelegate,UITableViewDataSource
class HJVideoViewController: HJBaseViewController,UITableViewDelegate,UITableViewDataSource {
//MARK: - 内部属性
private lazy var contentView: UITableView = {
let temp = UITableView.init(frame: CGRect.init(x: 0, y: HJMacro_sextyFourHeight, width: HJMacro_ScreenWidth, height: HJMacro_ScreenHeight - HJMacro_sextyFourHeight), style: .plain)
temp.delegate = self
temp.dataSource = self
return temp
}()
/// 视频数据
private lazy var videoArray: NSMutableArray = {
let temp = NSMutableArray.init()
return temp
}()
}
在viewDidLoad方法中注册cell,添加contentView并实现数据的加载
//MARK: - 重写方法
override func viewDidLoad() {
super.viewDidLoad()
self.addBackButton()
self.setNavigation(title: "视频")
//注册cell
self.contentView.register(UINib.init(nibName: "HJVideoCell", bundle: nil), forCellReuseIdentifier: "HJVideoCell")
self.view.addSubview(self.contentView)
//数据
let array = NSArray.init(array:
["http://wvideo.spriteapp.cn/video/2017/1126/fb6917b4-d2a9-11e7-9e71-90b11c479401_wpd.mp4",
"http://wvideo.spriteapp.cn/video/2017/1127/5790f2b6-d2c6-11e7-ae90-1866daeb0df1cutblack_wpd.mp4",
"http://wvideo.spriteapp.cn/video/2017/1202/5a228a358cbc0_wpd.mp4",
"http://wvideo.spriteapp.cn/video/2017/1203/36bc8e86d7dc11e7995b842b2b4c75ab_wpd.mp4",
"http://wvideo.spriteapp.cn/video/2017/1203/4391821ad7d211e793da842b2b4c75ab_wpd.mp4",
"http://wvideo.spriteapp.cn/video/2017/1202/ac98b61e-d702-11e7-a824-1866daeb0df1_wpd.mp4",
"http://wvideo.spriteapp.cn/video/2017/1202/5dafd8d4d74311e7b9c8842b2b4c75ab_wpd.mp4"
])
self.videoArray.removeAllObjects()
self.videoArray.addObjects(from: array as! [Any])
}
实现代理方法UITableViewDelegate,UITableViewDataSource
//MARK: - UITableViewDelegate,UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.videoArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:HJVideoCell = tableView.dequeueReusableCell(withIdentifier: "HJVideoCell", for: indexPath) as! HJVideoCell
cell.index = indexPath
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
return false
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 300
}
2、HJVideoCell UI的搭建
所有我们简单的实现"HJVideoCell",HJVideoCell继承于UITableViewCell,我们通过xib实现视图的搭建,如图1:
HJVideoCell代码的实现:index是外部传入的属性,当"播放按钮"点击时实现一个闭包用于HJVideoViewController中实现播放器的添加
class HJVideoCell: UITableViewCell {
//MARK: - 外部属性
var index:IndexPath?
var playButtonBlock:((_ sender:UIButton,_ index:IndexPath)->Void)?
@IBOutlet weak var backgroundImageView: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
@IBAction func playButtonDidClicked(_ sender: UIButton) {
if (self.playButtonBlock != nil && self.index != nil) {
self.playButtonBlock!(sender,self.index!)
}
}
}
回到HJVideoViewController的 tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell方法中,需要在播放按钮点击的时候创建一个播放视图,
第一步:添加一个内部属性 playerView
class HJVideoViewController: HJBaseViewController,UITableViewDelegate,UITableViewDataSource {
//MARK: - 内部属性
//....
/// 播放视图
private var playerView:HJVideoPlayView?
}
第二步:播放按钮点击的时将playerView添加到cell的contentView上
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:HJVideoCell = tableView.dequeueReusableCell(withIdentifier: "HJVideoCell", for: indexPath) as! HJVideoCell
cell.index = indexPath
//播放按钮点击
cell.playButtonBlock = {
(sender:UIButton,index:IndexPath)->Void in
//添加播放视图
self.playerView = HJVideoPlayView.singleView
self.playerView?.frame = cell.backgroundImageView.frame
self.playerView?.videoUrl = self.videoArray[index.row] as? String
cell.contentView.addSubview(self.playerView!)
}
return cell
}
3、创建播放器视图
新建一个Swift文件,取名:HJVideoPlayView,继承于UIView,使用xib进行UI布局,如图2:
视频播放视图我们运用单例模式来进行创建,保证只有一个播放视图,第一步:创建videoPlayView类方法,返回视图本身HJVideoPlayView,第二步:申明一个全局常量singleHJVideoPlayView,第三步:申明一个类变量,返回 singleHJVideoPlayView常量。
创建一个变量用于接收url地址,并重写set,get方法;导入AVFoundation框架,创建变量:asset,playerItem,player,playerLayer
import UIKit
import AVFoundation
let singleHJVideoPlayView:HJVideoPlayView = HJVideoPlayView.videoPlayView()
class HJVideoPlayView: UIView{
//MARK: - 外部属性
/// url地址
var videoUrl:String? {
set {
self.priVideoUrl = newValue
self.addPlayer()
}
get {
return nil
}
}
/// 单利
class var singleView:HJVideoPlayView {
return singleHJVideoPlayView
}
//MARK: - 内部属性
//播放
private var asset:AVURLAsset?
private var playerItem:AVPlayerItem?
private var player:AVPlayer?
private var playerLayer:AVPlayerLayer?
/// 视频url
private var priVideoUrl:String?
/// 全屏按钮
@IBOutlet weak var fullScreenButton: UIButton!
/// 小图片
@IBOutlet weak var currentImageView: UIImageView!
/// 滑动时间显示
@IBOutlet weak var centerSlipTimeLabel: UILabel!
/// 下方10%透明视图
@IBOutlet weak var bottomView: UIView!
//播放暂停按钮
@IBOutlet weak var playOrPauseButton: UIButton!
/// 总时间
@IBOutlet weak var durationTimeLabel: UILabel!
/// 当前时间
@IBOutlet weak var currentTimeLabel: UILabel!
/// 进度条
@IBOutlet weak var timeSlider: UISlider!
/// 缓存进度
@IBOutlet weak var cacheProgressView: UIProgressView!
/// 停止按钮
@IBOutlet weak var stopButton: UIButton!
class func videoPlayView() -> HJVideoPlayView {
return Bundle.main.loadNibNamed("HJVideoPlayView", owner: nil, options: nil)?.first as! HJVideoPlayView
}
//....
重写awakeFromNib方法,做一些需要的UI控制,centerSlipTimeLabel用于滚动条拖动时显示当前时间,currentImageView用于滚动条拖动时显示当前帧图片
//MARK: - 重写方法
override func awakeFromNib() {
super.awakeFromNib()
self.centerSlipTimeLabel.isHidden = true
self.currentImageView.isHidden = true
self.currentImageView.layer.masksToBounds = true
self.currentImageView.layer.cornerRadius = 4
self.backgroundColor = UIColor.black
//添加播放器
self.addPlayer()
}
/// 添加播放器
private func addPlayer() {
if self.priVideoUrl != nil {
self.asset = AVURLAsset.init(url: URL.init(string: self.priVideoUrl!)!)
self.playerItem = AVPlayerItem.init(asset: self.asset!, automaticallyLoadedAssetKeys: nil)
// 设置播放器和播放layer
if self.player != nil || self.playerLayer != nil {
self.stop()
}
self.player = AVPlayer.init(playerItem: self.playerItem!)
self.playerLayer = AVPlayerLayer.init(player: self.player)
self.playerLayer?.backgroundColor = UIColor.black.cgColor
self.playerLayer?.videoGravity = .resizeAspect
self.playerLayer?.masksToBounds = true
self.playerLayer?.frame = self.bounds
self.layer.insertSublayer(self.playerLayer!, at: 0)
//总时间
self.durationTimeLabel.text = self.formatTime(time:(self.asset?.duration)!)
//获取当前时间
self.currentTimeLabel.text = self.formatTime(time: (self.playerItem?.currentTime())!)
self.player?.addPeriodicTimeObserver(forInterval: CMTime.init(value: 1, timescale: (self.asset?.duration.timescale)!), queue: DispatchQueue.main, using: { (time) in
self.currentTimeLabel.text = self.formatTime(time: time)
self.timeSlider.value = Float(CMTimeGetSeconds(time) / CMTimeGetSeconds((self.asset?.duration)!))
})
//处理滑块
self.timeSlider.addTarget(self, action: #selector(timeSliderValueChange(slider:)), for: .valueChanged)
}
}
对时间进行格式化
//MARK: - 时间格式化
/// 时间格式化
///
/// - Parameter time:
/// - Returns:
private func formatTime(time:CMTime)->String {
let hour:NSInteger = NSInteger((time.value/Int64(time.timescale)) / 3600)
let min:NSInteger = NSInteger((time.value/Int64(time.timescale)) / 60) % 60
let second = (time.value/Int64(time.timescale)) % 60
let str:String?
if hour > 0 {
str = NSString.init(format: "%02d:%02d:%02d", hour,min,second) as String
} else {
str = NSString.init(format: "%02d:%02d",min,second) as String
}
return str!
}
拖动滑块时,添加target,监听valueChanged;在方法中去跟新进度条的值,获取当前帧图片,当前时间,self.player调用seek方法来设置视频播放的当前时间
/// 滑动滑块
///
/// - Parameter slider:
@objc private func timeSliderValueChange(slider:UISlider) {
if self.player?.status == AVPlayerStatus.readyToPlay {
//更新时间进度条
self.timeSlider.value = slider.value
let currentTime = CMTimeMake(Int64(slider.value*Float((self.playerItem?.duration.value)!)), (self.playerItem?.duration.timescale)!)
//我们去获取当前帧图片
var tempImage:UIImage?
if self.getCurrentTimeImage(currentTime: currentTime) != nil {
tempImage = (self.getCurrentTimeImage(currentTime: currentTime))!
}
self.currentImageView.image = tempImage
self.currentImageView.isHidden = false
self.centerSlipTimeLabel.text = self.formatTime(time: currentTime)
self.centerSlipTimeLabel.isHidden = false
self.currentTime = currentTime
self.player?.seek(to: self.currentTime!, completionHandler: { (complet) in
if complet {
self.currentImageView.isHidden = true
self.centerSlipTimeLabel.isHidden = true
}
})
}
}
/// 获取图片
///
/// - Parameter currentTime: 当前时间
/// - Returns: 返回一张image
func getCurrentTimeImage(currentTime:CMTime) -> UIImage? {
//获取AVAssetImageGenerator
var imageRef:CGImage?
var newImage:UIImage?
let assetImageG = AVAssetImageGenerator.init(asset: self.asset!)
assetImageG.appliesPreferredTrackTransform = true
assetImageG.apertureMode = AVAssetImageGeneratorApertureMode.encodedPixels
do {
imageRef = try assetImageG.copyCGImage(at: currentTime, actualTime: nil)
} catch {
print("获取图片失败")
}
if imageRef != nil {
newImage = UIImage.init(cgImage: imageRef!)
}
return newImage
}
回到addPlayer()方法中,去监听playerItem的播放状态,覆写监听的回调方法,判断playerItem的状态如果为readyToPlay,调用本类play方法
private func addPlayer() {
if self.priVideoUrl != nil {
//...
//添加监听
//播放的状态
self.playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil)
}
}
//MARK: - 监听回调
/// 监听回调
///
/// - Parameters:
/// - keyPath:
/// - object:
/// - change:
/// - context:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "status" {
if self.playerItem?.status == AVPlayerItemStatus.readyToPlay {
self.play()
}
}
}
播放和暂停方法,由于业务的需求,在视频未播放的时候是不能点击全屏按钮和拖动滑块的,所以在播放方法中,我需要去将全屏按钮和滑块的用户交互打开
/// 播放
func play() {
if self.player == nil {
self.addPlayer()
}
self.player?.play()
self.fullScreenButton.isUserInteractionEnabled = true
self.timeSlider.isUserInteractionEnabled = true
if self.playOrPauseButton.currentTitle == "播放" {
self.playOrPauseButton.setTitle("暂停", for: .normal)
}
}
/// 暂停
func pause() {
self.player?.pause()
if self.playOrPauseButton.currentTitle == "暂停" {
self.playOrPauseButton.setTitle("播放", for: .normal)
}
}
停止播放时调用暂停播放,控制进度条和全屏按钮用户交互,并将播放的时间设置为0,将player置为空,移除playerLayer视图
/// 停止
func stop() {
self.pause()
// 控制按钮
self.timeSlider.isUserInteractionEnabled = false
self.fullScreenButton.isUserInteractionEnabled = false
self.player?.seek(to: CMTimeMake(0, 1), completionHandler: { (complet) in
if complet {
self.currentTimeLabel.text = self.formatTime(time: kCMTimeZero)
}
})
self.player = nil
self.playerLayer?.removeFromSuperlayer()
}
这个时候,如果运行这个项目,是可以正常进行播放了,接下来我们需要对按钮的操作进行实现,以及程序切换到回台、前台的一些响应操作,以及实现一个缓冲进度条等
4播放视图按钮的操作实现
播放、暂停、停止操作,在播放或者暂停时,我们可以点击停止,所以打开了停止按钮的用户交互;在停止按钮点击了之后我们不希望再点击停止按钮,所以关闭了按钮的用户交互
//MARK: - 点击事件
/// 播放暂停
///
/// - Parameter sender:
@IBAction private func playOrPauseButtonDidClicked(_ sender: UIButton) {
self.stopButton.isUserInteractionEnabled = true
if sender.currentTitle == "暂停" {
self.pause()
} else if sender.currentTitle == "播放" {
self.play()
}
}
/// 停止播放
///
/// - Parameter sender:
@IBAction private func stopButtonDidClicked(_ sender: UIButton) {
sender.isUserInteractionEnabled = false
self.stop()
}
给视图添加点击手势,回到awakeFromNib方法,将视图的用户交互打开,添加一个点击手势,实现selector中的isHiddenOtherView方法,方法中用到了isHiddenOther这个变量来做判断,所以我们需要申明isHiddenOther这个变量,类型是Bool?类型,bottomView指的是底部的一条%10透明视图,我们的播放按钮、时间、进度以及停止按钮都在bottomView视图中
/// 是否隐藏其他试图
private var isHiddenOther:Bool?
//MARK: - 重写方法
override func awakeFromNib() {
//...
//添加手势
self.isUserInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer.init(target: self, action: #selector(self.isHiddenOtherView)))
}
/// 是否显示其他试图
@objc private func isHiddenOtherView() {
if self.isHiddenOther != nil && self.isHiddenOther == true {
self.isHiddenOther = false
} else {
self.isHiddenOther = true
}
self.bottomView.isHidden = self.isHiddenOther!
self.fullScreenButton.isHidden = self.isHiddenOther!
}
接下来实现全屏按钮的控制,首先控制文字显示,然后申明一个闭包用于外部调用,创建一个改变playerLayer尺寸的方法用于外部来调用
///是否全屏闭包
var fullScreenButtonBlock:(()->Void)?
/// 全屏
///
/// - Parameter sender:
@IBAction func fullScreenButtonDidClicked(_ sender: UIButton) {
if sender.currentTitle! == "全屏" {
sender.setTitle("返回", for: .normal)
} else if sender.currentTitle! == "返回" {
sender.setTitle("全屏", for: .normal)
}
if fullScreenButtonBlock != nil {
self.fullScreenButtonBlock!()
}
}
/// 改变playerLayer的size
///
/// - Parameter size:
func changePlayerLayerWith(size:CGSize) {
self.playerLayer?.removeFromSuperlayer()
self.playerLayer?.frame.size = size
self.layer.insertSublayer(self.playerLayer!, at: 0)
}
回到HJVideoViewController的tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell方法中来去实现闭包,判断当前设置的方向,改变当前设备方向,设置是否显示状态栏,控制playerView的frame,playerLayer的frame,playerView添加到哪个视图上。代码中我们用到了
isShowStatusBar,这是用于是否显示状态栏创建的变量,所以需要添加一个新的变量
/// 是否显示状态栏
private var isShowStatusBar:Bool?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//...
self.playerView?.fullScreenButtonBlock = {
()->Void in
if UIDevice.current.orientation == .landscapeLeft {
//改变方向-->返回
let tempNumber:NSNumber = NSNumber.init(value: Int8(UIDeviceOrientation.portrait.rawValue))
UIDevice.current.setValue(tempNumber, forKey: "orientation")
self.isShowStatusBar = true
self.setNeedsStatusBarAppearanceUpdate()
//接收到UIDeviceOrientationDidChange通知之后
self.playerView?.removeFromSuperview()
self.playerView?.frame = cell.backgroundImageView.frame
self.playerView?.changePlayerLayerWith(size: (self.playerView?.frame.size)!)
cell.contentView.addSubview(self.playerView!)
} else {
//改变方向-->全屏
let tempNumber:NSNumber = NSNumber.init(value: Int8(UIDeviceOrientation.landscapeLeft.rawValue))
UIDevice.current.setValue(tempNumber, forKey: "orientation")
self.isShowStatusBar = false
self.setNeedsStatusBarAppearanceUpdate()
//接收到UIDeviceOrientationDidChange通知之后
self.playerView?.removeFromSuperview()
self.playerView?.frame = CGRect.init(x: 0, y: 0, width: HJMacro_ScreenWidth, height: HJMacro_ScreenHeight)
self.playerView?.changePlayerLayerWith(size:CGSize.init(width: HJMacro_ScreenHeight, height: HJMacro_ScreenWidth))
self.view.addSubview(self.playerView!)
}
//...
}
在调用了 self.setNeedsStatusBarAppearanceUpdate()之后,为了让状态栏在我们需要的时候显示隐藏,需要去重写prefersStatusBarHidden,shouldAutorotate
override var shouldAutorotate: Bool {
return false
}
/// 显示隐藏状态栏
override var prefersStatusBarHidden: Bool {
get {
if self.isShowStatusBar != nil {
if self.isShowStatusBar! == true {
return false
} else {
return true
}
} else {
return false
}
}
}
回到viewDidLoad方法中去注册通知,监听当前设备的方向已经发生改变
//MARK: - 重写方法
override func viewDidLoad() {
//...
//注册通知
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
NotificationCenter.default.addObserver(self, selector: #selector(self.deviceOrientationDidChange), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
实现selector调用的方法deviceOrientationDidChange,竖屏和横屏去做动画,去改变bounds,最后别忘记,移除通知
//MARK: - 通知
@objc private func deviceOrientationDidChange() {
if UIDevice.current.orientation == .portrait {
//竖屏
self.orientationChange(change: false)
} else if UIDevice.current.orientation == .landscapeLeft {
self.orientationChange(change: true)
}
}
private func orientationChange(change:Bool) {
if change == true {
//横屏
UIView.animate(withDuration: 0.25, animations: {
self.playerView?.transform = CGAffineTransform.init(rotationAngle: CGFloat(Double.pi / 2))
self.view.bounds = CGRect.init(x: 0, y: 0, width: HJMacro_ScreenWidth, height: HJMacro_ScreenHeight)
}, completion: { (complet) in
})
} else {
//竖屏
UIView.animate(withDuration: 0.25, animations: {
self.playerView?.transform = CGAffineTransform.init(rotationAngle: CGFloat(0))
self.view.bounds = CGRect.init(x: 0, y: 0, width: HJMacro_ScreenWidth, height: HJMacro_ScreenHeight)
}, completion: { (complet) in
})
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
根据既定需求,当cell滚动出屏幕时我们需要移除播放视图,实现UITableViewDelegate的tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)方法
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if self.playerView != nil {
//如果 playerView 存在,并在某一个cell上,划出屏幕就移除并停止播放
if cell.contentView.subviews.contains(self.playerView!) {
self.playerView?.removeFromSuperview()
self.playerView?.stop()
}
}
}
5、完善我们的播放器
首先为我们的播放器实现缓冲机制,回到addPlayer方法中,注册观察者,在observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)方法中判断keyPath是否为"loadedTimeRanges",如果字符串相等,调用changeCacheProgressView方法进行缓冲处理
private func addPlayer() {
if self.priVideoUrl != nil {
//...
//添加监听
//缓存
self.playerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)
}
}
//MARK: - 监听回调
/// 监听回调
///
/// - Parameters:
/// - keyPath:
/// - object:
/// - change:
/// - context:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "status" {
//...
} else if keyPath == "loadedTimeRanges" {
self.changeCacheProgressView()
}
}
/// 缓冲处理
private func changeCacheProgressView() {
//获取缓冲
let cacheArray = self.playerItem?.loadedTimeRanges
if cacheArray != nil && cacheArray!.count > 0 {
let cacheTimeRange:CMTimeRange = cacheArray?.first as! CMTimeRange
//获取开始时间
let startTime = CMTimeGetSeconds(cacheTimeRange.start)
let durationTime = CMTimeGetSeconds(cacheTimeRange.duration)
let allCacheTime = startTime + durationTime
self.cacheProgressView.progress = Float(allCacheTime / CMTimeGetSeconds((self.asset?.duration)!))
}
}
实现视频播放完成的回调以及应用程序回到前台的监听,回到addPlayer,注册观察者,监听AVPlayerItemDidPlayToEndTime和UIApplicationWillEnterForeground
private func addPlayer() {
if self.priVideoUrl != nil {
//通知
//播放完成
NotificationCenter.default.addObserver(self, selector: #selector(self.playEnd(notifi:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
//app回到前台
NotificationCenter.default.addObserver(self, selector: #selector(self.willEnterForeground), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
}
}
实现playEnd(notifi:NSNotification)方法和willEnterForeground()方法,最后别忘记,移除观察者
/// 播放完成
///
/// - Parameter notifi:
@objc private func playEnd(notifi:NSNotification) {
print(notifi)
self.stop()
}
/// app回到前台
@objc private func willEnterForeground() {
self.play()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
一个简单的视频播放器就搞定了,欢迎持续的关注后续的文章,你的持续关注就是我前进的动力O(∩_∩)O哈哈~
来这里试试视频播放吧:项目地址