Nikita Goncharov
Nikita Goncharov

Reputation: 173

How to reload data for AVPlayer, if URL failed swift

I have json file with a few URLs (with .mp3) for words. Some of URLs are invalid (or valid, but return error, so i don't get data anyway).

This URL i use to play pronunciation for word. So, I go throw 3 steps:

  1. Finding URL for certain word. If can't find, than nothing happens
  2. Use this URL for initialising AVPlayerItem and preparing AVPlayer. Than just waiting, when user press.
  3. Play sound, when user press onto word

So, first of all, i'm preparing my AVPlayer, to avoid delay in playing.

I'm a little bit confused with multithreading, and i don't understand where should i check if I'm able to play this sound, or not and i should use next URL.

Code:

extension WordCell {

func playPronunciation() {
    player?.play()
    player?.seek(to: .zero)
}

func prepareForPronunciation() {
    if let word = myLabel.text {
        UIApplication.shared.isNetworkActivityIndicatorVisible = true
        DispatchQueue.global(qos: .userInteractive).async { [weak self] in
            let foundURL = self?.findURL(for: word)
            if let url = foundURL {
                let playerItem = AVPlayerItem(url: url)

                //here "playerItem.status" always equals .unknown (cause not initialized yet)
                if playerItem.status == .failed {
                     //self?.giveNextUrl() - will do some code there
                }
                self?.player = AVPlayer(playerItem: playerItem)
                self?.player!.volume = 1.0
            }
            // this is also not correct, because networking continueing
            // but i don't know where to place it
            DispatchQueue.main.async {
                UIApplication.shared.isNetworkActivityIndicatorVisible = false
            }
        }
    }
}

// right now i take URL from file, where there is only one.
// but i will use file with a few URL for one word
private func findURL(for word: String) -> URL? {

    if let path = Bundle.main.path(forResource: "data", ofType: "json") {
        do {
            let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
            let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
            if let jsonResult = jsonResult as? [String: String] {
                if let url = jsonResult[word] {
                    return URL(string: url)
                } else {
                    return nil
                }
            }
        } catch {
            return nil
        }
    }
    return nil
}

}

This is json file with few URLs per a word

"abel": [
    "http://static.sfdict.com/staticrep/dictaudio/A00/A0015900.mp3",
    "http://img2.tfd.com/pron/mp3/en/US/d5/d5djdgdyslht.mp3",
    "http://img2.tfd.com/pron/mp3/en/UK/d5/d5djdgdyslht.mp3",
    "http://www.yourdictionary.com/audio/a/ab/abel.mp3"
],
"abele": [
    "http://www.yourdictionary.com/audio/a/ab/abele.mp3",
    "http://static.sfdict.com/staticrep/dictaudio/A00/A0016300.mp3",
    "http://www.oxforddictionaries.com/media/english/uk_pron/a/abe/abele/abele__gb_2_8.mp3",
    "http://s3.amazonaws.com/audio.vocabulary.com/1.0/us/A/1B3JGI7ALNB2K.mp3",
    "http://www.oxforddictionaries.com/media/english/uk_pron/a/abe/abele/abele__gb_1_8.mp3"
],

So, i need to take first URL and check it. If failed, then take another and check ... and etc, while URLs are over, or find some valid URL. And all this stuff must be done before AVPlayer will try to play sound.

How to implement this and where?

Please, tell and describe resolution in simple words, cause i'm kind of beginner in swift and multithreading.

Upvotes: 2

Views: 4607

Answers (3)

Yonathan Goriachnick
Yonathan Goriachnick

Reputation: 201

SwiftUI Solution:

I had the same issue/question when working with an embedded view in SwiftUI and needed to monitor the status of the play item. I needed to show a placeholder if the link video was corrupt or wrong. Eventually what I did is that:

  let videoURL: URL
  @Published private var player: AVPlayer
  @Published private var hasLoadingError: Bool = true

  var cancellable = Set<AnyCancellable>()

  init(videoURL: URL) {
    self.videoURL = videoURL
    self._player = State(initialValue: AVPlayer(url: videoURL))
    
    player
        .currentItem? // create a publisher on the item, not the player
        .publisher(for: \AVPlayerItem.status) 
        .sink { status in
            print(status.rawValue)
            // here you can track the player item status
            // and if it's ".failed" you can do what you need
            if status == .failed {
               self.hasLoadingError = true
            }
        }
        .store(in: &cancellable)
}
..... 
}

Upvotes: 0

Peter Pajchl
Peter Pajchl

Reputation: 2709

I would use the AVPlayerItem.Status property to see when it failed. In your current code you are checking the status immediately after creating the item which will always yield the same result as when you init AVPlayerItem the status is by default unknown.

The AVPlayerItem gets enqueued once you associate with the player. To be able to track the status changes you want to setup an observer.

The documentation https://developer.apple.com/documentation/avfoundation/avplayeritem still suggests the "old-style" using addObserver but based on your preference I would opt for the newer block-style.

// make sure to keep a strong reference to the observer (e.g. in your controller) otherwise the observer will be de-initialised and no changes updates will occur
var observerStatus: NSKeyValueObservation?


// in your method where you setup your player item
observerStatus = playerItem.observe(\.status, changeHandler: { (item, value) in
    debugPrint("status: \(item.status.rawValue)")
    if item.status == .failed {
        // enqueue new asset with diff url
    }
})

You could also setup similar observer on the AVPlayer instance as well.


Updated with full example - this code is far from perfect but demonstrates the benefit of observer

import UIKit
import AVFoundation

class ViewController: UIViewController {
    var observerStatus: NSKeyValueObservation?
    var currentTrack = -1
    let urls = [
        "https://sample-videos.com/audio/mp3/crowd-cheerin.mp3", // "https://sample-videos.com/audio/mp3/crowd-cheering.mp3"
        "https://sample-videos.com/audio/mp3/wave.mp3"
    ]
    var player: AVPlayer? {
        didSet {
            guard let p = player else { return debugPrint("no player") }
            debugPrint("status: \(p.currentItem?.status == .unknown)") // this is set before status changes from unknown
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        nextTrack()
    }

    func nextTrack() {
        currentTrack += 1
        guard let url = URL(string: urls[currentTrack]) else { return }
        let item = AVPlayerItem(url: url)

        observerStatus = item.observe(\.status, changeHandler: { [weak self] (item, value) in
            switch item.status {
            case .unknown:
                debugPrint("status: unknown")
            case .readyToPlay:
                debugPrint("status: ready to play")
            case .failed:
                debugPrint("playback failed")
                self?.nextTrack()
            }
        })

        if player == nil {
            player = AVPlayer(playerItem: item)
        } else {
            player?.replaceCurrentItem(with: item)
        }
        player?.play()
    }
}

Upvotes: 6

Nikita Goncharov
Nikita Goncharov

Reputation: 173

Solution:

 private var player: AVPlayer? {
    didSet{
        if player?.currentItem?.status == .failed {
            if indexOfURL >= 0 {
                prepareForPronunciation(indexOfURL)
            }
        }
    }
}

If URLs are over, set indexOfURL equals -1, otherwise increment it to use next URL in the next function call

Upvotes: 0

Related Questions