【问题标题】:Swift iOS -AVPlayer Video Freezes / Pauses When App Comes Back from BackgroundSwift iOS - 当应用程序从后台返回时,AVPlayer 视频冻结/暂停
【发布时间】:2018-01-27 06:11:53
【问题描述】:

我在我的应用程序的登录页面上循环播放了一段视频。我按照这个 Youtube 教程让它工作loop video in view controller

问题是当应用程序进入后台时,如果我不立即返回,当我返回时,视频会冻结。

根据Apple Docs 这应该会发生。

我尝试使用通知中心的Notification.Name.UIApplicationWillResignActive,但没有成功。

应用从后台返回后如何让视频继续播放?

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)
    }

【问题讨论】:

    标签: ios swift avfoundation avplayer nsnotificationcenter


    【解决方案1】:

    根据Apple Docs,当正在播放视频并将应用发送到后台时,播放器会自动暂停:

    他们说要做的是在应用程序进入后台时删除AVPlayerLayer(设置为nil),然后在进入前台时重新初始化它:

    他们说处理这个问题的最好方法是applicationDidEnterBackgroundapplicationDidBecomeActive

    我使用 NSNotification 来监听背景和前景事件,并设置函数来暂停播放器并将 playerLayer 设置为 nil(均用于背景事件),然后重新初始化 playerLayer 并为前景事件播放播放器。这些是我使用的通知 .UIApplicationWillEnterForeground.UIApplicationDidEnterBackground

    我发现,由于某种原因,如果您长按 Home 键,如果您再次按 Home 键离开,则会弹出“我能帮您什么”的屏幕回到您的应用程序,视频将被冻结,使用上面的 2 个通知不会阻止它。我发现防止这种情况发生的唯一方法是使用通知.UIApplicationWillResignActive.UIApplicationDidBecomeActive。如果您在上述通知之外没有添加这些,那么您的视频将在主页按钮长按和返回时冻结。我发现防止所有冻结情况的最佳方法是使用所有 4 个通知。

    我必须做的与上面的代码不同的两件事是将 player 和 playerLayer 类变量设置为可选项而不是隐式解包的可选项,我还向 AVPlayer 类添加了一个扩展,以检查它是否在 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

    【讨论】:

    • 我认为我的情况有所不同。你的案子不会让他们对评论感到抱歉。
    • 好的,如果您需要帮助,请发布问题并发送链接。我会尽力帮助你的
    【解决方案2】:

    接受的答案对我不起作用。我的“欢迎”视频在某些情况下随机暂停。


    执行以下操作:
    Background:因为当应用“resignsActive”或进入“background”时 player 和 playerLayer 对象不会被销毁(这可以通过在调用各自的通知时观察它们的状态来验证) 我推测将这些对象中的任何一个设置为零,然后在进入背景或前景时重新初始化它们有点不必要。

    我只会在播放器对象进入前台时再次播放它。

    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)
    }
    

    【讨论】:

      【解决方案3】:

      添加观察者

      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()
      }
      

      试试这个代码

      【讨论】:

      • 我应该在 func playerItemDidPlayToEnd(_ notification: Notification) 中输入什么代码 { // 你的代码。 }
      • player.seek(to: kCMTimeZero) 这个代码。您发布的问题
      • 你试过了吗?它对我不起作用。出现同样的问题,视频卡住了
      • 您遇到了什么问题?
      • 我在问题中说的同样的问题。当应用程序进入后台并且我回到它时,视频被冻结。根据苹果的说法,这应该发生:developer.apple.com/library/content/qa/qa1668/_index.html#//…
      【解决方案4】:

      Tsonono 回答效果很好,我只是用它来修复冻结的视频。

      一方面不要摆脱他正在谈论的缺点(每次进入前台时都会重新启动视频),只需在使用这两种方法时调用播放器本身(在shutitdown方法中暂停播放器并在刷新方法中播放播放器):

      @objc func refresh() {
      self.player?.play()
      @objc func shutItDown() {
      self.player?.pause()
      

      }

      【讨论】:

      • 这正是解决方案。只需收听 UIApplication Notifications 并添加这些方法即可。在 iOS 14 和 15 上测试。无需将任何变量设置为 nil 或重新设置。
      【解决方案5】:

      我在 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)
      

      然后我有以下方法被类中的观察者调用:

      @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)
      }
      

      这种方法的“缺点”是每次从后台转到前台时都会重新启动视频。这在我的情况下是可以接受的,但在你的情况下可能不是。这是因为当您转到后台时,AVLayer 被删除,并且每次您转到前台时,我都会在 videoView 上放置一个新的 AVLayer。删除旧的 AVLayer 是防止渲染快照错误的基础,换句话说就是克服“冻结”。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-07-29
        • 2012-07-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多