AVQueuePlayerを用いた音声ファイルの連続再生

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,277

Nefytus

score 10

あらかじめアプリ内に用意された複数の音声ファイルを、AVQueuePlayerを使って連続再生するアプリを作成しています。
こちらの、Appleが提供しているコードを使用させていただいています。
Apple - AVFoundation Looping Player

ここでお尋ねしたいのが、addItemというボタンを使用せずに、アプリを起動した時点ですでにアプリ内に用意された全ての音声ファイルがキューにセットされている状態にする方法です。
アプリ起動後、再生ボタンを押すとセットされた音声ファイルが再生されるようにしたいです。

以下にある、addItemのボタンの機能を参考にするのかと考えていますが、自動で全音声ファイルをキューに加える方法が分からず、質問させていただきました。

@IBAction func addItemToQueueButtonPressed(_ sender: UIButton) {
        let alertTitle = NSLocalizedString("popover.title.addItem", comment: "Title of popover that adds items to the queue")

        let alertMessage = NSLocalizedString("popover.message.addItem", comment: "Message on popover that adds items to the queue")

        let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .actionSheet)

        // Populate the sheet with the titles of the assets we have loaded.
        for (loadedAssetTitle, loadedAsset) in loadedAssets {
            let alertAction = UIAlertAction(title:loadedAssetTitle, style: .default) { [unowned self] alertAction in
                let oldItems = self.player.items()

                let newPlayerItem = AVPlayerItem(asset: loadedAsset)

                self.player.insert(newPlayerItem, after: nil)

                self.queueDidChangeWithOldPlayerItems(oldPlayerItems: oldItems, newPlayerItems: self.player.items())
            }

            alertController.addAction(alertAction)
        }

        let cancelActionTitle = NSLocalizedString("popover.title.cancel", comment: "Title of popover cancel action")

        let cancelAction = UIAlertAction(title: cancelActionTitle, style: .cancel, handler: nil)

        alertController.addAction(cancelAction)

        presentModalPopoverAlertController(alertController: alertController, sender: sender)
    }

教えていただいたように一番下の部分に追加したところ、19個用意した音声ファイルが複数回選択され、全部で189個のキューが自動で設定されるようになりました。
これについては、for文の部分で回数の設定などを行うことで19個のキューにすることができるのでしょうか?

class PlayerViewController: UIViewController, UICollectionViewDataSource {
    static let assetKeysRequiredToPlay = [
        "playable",
        "hasProtectedContent"
    ]

    let player = AVQueuePlayer()

    var currentTime: Double {
        get {
            return CMTimeGetSeconds(player.currentTime())
        }

        set {
            let newTime = CMTimeMakeWithSeconds(newValue, 1)
            player.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
        }
    }

    var duration: Double {
        guard let currentItem = player.currentItem else { return 0.0 }

        return CMTimeGetSeconds(currentItem.duration)
    }

    var rate: Float {
        get {
            return player.rate
        }

        set {
            player.rate = newValue
        }
    }

    var playerLayer: AVPlayerLayer? {
        return playerView.playerLayer
    }

    /*
        A formatter for individual date components used to provide an appropriate
        value for the `startTimeLabel` and `durationLabel`.
    */
    let timeRemainingFormatter: DateComponentsFormatter = {
        let formatter = DateComponentsFormatter()
        formatter.zeroFormattingBehavior = .pad
        formatter.allowedUnits = [.minute, .second]

        return formatter
    }()

    var timeObserverToken: Any?

    var assetTitlesAndThumbnails: [URL: (title: String, thumbnail: UIImage)] = [:]

    var loadedAssets = [String: AVURLAsset]()

