1

I need to play some audios in a row, and therefore using AVQueuePlayer for that purpose. I'm loading audios in it as :

for (NSInteger i = row ; i<_messagesArray.count ; i++)
    {
        _urlString = ((PFFile*)(_messagesArray[i][@"messageFile"])).url;

        AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[NSURL URLWithString:_urlString] options:nil];
        NSArray *keys = @[@"playable"];


        AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];

        // Subscribe to the AVPlayerItem's DidPlayToEndTime notification.
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemDidFinishPlaying:) name:AVPlayerItemDidPlayToEndTimeNotification object:item];



        if (_queuePlayer == nil)
        {
            _queuePlayer = [[AVQueuePlayer alloc] initWithPlayerItem:item];

        }
        else
        {   /*This loads Async*/
            [asset loadValuesAsynchronouslyForKeys:keys completionHandler:^() {
                [_queuePlayer insertItem:item afterItem:nil];
            }];
        }

    }

[_queuePlayer play];

Now the problem is that since I'm loading the assets from URL via Async (See else condition in the code above), the order of audio files in my AVQueuePlayer is not same as the URLs in the _messagesArray

e.g : _messagesArray = audioURL1 , audioURL2, audioURL3

then due to async, whichever loads first, goes into AVQueuePlayer first

AVQueuePlayer = audio2 (from URL2) , audio1 (from URL1) , audio3 (from URL3)

Please suggest a solution to maintain the order in my player.

Thanks.

Hyder
  • 1,163
  • 2
  • 13
  • 37

2 Answers2

2

I know you are writing an App in Objective-c, but here is a code I wrote doing exactly what you need in Swift, hope it helps you and anyone looking for the same solution.

import Foundation
import AVFoundation
class AssetsLoader : NSObject {
    public typealias AssetsLoaderCallback = ((IndexPath,AVPlayerItem)->Void)
    private var callback: AssetsLoaderCallback
    private(set) var loadedAssets: [IndexPath:AVURLAsset] = [:]
    private(set) var pendingAssets: [IndexPath:AVURLAsset] = [:]
    static var assetKeysRequiredToPlay = [
        "playable",
        "tracks",
        "duration"
    ]
    init(callback:@escaping AssetsLoaderCallback) {
        self.callback = callback
    }

    func loadAssetsAsync(assets:[IndexPath:AVURLAsset]) {
        loadedAssets = [:]
        pendingAssets = [:]
        for (key, value) in assets {
            loadAssetAsync(index: key, asset: value)
        }
    }

    private func loadAssetAsync(index:IndexPath,asset:AVURLAsset) {
        asset.loadValuesAsynchronously(forKeys: AssetsLoader.assetKeysRequiredToPlay) { [weak self] in
            for key in AssetsLoader.assetKeysRequiredToPlay {
                var error : NSError?
                if asset.statusOfValue(forKey: key, error: &error) == .failed {
                    //FIXME: Asset Could not load
                    print("Asset Could not load")
                }
            }
            if !asset.isPlayable {
                print("Cant play, move to next asset?")
            } else {
                // Asset Ready, Check if
                self?.prepareAssetForCallback(index: index, asset: asset)
            }
        }
    }
    private func prepareAssetForCallback(index:IndexPath,asset:AVURLAsset) {
        if index.row == 0 {
            /// First Asset
            loadedAssets[index] = asset
            self.sendReadyAsset(index: index)
            if let nextInline = pendingAssets[index.rowSuccessor()] {
                self.freePendingAsset(index: index.rowSuccessor(), asset: nextInline)
            }
        } else {
            self.freePendingAsset(index: index, asset: asset)
        }
    }

    private func freePendingAsset(index:IndexPath,asset:AVURLAsset) {
        if loadedAssets[index.rowPredecessor()] != nil && loadedAssets[index] == nil {
            loadedAssets[index] = asset
            self.sendReadyAsset(index: index)
            if let nextInline = pendingAssets[index.rowSuccessor()] {
                self.freePendingAsset(index: index.rowSuccessor(), asset: nextInline)
            }
        } else {
            if pendingAssets[index] == nil {
                pendingAssets[index] = asset
            }
        }
    }

    private func sendReadyAsset(index:IndexPath) {
        self.callback(index, AVPlayerItem(asset:self.loadedAssets[index]!))
    }
}

NSIndexPath extension:

extension IndexPath {

    func rowPredecessor() -> IndexPath {
        assert(self.row != 0, "-1 is an illegal index row")
        return IndexPath(row: self.row - 1, section: self.section)
    }

    func rowSuccessor() -> IndexPath {
        return IndexPath(row: self.row + 1, section: self.section)
    }
}

Just Initialize the class with a callback, and pass call the method loadAssetAsync with a dictionary of an NSIndexPath and AVURLAsset each time you want to load some assets.

Samer Murad
  • 2,479
  • 25
  • 27
1

I have been stuck on this same problem as you have been but by using this answer : https://stackoverflow.com/a/34897956/12234693 and making the calls to create assets in a separate DispatchGroup() I was able to solve the problem. The code if you or anyone stuck at the same problem is as follows :

func makeItem(url: String) {
        let avAsset = AVURLAsset(url: URL(string: url)!)
        let group = DispatchGroup()
        group.enter()
        avAsset.loadValuesAsynchronously(forKeys: ["playable", "tracks", "duration"]) {

        }
        group.leave()
        group.notify(queue: .main) {
                self.enqueue(avAsset: avAsset)
        }
    }
func enqueue(avAsset: AVURLAsset) {
        let item = AVPlayerItem(asset: avAsset)
        self.player2!.insert(item, after: nil)
    }