Reputation: 173
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:
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
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
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
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