[iOS]動画を再生する(AVPlayerLayer使用)

(Xcode 11, Swift 5.1, iOS 13での実装)

iOSでの、動画を再生する機能を、前回はAVPlayerViewControllerを用いて作成しました。今回は、AVPlayerLayerを使用します。

AVPlayer – AVFoundation | Apple Developer Documentation

AVPlayerクラスを用いることで、映像やオーディオの再生ができます。
AVPlayerには画面に表示する機能がないので、前回はその部分をAVPLayerViewControllerがになう例を記しました。
今回は、AVPlayerLayerを使う例を記しました。

AVPlayerLayer – AVFoundation | Apple Developer Documentation

AVPlayerLayerはplayerプロパティを持ち、そこにAVPlayerを渡すことで映像を表示できます。
実際に使う際には、このAVPlayerLayerのドキュメントに記されているプレイヤー用のUIViewサブクラスを用いる方法が扱いやすいです。

class PlayerView: UIView {
    var player: AVPlayer? {
        get {
            return playerLayer.player
        }
        set {
            playerLayer.player = newValue
        }
    }
    
    var playerLayer: AVPlayerLayer {
        return layer as! AVPlayerLayer
    }
    
    // Override UIView property
    override static var layerClass: AnyClass {
        return AVPlayerLayer.self
    }
}

Appleのドキュメントに乗っているコード例です。layerClassをオーバーライドしてこのUIViewがもつlayerはAVPlayerLayerクラスを返すようにしています。プロパティとしてplayerがあり、そこにAVPlayerをセットします。

この記事では、画面表示にこのPlayerViewを用いて、プロジェクトのリソースに加えた動画ファイルを再生する方法を記します。

動画を再生するサンプル

プロジェクトに追加してある動画ファイルを再生・一時停止するサンプルを作成しました。バックグラウンドへ移行してもオーディオの再生・一時停止ができます。バックグラウンド移行時に動画再生中であった場合、一時停止するものを作成しますが、一時停止せずにオーディオを再生し続ける方法についても記しました。全体のソースコードは記事の最後にまとめてあります。
ポイントになる箇所について、説明をおこないました。
(今回の記事では再生・一時停止のみですが、その後、早戻し・早送り・シークを含むサンプルを作成する記事を書きました。[iOS]動画を再生する(早戻り、早送り、シーク)

動画再生機能作成については、Appleのサンプルが参考になります。
Creating a Movie Player App with Basic Playback Controls | Apple Developer Documentation

PlayerViewクラスを作成

今回は、ViewController.swiftのほかに、PlayerView.swiftを作成しました。
File > New > File > Cocoa Touch Classから、UIViewをスーパークラスとするPlayerViewクラスを作成します。
内容は、前段に記したAppleのドキュメントに載っているPlayerViewコード例の通りです。記事の最後にも載せています。

動画ファイルをプロジェクトに加える

File > Add Files to <プロジェクト名>。
動画ファイルを選び、プロジェクトに加えます。
今回は、clip.mp4という名前の動画ファイルが加えられたとして話を進めました。

UI作成

Main.storyboard > View Controller。
UIViewとボタン二つを貼り付けます。
ボタンのテキストを「Play」「Pause」とします。
(画像ではUIViewのBackgroundはオレンジ色にしてありますが、これはどこに貼り付けたかを見やすくするためで、実際にはSystem Background Colorのままで構いません)

貼り付けたUIViewを選択し、Identity inspector > Custom Class > Classで、PlayerViewとします。

Player ViewとViewController.swiftをOutletで結び、名前をplayerViewとします。

一つのボタンのテキストを「Play」、もう一つのボタンのテキストを「Pause」とします。
PlayボタンとViewController.swiftをActionで結び、名前をplayBtnTappedとします。@IBAction func playBtnTapped(_ sender: Any) {}が生成されます。
PauseボタンとViewController.swiftをActionで結び、名前をpauseBtnTappedとします。@IBAction func pauseBtnTapped(_ sender: Any) {}が生成されます。

Audio Sessionの準備

AVAudioSession – AVFoundation | Apple Developer Documentation

Audio Sessionとはアプリがどのようにオーディオを扱うかを示すオブジェクトです。再生のみなのか、録音再生を行うのか、バックグラウンド再生ありにするのかなどを設定します。用意されたカテゴリ・モード・オプションから適切なものを選んで設定する形式になっています。
今回は、カテゴリをAVAudioSessionCategoryPlayback(オーディオ機能をメインとしたアプリ用カテゴリ。バックグラウンド再生可能)、モードをAVAudioSessionModeMoviePlayback(ムービー再生用)に設定します。

/// Audio sessionを動画再生向けのものに設定し、activeにします
let audioSession = AVAudioSession.sharedInstance()
do {
    try audioSession.setCategory(.playback, mode: .moviePlayback)
        
} catch {
    print("Setting category to AVAudioSessionCategoryPlayback failed.")
}

do {
    try audioSession.setActive(true)
    print("Audio session set active !!")
} catch {
    
}

バックグラウンド再生の準備

今回のサンプルでは、バックグラウンドでのオーディオ再生を行うので、その設定を行います。
TARGET > Signing & Capabilitiesを選択。
「+ Capability」ボタンから-> Background Modes

Audio, AirPlay and Picture in Pictureをチェック。

これにより、info.plistにRequired background modesが加わり、そのitemがApp plays audio or streams audio/video using AirPlayとなります。バックグラウンド再生が可能になります。

動画プレイヤー

urlからAVPlayerItemを作成します。
AVPlayerに作成したAVPlayerItemをセットします。
AVPlayerを動画表示を担当するAVPlayerLayerと結びつけます。
PlayerViewのプロパティであるplayerにAVPlayerをセットします。これによって、PlayerViewの動画表示をになうlayerと結びつけることができます。
今回は、ViewControllerクラスに追加したitemURLプロパティに動画のurlも渡しています。これは、あとで、NowPlayingInfo(現在再生中のアプリの状況)を表示するさいに使用します。

let fileName = "clip"
let fileExtension = "mp4"
guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
    print("Url is nil")
    return
}

itemURL = url
let item = AVPlayerItem(url: url)
player = AVPlayer(playerItem: item)

playerView.player = player

リモートコントロール対応

イヤホンからの操作や、バックグラウンド移行後のコントロールセンターやロックスクリーンの再生コントロールからの操作に対応します。
MPRemoteCommandCenterクラスを用います。
また、コントロールセンターやロックスクリーンの再生コントロールに再生中アプリの現在の状況を表示する必要があります。それには、MPNowPlayingInfoCenterクラスを用います。
これらの実装については、Controlling Background Audio | Apple Developer Documentationが参考になります。

リモートコントロール対応部分

// MARK: Remote Command Event
func addRemoteCommandEvent() {
    
    let commandCenter = MPRemoteCommandCenter.shared()
    commandCenter.togglePlayPauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
        self.remoteTogglePlayPause(commandEvent)
        return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.playCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
        self.remotePlay(commandEvent)
        return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.pauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
        self.remotePause(commandEvent)
        return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.nextTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
        self.remoteNextTrack(commandEvent)
        return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.previousTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
        self.remotePrevTrack(commandEvent)
        return MPRemoteCommandHandlerStatus.success
    }
}

func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
    // イヤホンのセンターボタンを押した時の処理
    // (略)
}

func remotePlay(_ event: MPRemoteCommandEvent) {
    // 再生ボタンが押された時の処理
    // (略)
}

func remotePause(_ event: MPRemoteCommandEvent) {
    // ポーズボタンが押された時の処理
    // (略)
}

func remoteNextTrack(_ event: MPRemoteCommandEvent) {
    // 「次へ」ボタンが押された時の処理
    // (略)
}

func remotePrevTrack(_ event: MPRemoteCommandEvent) {
    // 「前へ」ボタンが押された時の処理
    // (略)
}

NowPlayingInfo部分

この例では、動画ファイルのurlの末尾部分をタイトル表示に使用しています。
MPNowPlayingInfoCenterに示すことができるメタデータはほかにも様々なものがあります。
Now Playing Metadata Properties | MPNowPlayingInfoCenter – Media Player | Apple Developer Documentation

func setupNowPlaying(url: URL) {
    // Define Now Playing Info
    var nowPlayingInfo = [String : Any]()
    
    // ここでは、urlのlastPathComponentをtitleとして表示しています。
    nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = url
    nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
    nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
    nowPlayingInfo[MPMediaItemPropertyTitle] = url.lastPathComponent
    nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = ""
    
    // Set the metadata
    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}

バックグラウンド移行時の動作について

AVPlayerでは、動画再生中にバックグラウンドへ移行すると、一時停止するのがデフォルトの動作です。
もし、バックグラウンド移行時に再生しているオーディオを鳴らし続けたい場合は、追加の処理が必要になります。
これについては、Playing Audio from a Video Asset in the Background | Apple Developer Documentationのドキュメントに記されています。
バックグラウンド移行時にAVPlayerのインスタンスをplayerLayerから外す必要があります。

// フォアグラウンド移行時に呼び出されます
@objc func willEnterForeground(_ notification: Notification) {
    playerView.player = player
}
     
// バックグラウンド移行時に呼び出されます
@objc func didEnterBackground(_ notification: Notification) {
    playerView.player = nil
}

Playボタンを押すと動画が再生され、Pauseボタンを押すと動画が一時停止するサンプルを作成しました。イヤホン、コントロールセンターなどからの再生・一時停止もできます。

使用したソースコード

PlayerView.swift

import UIKit
import AVFoundation

class PlayerView: UIView {
    
    // The player assigned to this view, if any.
    
    var player: AVPlayer? {
        get { return playerLayer.player }
        set { playerLayer.player = newValue }
    }
    
    // The layer used by the player.
    
    var playerLayer: AVPlayerLayer {
        return layer as! AVPlayerLayer
    }
    
    // Set the class of the layer for this view.
    override static var layerClass: AnyClass {
        return AVPlayerLayer.self
    }
}

ViewController.swift

以下のソースコードでは、バックグラウンド移行時に一時停止する動作になっています。動画再生中のバックグラウンド移行のさいに、続けてオーディオ再生を行う場合は、willEnterForeground(_;)、didEnterBackground(_:)でのコメントアウトを外してください。

import UIKit
import AVFoundation
import MediaPlayer
import AVKit

class ViewController: UIViewController {

    @IBOutlet weak var playerView: PlayerView!
    
    var player = AVPlayer()
    var itemURL: URL?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(.playback, mode: .moviePlayback)
        } catch {
            print("Setting category to AVAudioSessionCategoryPlayback failed.")
        }
        do {
            try audioSession.setActive(true)
            print("audio session set active !!")
        } catch {
            
        }
        
        setupPlayer()
        
        addLifeCycleObserver()
        
        addRemoteCommandEvent()
    }
    
    private func setupPlayer() {
        let fileName = "clip"
        let fileExtension = "mp4"
        guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
            print("Url is nil")
            return
        }
        
        itemURL = url
        let item = AVPlayerItem(url: url)
        player = AVPlayer(playerItem: item)
        
        playerView.player = player
        
    }

    /// Playボタンが押された
    @IBAction func playBtnTapped(_ sender: Any) {
        player.play()
        if let url = itemURL {
            setupNowPlaying(url: url)
        }
    }
        
    @IBAction func pauseBtnTapped(_ sender: Any) {
        player.pause()
    }
    
    // MARK: Life Cycle
    func addLifeCycleObserver() {
        let center = NotificationCenter.default
        center.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
        center.addObserver(self, selector: #selector(didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }
    
    // フォアグラウンド移行時に呼び出されます
    @objc func willEnterForeground(_ notification: Notification) {
        /// (動画再生中であったときにそのままオーディオ再生を続ける場合は、このあとのコメントアウトをとりのぞいてください)
        /// (バックグラウンド移行時に、playerLayerからplayerがはずされているので、再度つなげます。)
        //playerView.player = player
    }
         
    // バックグラウンド移行時に呼び出されます
    @objc func didEnterBackground(_ notification: Notification) {
        /// (動画再生中であったときにそのままオーディオ再生を続ける場合は、このあとのコメントアウトをとりのぞいてください)
        //playerView.player = nil
    }
    
    // MARK: Now Playing Info
    func setupNowPlaying(url: URL) {
        // Define Now Playing Info
        var nowPlayingInfo = [String : Any]()
        
        // ここでは、urlのlastPathComponentを表示しています。
        nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = url
        nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
        nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
        nowPlayingInfo[MPMediaItemPropertyTitle] = url.lastPathComponent
        nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = ""
        
        // Set the metadata
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }
    
    // MARK: Remote Command Event
    func addRemoteCommandEvent() {
        
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.togglePlayPauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remoteTogglePlayPause(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        }
        commandCenter.playCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remotePlay(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        }
        commandCenter.pauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remotePause(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        }
        commandCenter.nextTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remoteNextTrack(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        }
        commandCenter.previousTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
            self.remotePrevTrack(commandEvent)
            return MPRemoteCommandHandlerStatus.success
        }
    }
    
    func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
        // イヤホンのセンターボタンを押した時の処理
        // (略)
    }
    
    func remotePlay(_ event: MPRemoteCommandEvent) {
        player.play()
    }
    
    func remotePause(_ event: MPRemoteCommandEvent) {
        player.pause()
    }

    func remoteNextTrack(_ event: MPRemoteCommandEvent) {
        // 「次へ」ボタンが押された時の処理
        // (略)
    }
    
    func remotePrevTrack(_ event: MPRemoteCommandEvent) {
        // 「前へ」ボタンが押された時の処理
        // (略)
        
    }

}