    @IBOutlet weak var timeSlider: UISlider!
    @IBOutlet weak var startTimeLabel: UILabel!
    @IBOutlet weak var durationLabel: UILabel!
    @IBOutlet weak var rewindButton: UIButton!
    @IBOutlet weak var playPauseButton: UIButton!
    @IBOutlet weak var fastForwardButton: UIButton!
    @IBOutlet weak var clearButton: UIButton!
    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var queueLabel: UILabel!
    @IBOutlet weak var playerView: PlayerView!

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)


        addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.duration), options: [.new, .initial], context: &playerViewControllerKVOContext)
        addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.rate), options: [.new, .initial], context: &playerViewControllerKVOContext)
        addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.status), options: [.new, .initial], context: &playerViewControllerKVOContext)
        addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem), options: [.new, .initial], context: &playerViewControllerKVOContext)

        playerView.playerLayer.player = player

        let manifestURL = Bundle.main.url(forResource: "MediaManifest", withExtension: "json")!
        asynchronouslyLoadURLAssetsWithManifestURL(jsonURL: manifestURL)

        let interval = CMTimeMake(1, 1)
        timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main) { [unowned self] time in
            let timeElapsed = Float(CMTimeGetSeconds(time))

            self.timeSlider.value = Float(timeElapsed)
            self.startTimeLabel.text = self.createTimeString(time: timeElapsed)
        }

    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        if let timeObserverToken = timeObserverToken {
            player.removeTimeObserver(timeObserverToken)
            self.timeObserverToken = nil
        }

        player.pause()

        removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.duration), context: &playerViewControllerKVOContext)
        removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.rate), context: &playerViewControllerKVOContext)
        removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.status), context: &playerViewControllerKVOContext)
        removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem), context: &playerViewControllerKVOContext)
    }

    func asynchronouslyLoadURLAsset(asset: AVURLAsset, title: String, thumbnailResourceName: String) {

        asset.loadValuesAsynchronously(forKeys: PlayerViewController.assetKeysRequiredToPlay) {


            DispatchQueue.main.async() {



                for key in PlayerViewController.assetKeysRequiredToPlay {
                    var error: NSError?

                    if asset.statusOfValue(forKey: key, error: &error) == .failed {
                        let stringFormat = NSLocalizedString("error.asset_%@_key_%@_failed.description", comment: "Can't use this AVAsset because one of it's keys failed to load")

                        let message = String.localizedStringWithFormat(stringFormat, title, key)

                        self.handleError(with: message, error: error)

                        return
                    }
                }


                if !asset.isPlayable || asset.hasProtectedContent {
                    let stringFormat = NSLocalizedString("error.asset_%@_not_playable.description", comment: "Can't use this AVAsset because it isn't playable or has protected content")

                    let message = String.localizedStringWithFormat(stringFormat, title)

                    self.handleError(with: message)

                    return
                }


                self.loadedAssets[title] = asset

                let name = (thumbnailResourceName as NSString).deletingPathExtension
                let type = (thumbnailResourceName as NSString).pathExtension
                let path = Bundle.main.path(forResource: name, ofType: type)!

                let thumbnail = UIImage(contentsOfFile: path)!

                self.assetTitlesAndThumbnails[asset.url] = (title, thumbnail)

                for (loadedAssetTitle, loadedAsset) in self.loadedAssets {
                    let newPlayerItem = AVPlayerItem(asset: loadedAsset)
                    self.player.insert(newPlayerItem, after: nil)
                }

            }
        }
    }

よろしくお願いいたします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

0

「Apple - AVFoundation Looping Player」のサンプルの中のどこに
質問のソースコードのaddItemToQueueButtonPressedメソッドがあるのか
見つかりませんでしたが、質問のソースコードの中で音声ファイルをキューに加える処理は

let newPlayerItem = AVPlayerItem(asset: loadedAsset)
self.player.insert(newPlayerItem, after: nil)


の部分になると思いますので、loadedAssetsに入っている全ファイルをキューに加えるなら

for (loadedAssetTitle, loadedAsset) in loadedAssets {
    let newPlayerItem = AVPlayerItem(asset: loadedAsset)
    self.player.insert(newPlayerItem, after: nil)
}


という感じだろうと思います。
そして、それをアプリ起動時に実施したいなら、初期起動されるViewControllerの
viewDidLoadあたりで実施すればよいと思います。
もちろんその処理の前にloadedAssetsやself.playerを初期化しておく必要があります。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/02/23 09:30 編集

    ご丁寧に教えていただきありがとうございます。

    もしよろしければお尋ねしたいのですが、
    元の質問にも追記させていただいたように、キューが189個になるのはfor文の書き方で修正できるものでしょうか?

    キャンセル

  • 2017/02/23 11:30 編集

    先にloadedAssetsを作ってから後でそれに対応するAVPlayerItemを作るのではなくて、loadedAssetsを作りながらAVPlayerItemも一緒に作るのであれば、そんなやり方しちゃダメだと思います。
    今は、loadedAssetsに入っているファイルを毎回全部追加していますから、1+2+3+4+...+17+18+19=190個の追加になってしまうのだと思います。
    そのような構造の中でAVPlayerItemを作るのであれば、回答に示したfor文は不要で
    ```
    let newPlayerItem = AVPlayerItem(asset: asset)
    self.player.insert(newPlayerItem, after: nil)
    ```
    とするだけでよさそうに思います。
    自分が作ったプログラムがどのように動いているか、print()を出力したり、ブレークポイントを張ってステップ実行したりして、きちんと把握することをお勧めします。

    キャンセル

  • 2017/02/23 14:18

    形も不格好で、自分自身がよく把握できていないものを作ってしまいお恥ずかしいです。
    教えていただいたように、次に生かせるような作り方をしようと思います。
    ありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.21%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる