Swift iOS - 当应用程序从后台返回时,AVPlayer 视频冻结/暂停
Swift iOS -AVPlayer Video Freezes / Pauses When App Comes Back from Background
我的应用程序登录页面上有一个循环播放的视频。我按照这个 Youtube 教程进行操作 loop video in view controller
问题是当应用程序进入后台时,如果我不立即返回,当我返回时视频会被冻结。
根据 Apple Docs 应该发生的情况。
我尝试使用 NotificationCenter 的 Notification.Name.UIApplicationWillResignActive
但没有用。
如何让视频在应用 returns 从后台继续播放后继续播放?
var player: AVPlayer!
var playerLayer: AVPlayerLayer!
override func viewDidLoad() {
super.viewDidLoad()
configurePlayer()
}
@objc fileprivate func configurePlayer(){
let url = Bundle.main.url(forResource: "myVideo", withExtension: ".mov")
player = AVPlayer.init(url: url!)
playerLayer = AVPlayerLayer(player: player!)
playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
playerLayer.frame = view.layer.frame
player.actionAtItemEnd = AVPlayerActionAtItemEnd.none
player.play()
view.layer.insertSublayer(playerLayer, at: 0)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemReachedEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemReachedEnd), name: Notification.Name.UIApplicationWillResignActive, object: player.currentItem)
}
@objc fileprivate func playerItemReachedEnd(){
player.seek(to: kCMTimeZero)
}
添加观察者
func addPlayerNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidPlayToEnd), name: .AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground), name: .UIApplicationWillEnterForeground, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground), name: .UIApplicationDidEnterBackground, object: nil)
}
移除观察者
func removePlayerNotifations() {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIApplicationWillEnterForeground, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIApplicationDidEnterBackground, object: nil)
}
方法
// Player end.
@objc func playerItemDidPlayToEnd(_ notification: Notification) {
// Your Code.
player.seek(to: kCMTimeZero)
}
//App enter in forground.
@objc func applicationWillEnterForeground(_ notification: Notification) {
player.play()
}
//App enter in forground.
@objc func applicationDidEnterBackground(_ notification: Notification) {
player.pause()
}
试试这个代码
根据 Apple Docs 当视频正在播放并且应用程序被发送到后台时,播放器会自动暂停:
他们说要做的是在应用程序进入后台时删除 AVPlayerLayer
(设置为 nil),然后在进入前台时重新初始化它:
他们说处理这个问题的最好方法是 applicationDidEnterBackground
和 applicationDidBecomeActive
:
我使用 NSNotification 来侦听背景和前景事件并设置函数以暂停播放器并将 playerLayer 设置为 nil(均用于背景事件),然后重新初始化 playerLayer 并为前景事件播放播放器。这些是我使用的通知 .UIApplicationWillEnterForeground
和 .UIApplicationDidEnterBackground
我发现,出于某种原因,如果您长按主屏幕按钮并弹出显示 "What can I help you with" 的屏幕,如果您再次按主屏幕按钮返回对于您的应用程序,视频将被冻结,并且使用上面的 2 个通知不会阻止它。我发现防止这种情况的唯一方法是 也 使用通知 .UIApplicationWillResignActive
和 .UIApplicationDidBecomeActive
。如果除了上述通知之外您不添加这些,那么您的视频将在主页按钮长按并返回时冻结。我发现防止所有冻结情况的最佳方法是使用所有 4 个通知。
我必须做的与上面的代码不同的 2 件事是将 player 和 playerLayer class 变量设置为可选变量而不是隐式展开的可选变量,我还添加了 AVPlayer class 的扩展到检查它是否在 iOS 9 或以下版本中播放。在 iOS 10 及更高版本中有一个内置方法 .timeControlStatus
AVPlayer timer status
我上面的代码:
var player: AVPlayer?
var playerLayer: AVPlayerLayer?
在 iOS 9 或更低版本中将 AVPlayer 添加到 check the state of the AVPlayer:
import AVFoundation
extension AVPlayer{
var isPlaying: Bool{
return rate != 0 && error == nil
}
}
下面是完整的代码:
var player: AVPlayer?
var playerLayer: AVPlayerLayer? //must be optional because it will get set to nil on background event
override func viewDidLoad() {
super.viewDidLoad()
// background event
NotificationCenter.default.addObserver(self, selector: #selector(setPlayerLayerToNil), name: UIApplication.didEnterBackgroundNotification, object: nil)
// foreground event
NotificationCenter.default.addObserver(self, selector: #selector(reinitializePlayerLayer), name: UIApplication.willEnterForegroundNotification, object: nil)
// add these 2 notifications to prevent freeze on long Home button press and back
NotificationCenter.default.addObserver(self, selector: #selector(setPlayerLayerToNil), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reinitializePlayerLayer), name: UIApplication.didBecomeActiveNotification, object: nil)
configurePlayer()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// this is also for the long Home button press
if let player = player{
if #available(iOS 10.0, *) {
if player.timeControlStatus == .paused{
player.play()
}
} else {
if player.isPlaying == false{
player.play()
}
}
}
}
@objc fileprivate func configurePlayer(){
let url = Bundle.main.url(forResource: "myVideo", withExtension: ".mov")
player = AVPlayer.init(url: url!)
playerLayer = AVPlayerLayer(player: player!)
playerLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
playerLayer?.frame = view.layer.frame
player?.actionAtItemEnd = AVPlayerActionAtItemEnd.none
player?.play()
view.layer.insertSublayer(playerLayer!, at: 0)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemReachedEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
}
@objc fileprivate func playerItemReachedEnd(){
// this works like a rewind button. It starts the player over from the beginning
player?.seek(to: kCMTimeZero)
}
// background event
@objc fileprivate func setPlayerLayerToNil(){
// first pause the player before setting the playerLayer to nil. The pause works similar to a stop button
player?.pause()
playerLayer = nil
}
// foreground event
@objc fileprivate func reinitializePlayerLayer(){
if let player = player{
playerLayer = AVPlayerLayer(player: player)
if #available(iOS 10.0, *) {
if player.timeControlStatus == .paused{
player.play()
}
} else {
// if app is running on iOS 9 or lower
if player.isPlaying == false{
player.play()
}
}
}
}
不要忘记将 isPlaying
扩展添加到 AVPlayer
接受的答案对我不起作用。我的 "welcome" 视频有时会随机暂停。
这是做了什么:
Background:因为当应用 "resignsActive" 或进入 "background" 时,player 和 playerLayer 对象不会被销毁(可以通过在调用各自的通知时观察它们的状态)
我推测将这些对象中的任何一个设置为 nil 然后在进入背景或前景时重新初始化它们有点不必要。
我只在播放器对象进入前景时再次播放。
var player: AVPlayer?
var playerLayer: AVPlayerLayer?
在 ViewDidLoad 中,我配置我的播放器对象。
override func viewDidLoad() {
configurePlayer()
}
configurePlayer()函数定义如下
private func configurePlayer() {
guard let URL = Bundle.main.url(forResource: "welcome", withExtension: ".mp4") else { return }
player = AVPlayer.init(url: URL)
playerLayer = AVPlayerLayer(player: player)
playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerLayer?.frame = view.layer.frame
player?.actionAtItemEnd = AVPlayerActionAtItemEnd.none
playItem()
setupPlayNotificationItems()
}
这里是辅助函数的实现
private func setupPlayNotificationItems() {
NotificationCenter.default.addObserver(self,
selector: #selector(restartPlayerItem),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: player?.currentItem)
NotificationCenter.default.addObserver(self,
selector: #selector(playItem),
name: .UIApplicationWillEnterForeground,
object: nil)
}
@objc private func playItem() {
// If you please, you can also restart the video here
restartPlayerItem()
player?.play()
if let playerlayer = playerLayer {
view.layer.insertSublayer(playerlayer, at: 0)
}
}
@objc func restartPlayerItem() {
player?.seek(to: kCMTimeZero)
}
我在 Swift 4.3 中找到了一个适合我的简单解决方案。我刚刚在覆盖的 ViewDidLoad 中为应用程序何时进入后台以及何时进入前台创建了一个观察者。
NotificationCenter.default.addObserver(self, selector:#selector(VideoViewController.shutItDown), name: UIApplication.didEnterBackgroundNotification, object: UIApplication.shared)
NotificationCenter.default.addObserver(self, selector:#selector(VideoViewController.refresh), name: UIApplication.willEnterForegroundNotification, object: nil)
然后我在 class 中有以下观察者调用的方法:
@objc func refresh() {
setupVideo()
}
@objc func shutItDown() {
self.newLayer.removeFromSuperlayer()
}
其中 newLayer 是我的 AVLayer,它作为子层添加到我的 VideoView。为了更详细一点,我已经为我的视频设置添加了代码,以确保一切都可以理解,即使你的看起来可能非常不同。
private func setupVideo() {
self.path = URL(fileURLWithPath: Bundle.main.path(forResource: "coined", ofType: "mov")!)
self.player = AVPlayer(url: self.path)
self.newLayer = AVPlayerLayer(player: self.player)
self.newLayer.frame = self.videoView.frame
self.videoView.layer.addSublayer(newLayer)
self.newLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.player.play()
self.videoView.bringSubviewToFront(continueButton)
self.videoView.bringSubviewToFront(settingsButton)
}
这个方法的"drawback"就是每次从后台转到前台,视频都会重新开始播放。这在我的案例中是可以接受的,但在你的案例中可能不是这样。这是因为当你进入后台时 AVLayer 被删除,每次你进入前台时我都会在 videoView 上放置一个新的 AVLayer。删除旧的 AVLayer 是防止渲染快照错误的基础,换句话说,克服 "freeze".
Tsonono 的回答效果很好,我只是用它来修复卡住的视频。
一方面不要摆脱他所说的缺点(每次进入前台时视频都会重新启动),使用这两种方法时只需调用播放器本身(在关闭方法中暂停播放器并在刷新方法中播放播放器) :
@objc func refresh() {
self.player?.play()
@objc func shutItDown() {
self.player?.pause()
}
我的应用程序登录页面上有一个循环播放的视频。我按照这个 Youtube 教程进行操作 loop video in view controller
问题是当应用程序进入后台时,如果我不立即返回,当我返回时视频会被冻结。
根据 Apple Docs 应该发生的情况。
我尝试使用 NotificationCenter 的 Notification.Name.UIApplicationWillResignActive
但没有用。
如何让视频在应用 returns 从后台继续播放后继续播放?
var player: AVPlayer!
var playerLayer: AVPlayerLayer!
override func viewDidLoad() {
super.viewDidLoad()
configurePlayer()
}
@objc fileprivate func configurePlayer(){
let url = Bundle.main.url(forResource: "myVideo", withExtension: ".mov")
player = AVPlayer.init(url: url!)
playerLayer = AVPlayerLayer(player: player!)
playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
playerLayer.frame = view.layer.frame
player.actionAtItemEnd = AVPlayerActionAtItemEnd.none
player.play()
view.layer.insertSublayer(playerLayer, at: 0)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemReachedEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemReachedEnd), name: Notification.Name.UIApplicationWillResignActive, object: player.currentItem)
}
@objc fileprivate func playerItemReachedEnd(){
player.seek(to: kCMTimeZero)
}
添加观察者
func addPlayerNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidPlayToEnd), name: .AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground), name: .UIApplicationWillEnterForeground, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground), name: .UIApplicationDidEnterBackground, object: nil)
}
移除观察者
func removePlayerNotifations() {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIApplicationWillEnterForeground, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIApplicationDidEnterBackground, object: nil)
}
方法
// Player end.
@objc func playerItemDidPlayToEnd(_ notification: Notification) {
// Your Code.
player.seek(to: kCMTimeZero)
}
//App enter in forground.
@objc func applicationWillEnterForeground(_ notification: Notification) {
player.play()
}
//App enter in forground.
@objc func applicationDidEnterBackground(_ notification: Notification) {
player.pause()
}
试试这个代码
根据 Apple Docs 当视频正在播放并且应用程序被发送到后台时,播放器会自动暂停:
他们说要做的是在应用程序进入后台时删除 AVPlayerLayer
(设置为 nil),然后在进入前台时重新初始化它:
他们说处理这个问题的最好方法是 applicationDidEnterBackground
和 applicationDidBecomeActive
:
我使用 NSNotification 来侦听背景和前景事件并设置函数以暂停播放器并将 playerLayer 设置为 nil(均用于背景事件),然后重新初始化 playerLayer 并为前景事件播放播放器。这些是我使用的通知 .UIApplicationWillEnterForeground
和 .UIApplicationDidEnterBackground
我发现,出于某种原因,如果您长按主屏幕按钮并弹出显示 "What can I help you with" 的屏幕,如果您再次按主屏幕按钮返回对于您的应用程序,视频将被冻结,并且使用上面的 2 个通知不会阻止它。我发现防止这种情况的唯一方法是 也 使用通知 .UIApplicationWillResignActive
和 .UIApplicationDidBecomeActive
。如果除了上述通知之外您不添加这些,那么您的视频将在主页按钮长按并返回时冻结。我发现防止所有冻结情况的最佳方法是使用所有 4 个通知。
我必须做的与上面的代码不同的 2 件事是将 player 和 playerLayer class 变量设置为可选变量而不是隐式展开的可选变量,我还添加了 AVPlayer class 的扩展到检查它是否在 iOS 9 或以下版本中播放。在 iOS 10 及更高版本中有一个内置方法 .timeControlStatus
AVPlayer timer status
我上面的代码:
var player: AVPlayer?
var playerLayer: AVPlayerLayer?
在 iOS 9 或更低版本中将 AVPlayer 添加到 check the state of the AVPlayer:
import AVFoundation
extension AVPlayer{
var isPlaying: Bool{
return rate != 0 && error == nil
}
}
下面是完整的代码:
var player: AVPlayer?
var playerLayer: AVPlayerLayer? //must be optional because it will get set to nil on background event
override func viewDidLoad() {
super.viewDidLoad()
// background event
NotificationCenter.default.addObserver(self, selector: #selector(setPlayerLayerToNil), name: UIApplication.didEnterBackgroundNotification, object: nil)
// foreground event
NotificationCenter.default.addObserver(self, selector: #selector(reinitializePlayerLayer), name: UIApplication.willEnterForegroundNotification, object: nil)
// add these 2 notifications to prevent freeze on long Home button press and back
NotificationCenter.default.addObserver(self, selector: #selector(setPlayerLayerToNil), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reinitializePlayerLayer), name: UIApplication.didBecomeActiveNotification, object: nil)
configurePlayer()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// this is also for the long Home button press
if let player = player{
if #available(iOS 10.0, *) {
if player.timeControlStatus == .paused{
player.play()
}
} else {
if player.isPlaying == false{
player.play()
}
}
}
}
@objc fileprivate func configurePlayer(){
let url = Bundle.main.url(forResource: "myVideo", withExtension: ".mov")
player = AVPlayer.init(url: url!)
playerLayer = AVPlayerLayer(player: player!)
playerLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
playerLayer?.frame = view.layer.frame
player?.actionAtItemEnd = AVPlayerActionAtItemEnd.none
player?.play()
view.layer.insertSublayer(playerLayer!, at: 0)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemReachedEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
}
@objc fileprivate func playerItemReachedEnd(){
// this works like a rewind button. It starts the player over from the beginning
player?.seek(to: kCMTimeZero)
}
// background event
@objc fileprivate func setPlayerLayerToNil(){
// first pause the player before setting the playerLayer to nil. The pause works similar to a stop button
player?.pause()
playerLayer = nil
}
// foreground event
@objc fileprivate func reinitializePlayerLayer(){
if let player = player{
playerLayer = AVPlayerLayer(player: player)
if #available(iOS 10.0, *) {
if player.timeControlStatus == .paused{
player.play()
}
} else {
// if app is running on iOS 9 or lower
if player.isPlaying == false{
player.play()
}
}
}
}
不要忘记将 isPlaying
扩展添加到 AVPlayer
接受的答案对我不起作用。我的 "welcome" 视频有时会随机暂停。
这是做了什么:
Background:因为当应用 "resignsActive" 或进入 "background" 时,player 和 playerLayer 对象不会被销毁(可以通过在调用各自的通知时观察它们的状态)
我推测将这些对象中的任何一个设置为 nil 然后在进入背景或前景时重新初始化它们有点不必要。
我只在播放器对象进入前景时再次播放。
var player: AVPlayer?
var playerLayer: AVPlayerLayer?
在 ViewDidLoad 中,我配置我的播放器对象。
override func viewDidLoad() {
configurePlayer()
}
configurePlayer()函数定义如下
private func configurePlayer() {
guard let URL = Bundle.main.url(forResource: "welcome", withExtension: ".mp4") else { return }
player = AVPlayer.init(url: URL)
playerLayer = AVPlayerLayer(player: player)
playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerLayer?.frame = view.layer.frame
player?.actionAtItemEnd = AVPlayerActionAtItemEnd.none
playItem()
setupPlayNotificationItems()
}
这里是辅助函数的实现
private func setupPlayNotificationItems() {
NotificationCenter.default.addObserver(self,
selector: #selector(restartPlayerItem),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: player?.currentItem)
NotificationCenter.default.addObserver(self,
selector: #selector(playItem),
name: .UIApplicationWillEnterForeground,
object: nil)
}
@objc private func playItem() {
// If you please, you can also restart the video here
restartPlayerItem()
player?.play()
if let playerlayer = playerLayer {
view.layer.insertSublayer(playerlayer, at: 0)
}
}
@objc func restartPlayerItem() {
player?.seek(to: kCMTimeZero)
}
我在 Swift 4.3 中找到了一个适合我的简单解决方案。我刚刚在覆盖的 ViewDidLoad 中为应用程序何时进入后台以及何时进入前台创建了一个观察者。
NotificationCenter.default.addObserver(self, selector:#selector(VideoViewController.shutItDown), name: UIApplication.didEnterBackgroundNotification, object: UIApplication.shared)
NotificationCenter.default.addObserver(self, selector:#selector(VideoViewController.refresh), name: UIApplication.willEnterForegroundNotification, object: nil)
然后我在 class 中有以下观察者调用的方法:
@objc func refresh() {
setupVideo()
}
@objc func shutItDown() {
self.newLayer.removeFromSuperlayer()
}
其中 newLayer 是我的 AVLayer,它作为子层添加到我的 VideoView。为了更详细一点,我已经为我的视频设置添加了代码,以确保一切都可以理解,即使你的看起来可能非常不同。
private func setupVideo() {
self.path = URL(fileURLWithPath: Bundle.main.path(forResource: "coined", ofType: "mov")!)
self.player = AVPlayer(url: self.path)
self.newLayer = AVPlayerLayer(player: self.player)
self.newLayer.frame = self.videoView.frame
self.videoView.layer.addSublayer(newLayer)
self.newLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.player.play()
self.videoView.bringSubviewToFront(continueButton)
self.videoView.bringSubviewToFront(settingsButton)
}
这个方法的"drawback"就是每次从后台转到前台,视频都会重新开始播放。这在我的案例中是可以接受的,但在你的案例中可能不是这样。这是因为当你进入后台时 AVLayer 被删除,每次你进入前台时我都会在 videoView 上放置一个新的 AVLayer。删除旧的 AVLayer 是防止渲染快照错误的基础,换句话说,克服 "freeze".
Tsonono 的回答效果很好,我只是用它来修复卡住的视频。
一方面不要摆脱他所说的缺点(每次进入前台时视频都会重新启动),使用这两种方法时只需调用播放器本身(在关闭方法中暂停播放器并在刷新方法中播放播放器) :
@objc func refresh() {
self.player?.play()
@objc func shutItDown() {
self.player?.pause()
}