「[iOS]動画を再生する(AVPlayerLayer使用)」への21件のフィードバック

  1. nackpan様

    kkです。

    動画のバックグラウンド再生に関する貴重な記事、大変感謝しております。
    ありがとうございます。
    また、お世話になりたい事象があり、コメントさせて頂いております。

    投稿頂いた記事の内容を元に、アプリ開発をしている中で、
    AVPlayerViewControllerとAVPlayerLayerを、一つのプロジェクト内で
    使い分けるアプリを検討しております。

    仕様は、「Play 02]ボタンを追加し、
    ・「Play01」ボタン(元の「Play」ボタン)を押せば、AVPlayerLayerで「sample01.mp4」再生
    ・「Play02」ボタンを押せば、AVPlayerViewControllerで「sample02.mp4」再生というものです。

    問題は、下記の④です。
    ①「Play01」押しで「sample01」再生=> バックグラウンドへ移行しAVPlayerLayerのリモートコマンド操作で「sample01」を操作:OK
    ②「Play02」押しで「sample02」再生=> バックグラウンドへ移行しAVPlayerViewControllerのリモートコマンド操作で「sample02」を操作:OK
    ③「Play01」押しで「sample01」再生後、「Play02」押しで「sample02」再生=> バックグラウンドへ移行しAVPlayerViewControllerのリモートコマンド操作で「sample02」を操作:OK
    ④「Play01」押しで「sample01」再生後、「Play02」 押しで「sample02」再生、その後「Play01」押しで「sample01」を再生 => バックグラウンドへ移行しAVPlayerLayerのリモートコマンド操作での「sample01」の操作ができず、AVPlayerViewControllerの、「sample02」のリモートコマンド操作になってしまう。

    AVPlayerViewControllerを一度使うと、その後、AVPlayerLayerで再生後、バックグラウンド再生へ移行しても、リモートコマンド画面は、AVPlayerViewControllerのままとなってしまい、「sample01」と「sample02」の両方の音声が、再生されてしまいます。

    下記を検討しましたが、効果はありませんでした。
    (1)AVPlayerViewControllerを、閉じた(左上の×ボタン押し)後、「Play01」ボタン押し時に
     ・deleteRemoteCommandEvent()
     を実行後、再度、addRemoteCommandEvent()を実行
    (2)AVplayerViewController終了(左上の×ボタン押し)時に、リモートイベント解除(コード③に記載)
     ・NotificationCenterで、override func viewDidAppearにて、AVPlayerItemDidPlayToEndTime時に、
      deleteRemoteCommandEvent()と、self.dismiss(animated: true)を実施
      (参考:https://ameblo.jp/zexpertz/entry-12528476400.html)

    AVPlayerViewControllerで動画を、再生した後に、AVPlayerViewControllerのリモートイベントを解除(?という表現が正しいかわかっておりませんが)し、次回にAVPlayerLayerでの、リモートイベントを実行できる方法があれば、ご教示頂きたく、お時間のあるときに、どうか、よろしくお願い致します。

    検討したコードは、nackpan様が投稿しておられる、[iOS]動画を再生する(AVPlayerLayer使用)のコードに、
    //■以下、追加コード
    を、追加したものです。追加コードは、
    ご参考にして頂ければ幸いです。

    import UIKit
    import AVFoundation
    import MediaPlayer
    import AVKit

    class ViewController: UIViewController {

    @IBOutlet weak var playerView: PlayerView!

    var player = AVPlayer()
    var itemURL: URL?

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    playerView.player = nil

    let audioSession = AVAudioSession.sharedInstance()
    do {
    try audioSession.setCategory(.playback, mode: .moviePlayback)
    } catch {
    print(“Setting category to AVAudioSessionCategoryPlayback failed.”)
    }
    do {
    try audioSession.setActive(true)
    print(“audio session set active !!”)
    } catch {

    }

    setupPlayer()

    addLifeCycleObserver()

    addRemoteCommandEvent()
    }

    private func setupPlayer() {
    let fileName = “sample01”
    let fileExtension = “mp4”
    guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
    print(“Url is nil”)
    return
    }

    itemURL = url
    let item = AVPlayerItem(url: url)
    player = AVPlayer(playerItem: item)

    playerView.player = player

    }

    /// Playボタンが押された
    @IBAction func playBtnTapped(_ sender: Any) {

    //test
    deleteRemoteCommandEvent()
    addRemoteCommandEvent()
    //
    player.play()
    if let url = itemURL {
    setupNowPlaying(url: url)
    }
    }

    @IBAction func pauseBtnTapped(_ sender: Any) {
    player.pause()
    }

    // MARK: Life Cycle
    func addLifeCycleObserver() {
    let center = NotificationCenter.default
    center.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
    center.addObserver(self, selector: #selector(didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }

    // フォアグラウンド移行時に呼び出されます
    @objc func willEnterForeground(_ notification: Notification) {
    /// (動画再生中であったときにそのままオーディオ再生を続ける場合は、このあとのコメントアウトをとりのぞいてください)
    /// (バックグラウンド移行時に、playerLayerからplayerがはずされているので、再度つなげます。)
    //playerView.player = player
    }

    // バックグラウンド移行時に呼び出されます
    @objc func didEnterBackground(_ notification: Notification) {
    /// (動画再生中であったときにそのままオーディオ再生を続ける場合は、このあとのコメントアウトをとりのぞいてください)
    //playerView.player = nil
    }

    // MARK: Now Playing Info
    func setupNowPlaying(url: URL) {
    // Define Now Playing Info
    var nowPlayingInfo = [String : Any]()

    // ここでは、urlのlastPathComponentを表示しています。
    nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = url
    nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
    nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
    nowPlayingInfo[MPMediaItemPropertyTitle] = url.lastPathComponent
    nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = “”

    // Set the metadata
    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }

    // MARK: Remote Command Event
    func addRemoteCommandEvent() {

    let commandCenter = MPRemoteCommandCenter.shared()
    commandCenter.togglePlayPauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remoteTogglePlayPause(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.playCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePlay(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.pauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePause(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.nextTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remoteNextTrack(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.previousTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePrevTrack(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    }

    @objc func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
    // イヤホンのセンターボタンを押した時の処理
    // (略)
    }

    func remotePlay(_ event: MPRemoteCommandEvent) {
    player.play()
    }

    func remotePause(_ event: MPRemoteCommandEvent) {
    player.pause()
    }

    func remoteNextTrack(_ event: MPRemoteCommandEvent) {
    // 「次へ」ボタンが押された時の処理
    // (略)
    }

    func remotePrevTrack(_ event: MPRemoteCommandEvent) {
    // 「前へ」ボタンが押された時の処理
    // (略)

    }

    //■以下、追加コード
    //①リモコン対応解放の関数を定義:removeTarget
    func deleteRemoteCommandEvent() {//deleRemo
    let commandCenter = MPRemoteCommandCenter.shared()
    commandCenter.togglePlayPauseCommand.removeTarget(self, action: #selector(type(of: self).remoteTogglePlayPause(_:)))
    }//deleRemo

    //========ここからは、nackpan様の記事(:AVPlayerLayer使用)より転記=============
    //②AVPlayerViewControllerで再生
    var playerController = AVPlayerViewController()
    var player02 = AVPlayer()

    /// 動画プレイヤーにアイテムをセットして更新
    private func playMovie(fileName: String, fileExtension: String) {
    guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
    print(“Url is nil”)
    return
    }
    // AVPlayerにアイテムをセット
    let item = AVPlayerItem(url: url)
    player02.replaceCurrentItem(with: item)

    // 動画プレイヤーにplayerをセット
    playerController.player = player02

    // 動画プレイヤーを表示して再生
    self.present(playerController, animated: true) {
    self.player02.play()
    }
    }

    /// Play Videoボタンが押されました
    @IBAction func Play02(_ sender: Any) {
    let fileName = “sample02”
    let fileExtension = “mp4”
    playMovie(fileName: fileName, fileExtension: fileExtension)
    }
    //========記事の転記は、ここまで=============

    //③AVplayerViewController終了時、リモートイベント解除
    override func viewDidAppear(_ animated: Bool) {//func vDA
    super.viewDidAppear(animated)
    NotificationCenter.default.addObserver(self,selector: #selector(self.dismissFromAVplayerVC ),name:NSNotification.Name.AVPlayerItemDidPlayToEndTime,object: nil)
    }//vDA
    @objc func dismissFromAVplayerVC(note:NSNotification) {
    deleteRemoteCommandEvent()
    self.dismiss(animated: true)
    }

    }

    以上です。
    失礼致します。

  2. kkさん、こんにちは。

    コメントの事例で、バックグラウンドで音楽が重なって聞こえる状況を確認しました。
    いくつか修正方法を実験してみました。

    コメントのプログラミングリストでは、 AVPlayerViewController用のプレイヤーとしてplayer02を用意されていますが、それをとりやめて、すでに使っているplayerをAVPlayerViewController用のプレイヤーとすると、音楽が二重になるのを避けることができました。
    いちどきに2つの動画を再生するのでないのなら、これが良いのではないかと思います。
    playMovie(fileName:, fileExtension:)を修正

    
    private func playMovie(fileName: String, fileExtension: String) {
    	guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
    		print(“Url is nil”)
    		return
    	}
    	// AVPlayerにアイテムをセット
    	let item = AVPlayerItem(url: url)
    	player.replaceCurrentItem(with: item)
    	
    	// 動画プレイヤーにplayerをセット
    	playerController.player = player
    	
    	// 動画プレイヤーを表示して再生
    	self.present(playerController, animated: true) {
    		self.player.play()
    	}
    }
    
    

    play02ボタンを押すと、playerのアイテムが置き換えられ、動画プレイヤーにセットされ再生されます。
    このままですと、playerのアイテムはsample02のままです。
    play01ボタンを押した時には、sample01になるように修正します。

    
    @IBAction func playBtnTapped(_ sender: Any) {
    	let fileName = “sample01”
    	let fileExtension = “mp4”
    	guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
    		print(“Url is nil”)
    		return
    	}
    	
    	let item = AVPlayerItem(url: url)
    	player.replaceCurrentItem(with: item)
    	player.play()
    	
    	setupNowPlaying(url: url)
    }
    
    

    play01ボタンを押すと、playerのアイテムをsample01に変更して、再生します。
    従来は、setupPlayer()内でplayerにアイテムセットを行なっていましたが、それは必要なくなったので、setupPlayer()を修正します。

    
    private func setupPlayer() {
    	playerView.player = player
    }
    
    

    これで、バックグラウンドでの音楽が二重になる問題に関しては解消できると思います。

  3. nackpan様

    kkです。
    早速のコメントありがとうございます。
    大変有難く、早速、実験させて頂きました結果を、報告させて頂きます。
    (1)playerを一つにすることによって、sample01.mp4と、sample02.mp4の両方が、同時再生される事は、無くなりました。ありがとうございます。
    (2)上記、対応を導入後、バックグラウンドに移行したところ、下記のようになりました。
      ①AVPlayerLayer再生中=>バックグラウンド移行=>AVPlayerLayerのリモートコマンドセンターで、操作・再生継続可能
      ②AVPlayerViewController再生中=>バックグラウンド移行=>AVPlayerVieControllerのリモートコマンドセンターで、操作・再生継続可能
      ③AVPlayerLayer再生=>AVPlayerViewController再生=>AVPlayerLayer再生中=>バックグラウンド移行=>AVPlayerViewControllerのリモートコマンドセンターで、操作・再生継続可能(AVPlayerLayerのリモートコマンドセンターではない)

    アプリの仕様としては、
     ・AVPlayerLayerでは、短時間(3〜5秒)のmp4ファイルを、順次連続再生
     ・AVPlayerViewControllerでは、長時間(1〜2時間)のmp4ファイルを再生
     ・AVPlayerLayerのファイルとファイルの再生の間には、DispatchQを使用して、待ち時間(1〜10秒くらい)を置くので
      その間、誤動作を避けるために、ボタン表示を、無効化(薄いグレー)
      無効化のコードは、以下を使用、(待ち時間を過ぎれば有効化:それぞれをtrueにしています)
      func setupRemoteCommandCenter() {//func setupRemo
    let commandCenter = MPRemoteCommandCenter.shared();
    commandCenter.playCommand.isEnabled = false
    commandCenter.playCommand.addTarget {event in
    return .success
    }
    commandCenter.pauseCommand.isEnabled = false
    commandCenter.pauseCommand.addTarget {event in
    return .success
    }
    commandCenter.togglePlayPauseCommand.isEnabled = false
    commandCenter.togglePlayPauseCommand.addTarget {event in
    return .success
    }
    commandCenter.nextTrackCommand.isEnabled = false
    commandCenter.togglePlayPauseCommand.addTarget {event in
    return .success
    }
    commandCenter.previousTrackCommand.isEnabled = false
    commandCenter.togglePlayPauseCommand.addTarget {event in
    return .success
    }

    引き続きの質問となってしまい、申し訳ありませんが、
    ・AVPlayerLayerでの再生中のバックグラウンド移行後は、必ず、AVPlayerLayerのリモートコマンドセンター(⏪・▶️・⏩)で操作
    ・AVPlayerViewControllerでの再生中のバックグラウンド移行時は、必ず、AVPlayerViewControllerのリモートコマンドセンター(-15sec・▶️・+15sec)で操作
    をできるようにする方法は、有りますでしょうか。
    これについては、全く的外れかもしれませんが、AVPlayerLayer再生ボタンを押した時に、
    ・deleteRemoteCommandEvent() => addRemoteCommandEvent()
    を実行してみましたが、効果がありませんでした。

    コメントを頂ければ有難く、どうか、よろしくお願い致します。
    失礼致します。

  4. kkさん、こんにちは。

    いくつか実験してみました。
    バックグラウンドでの再生コントロールを、AVPlayerLayerのものとAVPlayerViewControllerのもので切り替えるのはうまくいきませんでした。
    AVPlayerViewControllerには、updatesNowPlayingInfoCenterというプロパティがあります。
    AVPlayerViewControllerがNowPlayingInfoCenter(バックグラウンドでの再生コントロールの情報)を更新するかどうかを示すフラグです。
    このupdatesNowPlayingInfoCenterのtrue/falseを切り替えることで、AVPlayerLayerでの再生コントロールとAVPlayerViewControllerでの再生コントロールを切り替えられるか試しましたが、だめでした。true/falseを切り替えていると、AVPlayerLayerでのバックグラウンド移行後に再生コントロールが表示されない事態になってしまいました。

    そこで、updatesNowPlayingInfoCenterのtrue/falseを切り替えるのではなく、最初にfalseとしてしまう方法をためしてみました。
    AVPlayerViewControllerで自動的に用意されるバックグラウンド用再生コントロールを取りやめて、自前でコントロールを管理します。
    そうなると、
    * 再生が進むと、現在の時刻が更新され、シークバー(プログレスバー)が進む
    * シークバーのボタンを動かすと再生位置が変わる
    * 十数秒の早戻し、早送り
    を実装する必要があります。
    それに関して、記事を書きました。
    [iOS]動画を再生する(早戻り、早送り、シーク) – nackpan Blog
    [iOS]動画を再生する(バックグラウンドでの早戻り、早送り、シーク) – nackpan Blog
    ごらんください。

    play01ボタンを押すとsample01を再生、play02ボタンを押すとsample02を再生する事例に戻ります。

    実装する事柄。
    * play01ボタンを押すとsample01を再生、play02ボタンを押すとsample02を再生
    * sample01再生時には、バックグラウンド用再生コントロールに「再生」「次トラック」「前トラック」ボタンを表示。バー操作は使用不能。
    * sample02再生時には、バックグラウンド用再生コントロールに「再生」「早戻し」「早送り」ボタンを表示。バー操作で時間移動可能。

    [iOS]動画を再生する(バックグラウンドでの早戻り、早送り、シーク) – nackpan Blogの記事にあるプログラミングリストに加筆するかたちで紹介します。

    1. Play02ボタンを配置して、ViewControllerとActionで結びつけてください。
    名前はplay02BtnTappedとします。

    2. ViewControllerのプロパティに以下を加えてください

    
    var playerController = AVPlayerViewController()
    /// playerViewControllerで再生があったことを示すフラグ
    var playerViewControllerPlayedFlag = false
    
    

    3. viewDidLoadあたりで、AVPlayerViewControllerはNowPlayingInfoCenterの更新はしないと示します。

    
    playerController.updatesNowPlayingInfoCenter = false
    
    

    4. playBtnTapped()を修正

    
    @IBAction func playBtnTapped(_ sender: Any) {
            let commandCenter = MPRemoteCommandCenter.shared()
            commandCenter.skipBackwardCommand.isEnabled = false
            commandCenter.skipForwardCommand.isEnabled = false
            commandCenter.changePlaybackPositionCommand.isEnabled = false
            
            if playerViewControllerPlayedFlag {
                // playerViewControllerで再生があったあとは、ファイルを置き換えます
                replacePlayerItem(fileName: "clip", fileExtension: "mp4")
                
                // playerViewControllerで再生したflagをfalseに戻す
                playerViewControllerPlayedFlag = false
            }
    
            player.play()
        }
    
    

    5. Play02ボタン関連の記述

    
    @IBAction func play02BtnTapped(_ sender: Any) {
        playerViewControllerPlayedFlag = true
        
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.skipBackwardCommand.isEnabled = true
        commandCenter.skipForwardCommand.isEnabled = true
        commandCenter.changePlaybackPositionCommand.isEnabled = true
        
        let fileName = "sample02"
        let fileExtension = "mp4"
        playLongVideo(fileName: fileName, fileExtension: fileExtension)
        
    }
    
    private func playLongVideo(fileName: String, fileExtension: String) {
        // Playerのファイル置き換え
        replacePlayerItem(fileName: fileName, fileExtension: fileExtension)
        
        // 動画プレイヤーにplayerをセット
        playerController.player = player
        
        // 動画プレイヤーを表示して再生
        self.present(playerController, animated: true) {
            self.player.play()
        }
    }
    
    

    これで、バックグラウンド用の再生コントロールを意図する表示にできると思います。

  5. nackpan様
    kkです。
    多岐に渡る内容の記事掲載、ありがとうございます。
    豊富な内容のため、一つづつ実装して勉強させて頂きます。

    今後とも、よろしくお願い致します。

  6. kkさん、こんにちは。

    さきほど(2020/05/18朝)、kkさんのコメントを誤判定して削除してしまいました。
    毎朝、ブランドコピー品のセールを謳うspamコメントを削除しているのですが、そのさいにkkさんのコメントが入っているのに気づかず、削除を選んでいました。
    削除中にkkさんのコメントと気付いたのですが、削除取り消しが間に合いませんでした。
    申し訳ありません。

    もし面倒でなければ、コメントの再送をおねがいします。
    あらためて送るほどでなければ放置してください。
    もうしわけありません。

  7. nackpan様

    いつもありがとうございます。
    以下のコメントを、再送させて頂きます。
    お手数をおかけしますが、よろしくお願いいたします。
    ======= 以下が、送付させて頂いたコメントです。======
    kkと申します。以前は、バックグラウンドでの動画再生について
    丁寧なご説明を頂き、大変感謝しております。
    ありがとうございました。

    今回、その続きで、アドバイスを頂きたく、投稿させて頂きます。

    下記の仕様で、動画再生を、継続的に実行していると、突然、停止
    してしまうことがあります。

    【現象】
    ・複数動画の連続再生
     (語学学習用で、台詞毎の、短い動画の連続再生を意図しております。)
    ・動画一つの長さは約5秒〜10秒
    ・一つの動画を、複数回(2〜3回)繰り返した後、次の動画再生へ移行
    ・動画のサイズ:640×340、500kB〜1MB、コーデック:H.264
    ・再生例:動画①1回目再生 => 動画①2回目再生 => 動画②1回目再生 => 動画②2回目再生 =>・・・つづく
    ・全ての動画を、1回ずつ再生する場合は、問題ないが、各動画を複数回ずつ再生した時に発生
    ・再生速度を、上げると発生しやすくなる。
    ・再生したい動画のファイル数が増えると、発生しやすくなる。

    【発生条件の例】
    上記現象は、起こったり、起こらなかったりするが、下記条件にすると、高い確率で発生する。
    ・再生速度アップ:1.8 倍(通常速度でも発生するが、頻度が少ない)
    ・6個目までの動画では、起こらず、7個目以降で、同じ現象が起こりやすくなる。
    ・7個目の動画の再生が始まった直後に、停止する。

    【確認した内容】
    以下は、現象発生後、確認した内容です。
    ・再生ボタンを押すと、再び再生を始めることができるため、ハングではないように見える
    ・よく起こる箇所(繰り返し再生のところ)で、player.play()の後で、
     再生速度、エラーがあるかどうかを”isPlaying”で、チェックしたが、速度は、”=1.8”、エラーは”=nil”となる
    ・player.play()までは、動作しているが、再生が開始直後に停止しているので、再生終了通知が来ないまま停止し
     しており、再生状態の検出が「できない
    ・シミュレーターでは、発生しない

    【確認した実機】
    確認デバイスは、iPhone 6plus、iPad Pro(2nd Gene)で、iPhone 6plusは頻繁に、iPad proは
    稀(今まで約100時間ほど再生して、1回発生)に起こります。

    【お願い】
    下記の内容で、アドバイスを頂けないでしょうか。
    1)起こさなくすることができる回避策、案がありますでしょうか。
    2)もし起こった場合の検出と、再生継続させる方法は、ありますでしょうか。

    【ソースコード】
    使用しているソースコードは、nackpan様の、投稿記事のコードに、追加(1)、追加(2)を
    挿入したものです。

    ====== 以下、実際のコード ======

    import UIKit
    import AVFoundation
    import MediaPlayer
    import AVKit

    class ViewController: UIViewController {

    @IBOutlet weak var playerView: PlayerView!

    var player = AVPlayer()
    var itemURL: URL?

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    let audioSession = AVAudioSession.sharedInstance()
    do {
    try audioSession.setCategory(.playback, mode: .moviePlayback)
    } catch {
    print(“Setting category to AVAudioSessionCategoryPlayback failed.”)
    }
    do {
    try audioSession.setActive(true)
    print(“audio session set active !!”)
    } catch {

    }

    setupPlayer()

    addLifeCycleObserver()

    addRemoteCommandEvent()

    //追加(1)viewDidLoad内:ここから
    //(1)-1:動画が再生し終わったことを監視する設定
    NotificationCenter.default.addObserver(
    self, selector: #selector(self.endOfMovie),
    name: .AVPlayerItemDidPlayToEndTime, object: nil)

    //(1)-2:playerItemsの作成
    //①pathの設定
    let path1 = Bundle.main.path(forResource: “sample001″, ofType: “mp4″)
    let path2 = Bundle.main.path(forResource: “sample002”, ofType: “mp4”)
    let path3 = Bundle.main.path(forResource: “sample003”, ofType: “mp4″)
    let path4 = Bundle.main.path(forResource: “sample004”, ofType: “mp4”)
    let path5 = Bundle.main.path(forResource: “sample005”, ofType: “mp4”)
    let path6 = Bundle.main.path(forResource: “sample006”, ofType: “mp4”)
    let path7 = Bundle.main.path(forResource: “sample007”, ofType: “mp4”)
    let path8 = Bundle.main.path(forResource: “sample008”, ofType: “mp4”)
    let path9 = Bundle.main.path(forResource: “sample009”, ofType: “mp4”)
    let path10 = Bundle.main.path(forResource: “sample010”, ofType: “mp4″)
    //②urlの設定
    let url1 = URL(fileURLWithPath: path1!)
    let url2 = URL(fileURLWithPath: path2!)
    let url3 = URL(fileURLWithPath: path3!)
    let url4 = URL(fileURLWithPath: path4!)
    let url5 = URL(fileURLWithPath: path5!)
    let url6 = URL(fileURLWithPath: path6!)
    let url7 = URL(fileURLWithPath: path7!)
    let url8 = URL(fileURLWithPath: path8!)
    let url9 = URL(fileURLWithPath: path9!)
    let url10 = URL(fileURLWithPath: path10!)
    //③playerItemの設定
    let playerItem1 = AVPlayerItem(url: url1)
    let playerItem2 = AVPlayerItem(url: url2)
    let playerItem3 = AVPlayerItem(url: url3)
    let playerItem4 = AVPlayerItem(url: url4)
    let playerItem5 = AVPlayerItem(url: url5)
    let playerItem6 = AVPlayerItem(url: url6)
    let playerItem7 = AVPlayerItem(url: url7)
    let playerItem8 = AVPlayerItem(url: url8)
    let playerItem9 = AVPlayerItem(url: url9)
    let playerItem10 = AVPlayerItem(url: url10)
    //④配列playerItemsの作成
    playerItems = [playerItem1,playerItem2,playerItem3,playerItem4,playerItem5,playerItem6,playerItem7,playerItem8,playerItem9,playerItem10]
    //追加(1):ここまで

    }

    private func setupPlayer() {
    let fileName = “sample001”
    let fileExtension = “mp4”
    guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
    print(“Url is nil”)
    return
    }

    itemURL = url
    let item = AVPlayerItem(url: url)
    player = AVPlayer(playerItem: item)

    playerView.player = player

    }

    /// Playボタンが押された
    @IBAction func playBtnTapped(_ sender: Any) {
    player.play()
    player.rate = playerRate
    if let url = itemURL {
    setupNowPlaying(url: url)
    }
    }

    //追加(2):ここから
    //①パラメーターの設定
    var currentTrack:Int = 0
    var subCurrentTrack:Int = 0
    var playerItems:[AVPlayerItem] = []
    var playerRate:Float = 1.8
    //var totalCounterNumber:Int = 1

    //全体の繰り返し回数の表示用
    //@IBOutlet weak var totalCounter: UILabel!
    //②endOfMovewの定義
    @objc func endOfMovie() {//objc func(1)
    //ここにしたいことを記載
    //if currentTrack < 5{// <=この時は起こらない
    if currentTrack < 9{
    // if subCurrentTrack < 0{// <=この時は起こらない
    if subCurrentTrack < 2{
    currentTrack += 0
    subCurrentTrack += 1
    player.replaceCurrentItem(with: playerItems[currentTrack])
    playerView.player?.seek(to: CMTime.zero)
    player.play()
    player.rate = playerRate

    //動画が止まった時に検出、ここから
    var isPlaying: Bool {
    return player.rate != 0 && player.error == nil
    }
    print("isPlaying =",isPlaying)

    //動画が止まった時に検出、ここまで

    }else{
    currentTrack += 1
    subCurrentTrack = 0
    player.replaceCurrentItem(with: playerItems[currentTrack])
    playerView.player?.seek(to: CMTime.zero)
    player.play()
    player.rate = playerRate
    }
    }else{
    //if subCurrentTrack < 0{// <=この時は起こらない
    if subCurrentTrack MPRemoteCommandHandlerStatus in
    self.remoteTogglePlayPause(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.playCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePlay(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.pauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePause(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.nextTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remoteNextTrack(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.previousTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePrevTrack(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    }

    func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
    // イヤホンのセンターボタンを押した時の処理
    // (略)
    }

    func remotePlay(_ event: MPRemoteCommandEvent) {
    player.play()
    }

    func remotePause(_ event: MPRemoteCommandEvent) {
    player.pause()
    }

    func remoteNextTrack(_ event: MPRemoteCommandEvent) {
    // 「次へ」ボタンが押された時の処理
    // (略)
    }

    func remotePrevTrack(_ event: MPRemoteCommandEvent) {
    // 「前へ」ボタンが押された時の処理
    // (略)

    }

    }

  8. kkさん、こんにちは。
    あらためて、コメントをお送りいただきありがとうございます。
    これから、調べてみたいと思います。

  9. nackpan様

    kkです。投稿の掲載、ありがとうございます。
    掲載頂いた、コードを確認したところ、
    ・追加(2)ここまで
    ・上記と、そこから数行
    が欠落していましたので、念のため、もう一度、送付させて頂きます。
    よろしくお願いいたします。

    ====再々送付====
    kkと申します。以前は、バックグラウンドでの動画再生について
    丁寧なご説明を頂き、大変感謝しております。
    ありがとうございました。

    今回、その続きで、アドバイスを頂きたく、投稿させて頂きます。

    下記の仕様で、動画再生を、継続的に実行していると、突然、停止
    してしまうことがあります。

    【現象】
    ・複数動画の連続再生
     (語学学習用で、台詞毎の、短い動画の連続再生を意図しております。)
    ・動画一つの長さは約5秒〜10秒
    ・一つの動画を、複数回(2〜3回)繰り返した後、次の動画再生へ移行
    ・動画のサイズ:640×340、500kB〜1MB、コーデック:H.264
    ・再生例:動画①1回目再生 => 動画①2回目再生 => 動画②1回目再生 => 動画②2回目再生 =>・・・つづく
    ・全ての動画を、1回ずつ再生する場合は、問題ないが、各動画を複数回ずつ再生した時に発生
    ・再生速度を、上げると発生しやすくなる。
    ・再生したい動画のファイル数が増えると、発生しやすくなる。

    【発生条件の例】
    上記現象は、起こったり、起こらなかったりするが、下記条件にすると、高い確率で発生する。
    ・再生速度アップ:1.8 倍(通常速度でも発生するが、頻度が少ない)
    ・6個目までの動画では、起こらず、7個目以降で、同じ現象が起こりやすくなる。
    ・7個目の動画の再生が始まった直後に、停止する。

    【確認した内容】
    以下は、現象発生後、確認した内容です。
    ・再生ボタンを押すと、再び再生を始めることができるため、ハングではないように見える
    ・よく起こる箇所(繰り返し再生のところ)で、player.play()の後で、
     再生速度、エラーがあるかどうかを”isPlaying”で、チェックしたが、速度は、”=1.8”、エラーは”=nil”となる
    ・player.play()までは、動作しているが、再生が開始直後に停止しているので、再生終了通知が来ないまま停止し
     しており、再生状態の検出が「できない
    ・シミュレーターでは、発生しない

    【確認した実機】
    確認デバイスは、iPhone 6plus、iPad Pro(2nd Gene)で、iPhone 6plusは頻繁に、iPad proは
    稀(今まで約100時間ほど再生して、1回発生)に起こります。

    【お願い】
    下記の内容で、アドバイスを頂けないでしょうか。
    1)起こさなくすることができる回避策、案がありますでしょうか。
    2)もし起こった場合の検出と、再生継続させる方法は、ありますでしょうか。

    【ソースコード】
    使用しているソースコードは、nackpan様の、投稿記事のコードに、追加(1)、追加(2)を挿入したものです。

    ====== 以下、実際のコード ======

    import UIKit
    import AVFoundation
    import MediaPlayer
    import AVKit

    class ViewController: UIViewController {

    @IBOutlet weak var playerView: PlayerView!

    var player = AVPlayer()
    var itemURL: URL?

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    let audioSession = AVAudioSession.sharedInstance()
    do {
    try audioSession.setCategory(.playback, mode: .moviePlayback)
    } catch {
    print(“Setting category to AVAudioSessionCategoryPlayback failed.”)
    }
    do {
    try audioSession.setActive(true)
    print(“audio session set active !!”)
    } catch {

    }

    setupPlayer()

    addLifeCycleObserver()

    addRemoteCommandEvent()

    //追加(1)viewDidLoad内:ここから
    //(1)-1:動画が再生し終わったことを監視する設定
    NotificationCenter.default.addObserver(
    self, selector: #selector(self.endOfMovie),
    name: .AVPlayerItemDidPlayToEndTime, object: nil)

    //(1)-2:playerItemsの作成
    //①pathの設定
    let path1 = Bundle.main.path(forResource: “sample001″, ofType: “mp4″)
    let path2 = Bundle.main.path(forResource: “sample002”, ofType: “mp4”)
    let path3 = Bundle.main.path(forResource: “sample003”, ofType: “mp4″)
    let path4 = Bundle.main.path(forResource: “sample004”, ofType: “mp4”)
    let path5 = Bundle.main.path(forResource: “sample005”, ofType: “mp4”)
    let path6 = Bundle.main.path(forResource: “sample006”, ofType: “mp4”)
    let path7 = Bundle.main.path(forResource: “sample007”, ofType: “mp4”)
    let path8 = Bundle.main.path(forResource: “sample008”, ofType: “mp4”)
    let path9 = Bundle.main.path(forResource: “sample009”, ofType: “mp4”)
    let path10 = Bundle.main.path(forResource: “sample010”, ofType: “mp4″)
    //②urlの設定
    let url1 = URL(fileURLWithPath: path1!)
    let url2 = URL(fileURLWithPath: path2!)
    let url3 = URL(fileURLWithPath: path3!)
    let url4 = URL(fileURLWithPath: path4!)
    let url5 = URL(fileURLWithPath: path5!)
    let url6 = URL(fileURLWithPath: path6!)
    let url7 = URL(fileURLWithPath: path7!)
    let url8 = URL(fileURLWithPath: path8!)
    let url9 = URL(fileURLWithPath: path9!)
    let url10 = URL(fileURLWithPath: path10!)
    //③playerItemの設定
    let playerItem1 = AVPlayerItem(url: url1)
    let playerItem2 = AVPlayerItem(url: url2)
    let playerItem3 = AVPlayerItem(url: url3)
    let playerItem4 = AVPlayerItem(url: url4)
    let playerItem5 = AVPlayerItem(url: url5)
    let playerItem6 = AVPlayerItem(url: url6)
    let playerItem7 = AVPlayerItem(url: url7)
    let playerItem8 = AVPlayerItem(url: url8)
    let playerItem9 = AVPlayerItem(url: url9)
    let playerItem10 = AVPlayerItem(url: url10)
    //④配列playerItemsの作成
    playerItems = [playerItem1,playerItem2,playerItem3,playerItem4,playerItem5,playerItem6,playerItem7,playerItem8,playerItem9,playerItem10]
    //追加(1):ここまで

    }

    private func setupPlayer() {
    let fileName = “sample001”
    let fileExtension = “mp4”
    guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
    print(“Url is nil”)
    return
    }

    itemURL = url
    let item = AVPlayerItem(url: url)
    player = AVPlayer(playerItem: item)

    playerView.player = player

    }

    /// Playボタンが押された
    @IBAction func playBtnTapped(_ sender: Any) {
    player.play()
    player.rate = playerRate
    if let url = itemURL {
    setupNowPlaying(url: url)
    }
    }

    //追加(2):ここから
    //①パラメーターの設定
    var currentTrack:Int = 0
    var subCurrentTrack:Int = 0
    var playerItems:[AVPlayerItem] = []
    var playerRate:Float = 1.8
    //var totalCounterNumber:Int = 1

    //全体の繰り返し回数の表示用
    //@IBOutlet weak var totalCounter: UILabel!
    //②endOfMovewの定義
    @objc func endOfMovie() {//objc func(1)
    //ここにしたいことを記載
    //if currentTrack < 5{// <=この時は起こらない
    if currentTrack < 9{
    // if subCurrentTrack < 0{// <=この時は起こらない
    if subCurrentTrack < 2{
    currentTrack += 0
    subCurrentTrack += 1
    player.replaceCurrentItem(with: playerItems[currentTrack])
    playerView.player?.seek(to: CMTime.zero)
    player.play()
    player.rate = playerRate

    //動画が止まった時に検出、ここから
    var isPlaying: Bool {
    return player.rate != 0 && player.error == nil
    }
    print("isPlaying =",isPlaying)

    //動画が止まった時に検出、ここまで

    }else{
    currentTrack += 1
    subCurrentTrack = 0
    player.replaceCurrentItem(with: playerItems[currentTrack])
    playerView.player?.seek(to: CMTime.zero)
    player.play()
    player.rate = playerRate
    }
    }else{
    //if subCurrentTrack < 0{// <=この時は起こらない
    if subCurrentTrack MPRemoteCommandHandlerStatus in
    self.remoteTogglePlayPause(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.playCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePlay(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.pauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePause(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.nextTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remoteNextTrack(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.previousTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePrevTrack(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    }

    func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
    // イヤホンのセンターボタンを押した時の処理
    // (略)
    }

    func remotePlay(_ event: MPRemoteCommandEvent) {
    player.play()
    }

    func remotePause(_ event: MPRemoteCommandEvent) {
    player.pause()
    }

    func remoteNextTrack(_ event: MPRemoteCommandEvent) {
    // 「次へ」ボタンが押された時の処理
    // (略)
    }

    func remotePrevTrack(_ event: MPRemoteCommandEvent) {
    // 「前へ」ボタンが押された時の処理
    // (略)

    }

    }

  10. nackpan様
    kkです。

    再々送付しましたが、やはり欠落しているようなので、
    ・追加(2)の部分とその前後のみ
    を、以下に送付致します。
    ややこしくなり、申し訳ありませんが、よろしくお願いいたします。

    /// Playボタンが押された
    @IBAction func playBtnTapped(_ sender: Any) {
    player.play()
    player.rate = playerRate
    if let url = itemURL {
    setupNowPlaying(url: url)
    }
    }

    //追加(2):ここから
    //①パラメーターの設定
    var currentTrack:Int = 0
    var subCurrentTrack:Int = 0
    var playerItems:[AVPlayerItem] = []
    var playerRate:Float = 1.8
    //var totalCounterNumber:Int = 1
    //全体の繰り返し回数の表示用
    //@IBOutlet weak var totalCounter: UILabel!
    //②endOfMovewの定義
    @objc func endOfMovie() {//objc func(1)
    //ここにしたいことを記載
    //if currentTrack < 5{// <=この時は起こらない
    if currentTrack < 9{
    // if subCurrentTrack < 0{// <=この時は起こらない
    if subCurrentTrack < 2{
    currentTrack += 0
    subCurrentTrack += 1
    player.replaceCurrentItem(with: playerItems[currentTrack])
    playerView.player?.seek(to: CMTime.zero)
    player.play()
    player.rate = playerRate
    //動画が止まった時に検出、ここから
    var isPlaying: Bool {
    return player.rate != 0 && player.error == nil
    }
    print("isPlaying =",isPlaying)
    //動画が止まった時に検出、ここまで
    }else{
    currentTrack += 1
    subCurrentTrack = 0
    player.replaceCurrentItem(with: playerItems[currentTrack])
    playerView.player?.seek(to: CMTime.zero)
    player.play()
    player.rate = playerRate
    }
    }else{
    //if subCurrentTrack < 0{// <=この時は起こらない
    if subCurrentTrack < 2{
    currentTrack += 0
    subCurrentTrack += 1
    }else{
    currentTrack = 0
    subCurrentTrack = 0
    totalCounterNumber += 1
    //全体の繰り返し回数表示用
    totalCounter.text = String(totalCounterNumber)
    }
    player.replaceCurrentItem(with: playerItems[currentTrack])
    playerView.player?.seek(to: CMTime.zero)
    player.play()
    player.rate = playerRate
    }
    }
    //追加(2):ここまで

    @IBAction func pauseBtnTapped(_ sender: Any) {
    print("pauseBtn at L151")
    player.pause()
    }

  11. kkさん、こんにちは。
    手元の環境で実験してみました。
    iPhone 5s (iOS 12.4)、
    iPhone 6 Plus (iOS 12.4)、
    iPhone Xs Max (iOS 13.4)
    iPad Pro(12.9インチ)(第3世代) (iOS 13.4)
    で、各々一時間ほど検証を行いました。
    しかしながら、手元の環境では問題は発生しませんでした。

    そのため、なんとも改善の手立てについては申し上げることができません。
    お力になれず、申し訳ありません。

  12. nackpan様
    kkです。早速ご確認いただきありがとうございます。また、再現のできない提示になってしまい、申し訳ありません。確実に再現できる条件を明確にして、再度相談させて頂きます。お手数をお掛けし、申し訳ありませんでした。

  13. nackpan様
    ご無沙汰しております。kkです。
    以前は、何度も大変お世話になっており、大変感謝致しております。
    今回も、動画再生について、課題解決できずで、行き詰まっており
    アドバイスを頂きたく、勝手ながら投稿させて頂きます。

    2019年の12月に投稿されている
    ・[iOS]動画を再生する(AVPlayerLayer使用)
    のコードをベースに、
    ・複数動画の連続再生ができるようにコードを追加
    させていただき、バックグラウンドで動画連続再生を実行すると、
    ・1つの動画再生後、次の動画再生へ行かないことが、頻繁に起こった(OKの時もある)
    ので、下記のように、対策(1)、対策(2)を実施したところ、バックグラウンドで繰り返し再生を
    継続してできるようになりました。
    対策(1)バックグラウンドでの連続再生時に、1つの動画終了後、次の動画を再生するときに
         毎回、 playerView.player=nilを実行
    対策(2)1つの動画再生(player.play())実施後、毎回、playerView.player=playerに戻す
    このサンプルプログラムでは、以降、問題は発生せず数時間連続再生しても、再生停止は、しておりません。

    以上を基本に、英語、日本語のセリフ毎の動画を扱った、語学用のアプリを作成しようとしており、
    動画ファイル数を増やし、再生回数を選択できるようにしたり、必要な動画のみ再生、等の機能を
    追加しているのですが、以下のような、問題が起こっています。
    ・フォアグラウンドでの連続再生時は、問題なし
    ・再生開始後、すぐにバックグラウンドでの連続再生モードにすると、連続再生開始後、5分〜10分、長い時では、40分後に再生停止
    ・動作モードは、再生状態(playか、pauseかをトグルで動くようにした状態変数が、play状態のまま、の意味で、実際には、pauseに
     なっています。)のまま止まっている(<=pauseは、実行されてはいない)
    ・player.play()を実施後、endOfMovieへ移行していない(endOfMovieの最初のprint文が表示されない)ように見える
    ・再生ボタンを2回押す(トグルSW仕様で、play状態なので、pause=>play)と、再び再生開始するが、しばらくすると再度、再生停止が発生
    実機テストの結果は
    ・iPhone 6plus : iOS12.5.7では、上記現象は、起きない
    ・iPhone 6s plus:iOS15.7.3では、上記現象は、起きない
    ・iPad Pro(10.5inch):iOS16.4.1で発生
    ・iPhone 13:iOS16.4.1で発生
    ・iPhone 14plus:iOS16.4.1で発生

    お尋ねしたい内容は、以下の通りです。
    (1)そもそも、連続再生が継続されないときの、対策(1)(2)は、正しいでしょうか。
       なぜ、この対策でOKなのか、自分でもよくわかっておらず、試行錯誤の中で
       「うまく行った」に過ぎない対策であり、お恥ずかしい内容で、申し訳ありません。
    (2)何らかの原因で、再生停止してしまった場合、その時の状態を監視して、強制的に、endOfMovieへ
       移行させる方法は、ないものでしょうか。
       条件例:①状態変数を使っているのでパラメータは再生状態(pauseではない)かつ、player.rateが0等
    (3)(1)、(2)と同じになるのですが、こういった現象に対する、本来の対応策は、どのようなことが
       考えられるのでしょうか。

    お忙しい中、お手数をおかけし、申し訳ありませんが、アドバイしを頂ければありがたいです。
    以下は、追加した内容と、実際のコードです。
    使用している動画は映画のセリフひとつ分くらいなので、短いもので1〜2秒、長いもので、5〜10秒、
    サイズは、1920×1080ですが、rateを下げており、サイズは、1〜2M Bです。

    追加したこと
    (0)追加00:3つのパラメータ
    (1)追加01:複数の動画を連続再生させるための監視とplayerItemsの準備(viewDidLoad)
    (2)追加02:func playTrack()で、複数の動画を連続再生できるように設定、対策(1)、対策(2)をここで実施
    (3)追加03:追加01の監視で使用するendOfMovie()に、順次再生と繰り返し再生のコードを記載
    (4)追加04:対策(1)を実施するために、Background移行時に、Bool値の設定 background = false
    (5)追加05:対策(1)を実施するために、Foreground移行時に、Bool値の設定 background = true

    import UIKit
    import AVFoundation
    import MediaPlayer
    import AVKit

    class ViewController: UIViewController {

    //✳️追加00 ここから
    var currentTrack:Int = 0
    var playerItems:[AVPlayerItem] = []
    var background:Bool = false
    //✳️追加00 ここまで

    @IBOutlet weak var playerView: PlayerView!

    var player = AVPlayer()
    var itemURL: URL?

    override func viewDidLoad() {//vDL
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    let audioSession = AVAudioSession.sharedInstance()
    do {
    try audioSession.setCategory(.playback, mode: .moviePlayback)
    } catch {
    print(“Setting category to AVAudioSessionCategoryPlayback failed.”)
    }
    do {
    try audioSession.setActive(true)
    print(“audio session set active !!”)
    } catch {

    }

    setupPlayer()
    addLifeCycleObserver()
    addRemoteCommandEvent()
    addPeriodicTimeObserver()

    //✳️追加01 ここから
    //動画が再生し終わったことを監視する設定:vDL内に記載
    NotificationCenter.default.addObserver(
    self, selector: #selector(self.endOfMovie),
    name: .AVPlayerItemDidPlayToEndTime, object: nil)

    //playerItemsの作成
    //①pathの設定
    let path1 = Bundle.main.path(forResource: “mp4_001″, ofType: “mp4”)
    let path2 = Bundle.main.path(forResource: “mp4_002”, ofType: “mp4”)
    let path3 = Bundle.main.path(forResource: “mp4_003”, ofType: “mp4”)
    let path4 = Bundle.main.path(forResource: “mp4_004”, ofType: “mp4”)
    let path5 = Bundle.main.path(forResource: “mp4_005”, ofType: “mp4”)
    let path6 = Bundle.main.path(forResource: “mp4_006”, ofType: “mp4”)
    let path7 = Bundle.main.path(forResource: “mp4_007”, ofType: “mp4”)
    let path8 = Bundle.main.path(forResource: “mp4_008”, ofType: “mp4”)
    let path9 = Bundle.main.path(forResource: “mp4_009”, ofType: “mp4”)
    let path10 = Bundle.main.path(forResource: “mp4_010”, ofType: “mp4”)
    let path11 = Bundle.main.path(forResource: “mp4_011”, ofType: “mp4”)
    let path12 = Bundle.main.path(forResource: “mp4_012”, ofType: “mp4”)
    let path13 = Bundle.main.path(forResource: “mp4_013”, ofType: “mp4”)
    let path14 = Bundle.main.path(forResource: “mp4_014”, ofType: “mp4”)
    let path15 = Bundle.main.path(forResource: “mp4_015”, ofType: “mp4”)
    let path16 = Bundle.main.path(forResource: “mp4_016”, ofType: “mp4”)
    let path17 = Bundle.main.path(forResource: “mp4_017”, ofType: “mp4”)
    let path18 = Bundle.main.path(forResource: “mp4_018”, ofType: “mp4”)
    let path19 = Bundle.main.path(forResource: “mp4_019”, ofType: “mp4”)
    let path20 = Bundle.main.path(forResource: “mp4_020”, ofType: “mp4”)
    //②urlの設定
    let url1 = URL(fileURLWithPath: path1!)
    let url2 = URL(fileURLWithPath: path2!)
    let url3 = URL(fileURLWithPath: path3!)
    let url4 = URL(fileURLWithPath: path4!)
    let url5 = URL(fileURLWithPath: path5!)
    let url6 = URL(fileURLWithPath: path6!)
    let url7 = URL(fileURLWithPath: path7!)
    let url8 = URL(fileURLWithPath: path8!)
    let url9 = URL(fileURLWithPath: path9!)
    let url10 = URL(fileURLWithPath: path10!)
    let url11 = URL(fileURLWithPath: path11!)
    let url12 = URL(fileURLWithPath: path12!)
    let url13 = URL(fileURLWithPath: path13!)
    let url14 = URL(fileURLWithPath: path14!)
    let url15 = URL(fileURLWithPath: path15!)
    let url16 = URL(fileURLWithPath: path16!)
    let url17 = URL(fileURLWithPath: path17!)
    let url18 = URL(fileURLWithPath: path18!)
    let url19 = URL(fileURLWithPath: path19!)
    let url20 = URL(fileURLWithPath: path20!)
    //③playerItemの設定
    let playerItem1 = AVPlayerItem(url: url1)
    let playerItem2 = AVPlayerItem(url: url2)
    let playerItem3 = AVPlayerItem(url: url3)
    let playerItem4 = AVPlayerItem(url: url4)
    let playerItem5 = AVPlayerItem(url: url5)
    let playerItem6 = AVPlayerItem(url: url6)
    let playerItem7 = AVPlayerItem(url: url7)
    let playerItem8 = AVPlayerItem(url: url8)
    let playerItem9 = AVPlayerItem(url: url9)
    let playerItem10 = AVPlayerItem(url: url10)
    let playerItem11 = AVPlayerItem(url: url11)
    let playerItem12 = AVPlayerItem(url: url12)
    let playerItem13 = AVPlayerItem(url: url13)
    let playerItem14 = AVPlayerItem(url: url14)
    let playerItem15 = AVPlayerItem(url: url15)
    let playerItem16 = AVPlayerItem(url: url16)
    let playerItem17 = AVPlayerItem(url: url17)
    let playerItem18 = AVPlayerItem(url: url18)
    let playerItem19 = AVPlayerItem(url: url19)
    let playerItem20 = AVPlayerItem(url: url20)
    //④配列playerItemsの作成
    playerItems = [playerItem1,playerItem2,playerItem3,playerItem4,playerItem5,playerItem6,playerItem7,playerItem8,playerItem9,playerItem10,playerItem11,playerItem12,playerItem13,playerItem14,playerItem15,playerItem16,playerItem17,playerItem18,playerItem19,playerItem20]
    //✳️追加01 ここまで
    }//vDL

    //✳️追加03 ここから
    //endOfMovew時に実施するコマンド記載
    @objc func endOfMovie() {//func endOfMovie()

    //ここにしたいことを記述
    if currentTrack == 19{
    currentTrack = 0
    }else{
    currentTrack += 1
    }
    playerView.player = player
    playTrack()
    player.rate = Float(1.5)
    }//endOfMovie()
    //✳️追加03 ここまで

    //✳️追加02 ここから
    func playTrack(){
    player.replaceCurrentItem(with: playerItems[currentTrack])
    playerView.player?.seek(to: CMTime.zero)
    //■対策(1)再生が始まる前に、Backgroundか、Foregroundかを確認し、
    //Backgroundなら、playerView.player = nil
    //Foregroundなら、playerView.player = player
    //に設定
    if background == true{
    playerView.player = nil
    }else{
    playerView.player = player
    }
    player.play()
    //■対策(2)player.play()を実施後、playerView.player を、”player”に戻しておく
    playerView.player = player
    }
    //✳️追加02 ここまで

    private func setupPlayer() {

    let fileName = “mp4_020”
    let fileExtension = “mp4”
    guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension) else {
    print(“Url is nil”)
    return
    }
    itemURL = url
    let item = AVPlayerItem(url: url)
    player = AVPlayer(playerItem: item)

    playerView.player = player

    }

    /// Playボタンが押された
    @IBAction func playBtnTapped(_ sender: Any) {//func play
    player.play()
    if let url = itemURL {
    setupNowPlaying(url: url)
    }
    }

    @IBAction func pauseBtnTapped(_ sender: Any) {
    player.pause()
    }

    @IBAction func BackButton(_ sender: UIButton) {
    print(“BackBtn at FG”)
    if currentTrack == 1{
    print(“Do nothing”)
    endOfMovie()
    //player.play()
    }else{
    print(“currentTrack = currentTrack – 2”)
    currentTrack = currentTrack – 2

    endOfMovie()
    //player.play()
    }
    }

    @IBAction func ForwarButton(_ sender: UIButton) {
    print(“ForwardBtn at FG”)
    currentTrack += 0
    endOfMovie()
    }

    // MARK: Life Cycle
    func addLifeCycleObserver() {
    let center = NotificationCenter.default
    center.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
    center.addObserver(self, selector: #selector(didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }

    // フォアグラウンド移行時に呼び出されます
    @objc func willEnterForeground(_ notification: Notification) {
    /// (動画再生中であったときにそのままオーディオ再生を続ける場合は、このあとのコメントアウトをとりのぞいてください)
    /// (バックグラウンド移行時に、playerLayerからplayerがはずされているので、再度つなげます。)
    playerView.player = player
    //✳️追加04 ここから
    background = false
    //✳️追加04 ここまで
    }

    // バックグラウンド移行時に呼び出されます
    @objc func didEnterBackground(_ notification: Notification) {
    /// (動画再生中であったときにそのままオーディオ再生を続ける場合は、このあとのコメントアウトをとりのぞいてください)
    playerView.player = nil

    //✳️追加05 ここから
    background = true
    //✳️追加05 ここまで

    }

    // MARK: Now Playing Info
    func setupNowPlaying(url: URL) {
    // Define Now Playing Info
    var nowPlayingInfo = [String : Any]()
    // ここでは、urlのlastPathComponentを表示しています。
    nowPlayingInfo[MPNowPlayingInfoPropertyAssetURL] = url
    nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
    nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
    nowPlayingInfo[MPMediaItemPropertyTitle] = url.lastPathComponent
    nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = “”
    // Set the metadata
    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }

    func updateNowPlaying(time: Double) {
    var nowPlayingInfo = [String : Any]()
    nowPlayingInfo[MPMediaItemPropertyTitle] = itemURL?.lastPathComponent ?? (“currentTrack ” + String(currentTrack))
    nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = time
    if let duration = player.currentItem?.duration {
    nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = CMTimeGetSeconds(duration)
    }
    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }

    var timeObserverToken:Any?

    // MARK: Periodic Time Observer
    func addPeriodicTimeObserver() {
    // Notify every half second
    let timeScale = CMTimeScale(NSEC_PER_SEC)
    let time = CMTime(seconds: 0.25, preferredTimescale: timeScale)

    timeObserverToken = player.addPeriodicTimeObserver(forInterval: time,
    queue: .main)
    { [weak self] time in
    // update player transport UI
    DispatchQueue.main.async {
    // NowPlayingInfoCenterを更新
    self?.updateNowPlaying(time: CMTimeGetSeconds(time))
    }
    }
    }

    func removePeriodicTimeObserver() {
    //if let timeObserverToken = timeObserverToken {
    if let timeObserverToken01 = timeObserverToken {
    player.removeTimeObserver(timeObserverToken01)
    self.timeObserverToken = nil
    }
    }

    // MARK: Remote Command Event
    func addRemoteCommandEvent() {

    let commandCenter = MPRemoteCommandCenter.shared()
    commandCenter.togglePlayPauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remoteTogglePlayPause(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.playCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePlay(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.pauseCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePause(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.nextTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remoteNextTrack(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    commandCenter.previousTrackCommand.addTarget{ [unowned self] commandEvent -> MPRemoteCommandHandlerStatus in
    self.remotePrevTrack(commandEvent)
    return MPRemoteCommandHandlerStatus.success
    }
    }

    func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
    // イヤホンのセンターボタンを押した時の処理
    // (略)
    }

    func remotePlay(_ event: MPRemoteCommandEvent) {
    player.play()
    }

    func remotePause(_ event: MPRemoteCommandEvent) {
    player.pause()
    }

    func remoteNextTrack(_ event: MPRemoteCommandEvent) {
    print(“ForwardBtn at FG”)
    currentTrack += 0
    endOfMovie()
    //player.play()
    // 「次へ」ボタンが押された時の処理
    // (略)
    }

    func remotePrevTrack(_ event: MPRemoteCommandEvent) {
    print(“BackBtn at BG”)
    if currentTrack == 1{
    print(“Do nothing”)
    endOfMovie()
    //player.play()
    }else{
    print(“currentTrack = currentTrack – 1”)
    currentTrack = currentTrack – 2
    playerView.player = player
    endOfMovie()
    //player.play()
    }
    // 「前へ」ボタンが押された時の処理
    // (略)

    }

    }

  14. kkさん、こんにちは
    コメントありがとうございます

    お伝えいただいたソースコードを元に、こちらでも実験して調べてみました

    対策(1)(2)をコメントアウトして実行してみると、「1つの動画再生後、次の動画再生へ行かない」という事態が発生するのを確認しました。
    調べていくと、
endOfMovie()内の
    playerView.player = player
    が原因ではないかと思います。

    playerView.player = player
    は、PlayerViewの動画表示画面とAVPlayerを結びつける処理となります。
    この処理で、動画画像が表示されることになります。
    playerView.player = nil
    とすると、PlayerViewは、AVPlayerと関係がなくなります。よって、動画画像も消えます。
    試しに、ボタンを一つ追加して、@IBActionで結んで
    @IBAction func debugBtnTapped(_ sender: Any) {
    if (playerView.player != nil) {
    playerView.player = nil
    } else {
    playerView.player = player
    }
    }

    としてもらうと、このボタンを押すたびに、動画画像が現れ、消えるのを確認できると思います

    さて、Apple Developerのドキュメントにあるように
    https://developer.apple.com/documentation/avfoundation/media_playback/creating_a_basic_video_player_ios_and_tvos/playing_audio_from_a_video_asset_in_the_background
    AVPlayerの仕様で、バックグラウンド移行時に、オーディオを途切れないで再生し続けるには、
    @objc func didEnterBackground(_ notification: Notification) {
    playerView.player = nil
    }

    といったふうに、PlayerViewとplayerとの結びつきを外す必要があります。

    しかし、今回お伝えいただいたコードではバックグラウンドに移行した後、すぐにファイル再生が終わった場合、endOfMovie()が呼び出され、
    そこで、
    
playerView.player = player
    が呼び出されます。
    これによって、AVPlayerの仕様として、オーディオの再生を停止したものと思われます。

    ここまで調査を進めて、実は疑問を感じました。
    バックグラウンド移行時にはplayerView.player = nilとなっていて、
    それから0.5秒後ぐらいにplayerView.player = playerとなる。
    それでも、オーディオの再生は止まる、ということがあるのだろうか?

    実験として、
    @objc func didEnterBackground(_ notification: Notification) {
    playerView.player = nil

    /// 0.9秒後にPlayerViewとplayerを結びつける
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.9, execute: {
    self.playerView.player = self.player

    })

    }
    といった処理を試してみました。
これで、再生中にバックグラウンドに移行すると、オーディオは止まりました。
    1秒後やもっと大きい数値にすると、オーディオは止まりませんでした。

    AVPlayerの仕様としては、バックグラウンドに移行して0.9秒あたりで playerView.player = nilになっていれば、オーディオはそのまま再生される。
そうでないなら、オーディオは一時停止となる、ということのようです。

    ——

    今回、お伝えいただいたコードでは、
    endOfMovie()内の
    playerView.player = player
    が、あるため、バックグラウンド移行とファイル末尾到達のタイミングによって、オーディオが一時停止となったのだと思います。

    お伝えいただいたコードの変更案としては、
    @objc func endOfMovie() {//func endOfMovie()
    if currentTrack == playerItems.count - 1 {
    currentTrack = 0
    }else{
    currentTrack += 1
    }
    playTrack()
    player.rate = Float(1.5)
    }

    func playTrack(){
    player.replaceCurrentItem(with: playerItems[currentTrack])
    player.seek(to: CMTime.zero)
    player.play()
    }

    といったものはどうでしょうか

  15. nackpan様
    kkです。
    早速のご返信を頂きありがとうございました。
    今回、ご提案頂いた内容で、実機でのバックグラウンド再生を実施してみますと
    以下のような結果となりました。
    ・iPhone 6plus (iOS12.5.7) シームレスにバックグラウンドへ移行後、停止することなく、複数の動画再生を継続
    ・iPhone 6s plus (iOS15.7.6) シームレスにバックグラウンドへ移行後、20個目の動画再生後、再生が停止し、currentTrackのカウントアップの繰り返し状態になる
    ・iPad Pro(10.5inch) (iOS16.4.1) シームレスにバックグラウンドへ移行後、20個目の動画再生後、再生が停止し、currentTrackのカウントアップの繰り返し状態になる
    ・iPhone 13(iOS16.4.1) シームレスにバックグラウンドへ移行後、20個目の動画再生後、連続再生できず、currentTrackのカウントアップの繰り返し状態になる
    上記の、「20個目の動画」というのは、試験用の動画を20個並べているので、「全動画再生後」の意味です。
    これを、10個にすると、10個の動画再生後、連続再生を継続できず、currentTrack()のカウントアップが回り続ける状態になってしまいます。

    ここで、endOfMovie内のcurrentTrackカウントアップの中で、下記のような、試験(1)、試験(2)を試したところ、試験(2)の方だと、20個の動画連続再生後、継続して動画連続再生ができましたが、試験(1)では、20個連続再生後、再生が止まってしまい、currentTrackのカウントアップループだけが回っている状態になりました。

    @objc func endOfMovie() {//func endOfMovie()

    //追加
    print(“currentTrack =“, currentTrack)
    
if currentTrack == playerItems.count – 1 {
currentTrack = 0

    //試験(1)下記を追加
    //playerView.player = nil
    //試験(2)下記を追加
    //playerView.player = player
    
}else{
currentTrack += 1
}

    ここまでで、わからないことは、
    ①playerItems[currentTrack]の、currentTrackの値が、max(19)から0に移行するところで、なぜ、連続再生ループが止まってしまうのか。
    ②playerView.playerを実行すると、なぜ、連続再生を継続できるのか
    ③iPhone6は、なぜ、連続再生を継続できるのか
    で、ございます。
    こういう実験をしてみては?、など、ご提案いただければ、試してみたく、お手数をおかけして申し訳ありませんが、アドバイスを頂きたく、どうかよろしくお願い致します。

  16. kkさん、こんにちは
    コメントありがとうございます

    > 20個目の動画再生後、連続再生できず、currentTrackのカウントアップの繰り返し状態になる
    とのこと

    また、endOfMovie()に、
playerView.player = player
を追加すると、連続再生ができる、とのこと。

    「currentTrackのカウントアップの繰り返し」となるということは、endOfMovie()は繰り返し呼び出されている、と受け取りました
    とすると、playerのseek部分がうまく働いていないのかもしれません。
    
試験(2)下記を追加
    playerView.player = player
    の処理で、連続再生ができるようになった、というのは、この処理でseekが働くようになったのかもしれません。

    endOfMovie()では、そこから、playTrack()が呼び出されてますが、

    func playTrack(){
    player.replaceCurrentItem(with: playerItems[currentTrack])
    player.seek(to: CMTime.zero)
    player.play()
    }

    となっているか、ご確認ください。
こちらは前回の返信時のコードです。
元々お伝えいただいたコードから、
playerView.player?.seek(to: CMTime.zero)
    の部分が、
    player.seek(to: CMTime.zero)
    と変更になっています。
    元の記述では、playerView.playerとplayerが結びついている場合は、
player.seek(to: CMTime.zero)
と同じこととなりますが、nilの場合、
    playerView.player?.seek(to: CMTime.zero)
    で、なにも起こらず、seekできません。

    itemが末尾に到達後、そのitemの再生位置がzeroに戻らないと、AVPlayerでは再生がすぐ終わる、あるいはとばされてしまう、といった状態になります。
    そのため、連続再生できない状態になったのではないか、と推測します

    また、複数のファイルでの連続再生の実験となると、
    * 単独のファイルの再生で発生する問題
    * 2つのファイルの再生で発生する問題
    * 3つのファイルの再生で発生する問題
 * 多数のファイルの再生で発生する問題
    * 非常に多数のファイルの再生で発生する問題
    と、さまざまなパターンで問題が発生することがありますので、実験の際には、少ないファイル数から動作確認をするのをおすすめします。

  17. nackpan様
    kkです。
    アドバイスありがとうございます。大変貴重なアドバイスを頂き、感謝申し上げます。
    ご指摘のとおりで、私が実験しているコードは、seekの部分は
    playerView.player?.seek(to: CMTime.zero)
    となっておりました。
    これは、複数動画の連続再生やループ再生に関する投稿記事を参考にして、使っていたコマンドです。
    この部分を、アドバイス頂いた、下記に変更すると、カウントアップだけの状態に入ることはなくなりました。
    player.seek(to: CMTime.zero)

    今後、
    ①このseekに関して、知識を深めること
    ②お薦めいただいたような、下記の内容を一つずつ検証していく
    * 単独のファイルの再生で発生する問題

    * 2つのファイルの再生で発生する問題

    * 3つのファイルの再生で発生する問題

    * 多数のファイルの再生で発生する問題

    * 非常に多数のファイルの再生で発生する問題

    そこで、質問が一つと、お願いが一つあるのですが、お時間あれば、お願いできますでしょうか。
    質問:
    非常に初歩的質問で申し訳ありませんが、現在使用している下記のコードで、player.play()をコメントアウトしても、連続再生が継続されているのですが、player.play()は、▶️ボタン押し後、一度、実行すれば、あとは、player.replaceCurrentItem(with: playerItems[currentTrack])
    のcurrentTrackが変更されれば、連続再生継続できている、という解釈は、正しいでしょうか。
    func playTrack(){
player.replaceCurrentItem(with: playerItems[currentTrack])
player.seek(to: CMTime.zero)
//player.play() <=✴️たまたま、コメントアウトしたまま、戻すのを忘れていた
}
    お願い:
    seekと、replaceCurrentItem(with:)に関して、知識を深めようとして、Article(ご紹介いただいた、playerView.player=nilのような実際の使用事例)を探して、Apple Developerで検索してみるのですが、なかなか辿り着けず、参考になるような記事があれば、ご紹介いただけたらありがたいです。

    お手数をおかけし申し訳ありませんが、どうぞ、よろしくお願い致します。

  18. kkさん、こんにちは
    コメントありがとうございます

    > player.play()をコメントアウトしても、連続再生が継続されている

    AVPlayerの「再生」に関しては、私も困惑した覚えがあります。
    endOfMovie()内で、
    player.rate = Float(1.5)
    とありますが、この部分、「再生時の速度設定」だけでなく、「その速度にして再生」というAVPlayerの仕様となっています。
    そのため、playTrack()内のplayer.play()をコメントアウトしても、そのあとのこの部分があることで再生が継続されます。

    playTrack()内のplayer.play()をコメントアウトした上で、
さらに
    endOfMovie()内のplayer.rate = Float(1.5)もコメントアウトすると、
    最初のアイテムが再生終了後、アイテムが切り替わったところでポーズ状態になると思います。

    また、動画・音声の再生やSwiftの文法の調査に関してですが、コメントで書くには長くなりすぎたので、blog記事にしました

    Appleの公式ドキュメント上の動画の再生に関する記事やサンプルコード

    Swiftを学習するのに助けになるサイト

    参考になれば幸いです

  19. nackpan様
    player.play( )の記述に対するコメント、ありがとうございました。
    また、AVFoundationのご紹介についても、ありがとうございます。
    フレームワークとは?
    くらいからの学習になりますが、アドバイス頂いた内容をベースに
    この辺りのドキュメント、サンプルコードを学習致します。
    今回も、とても貴重な情報の数々、ありがとうございました。

コメントする

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください