Search code examples
swiftuiavplayerinit

Postpone init() AVPlayer SwitUI


I have found the code for an OObject that serves me as a basic audio player (with a slider) Works fine , however i can use it so far in the ContentView like this :

@ObservedObject var player: AudioPlayerAV

let audioFileURL = URL(string: "https://file-examples-com.github.io/uploads/2017/11/file_example_MP3_5MG.mp3")


init(){
    
    var playerItem = AVPlayerItem(url: audioFileURL!)
    player = AudioPlayerAV(avPlayer: AVPlayer(playerItem: playerItem))
}

If i have the URL or the local path , i can use it . I don't know how to postpone the initialisation , if by example, i want to use it on a element that is recorded within the same ContentView.

//OObject

import AVFoundation
import Combine

let timeScale = CMTimeScale(1000)
let time = CMTime(seconds: 0.5, preferredTimescale: timeScale)

enum PlayerScrubState {
    case reset
    case scrubStarted
    case scrubEnded(TimeInterval)
}


final class AudioPlayerAV: NSObject, ObservableObject {

 
    @Published var displayTime: TimeInterval = 0
    @Published var observedTime: TimeInterval = 0
    @Published var itemDuration: TimeInterval = 0
    @Published var audioFinishedPlaying: Bool = false
    @Published var timeControlStatus: AVPlayer.TimeControlStatus = .paused
   
    
    fileprivate var itemDurationKVOPublisher: AnyCancellable!
    fileprivate var timeControlStatusKVOPublisher: AnyCancellable!
    fileprivate var avPlayer: AVPlayer
    fileprivate var periodicTimeObserver: Any?

    var scrubState: PlayerScrubState = .reset {
        didSet {
            switch scrubState {
            case .reset:
                return
            case .scrubStarted:
                return
            case .scrubEnded(let seekTime):
                avPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000))
            }
        }
    }

    init(avPlayer: AVPlayer) {
        self.avPlayer = avPlayer
        super.init()

        addPeriodicTimeObserver()
        addTimeControlStatusObserver()
        addItemDurationPublisher()
        
        NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil, queue: .main) { (_) in
            self.audioFinishedPlaying = true

        }
        
        
    }

    deinit {
        removePeriodicTimeObserver()
        timeControlStatusKVOPublisher.cancel()
        itemDurationKVOPublisher.cancel()
        
        NotificationCenter.default.removeObserver(self)
    }

    func play() {
        self.avPlayer.play()
    }

    func pause() {
        self.avPlayer.pause()
    }

    fileprivate func addPeriodicTimeObserver() {
        self.periodicTimeObserver = avPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] (time) in
            guard let self = self else { return }

            self.observedTime = time.seconds

            switch self.scrubState {
            case .reset:
                self.displayTime = time.seconds
            case .scrubStarted:
                break
            case .scrubEnded(let seekTime):
                self.scrubState = .reset
                self.displayTime = seekTime
            }
        }
    }

    fileprivate func removePeriodicTimeObserver() {
        guard let periodicTimeObserver = self.periodicTimeObserver else {
            return
        }
        avPlayer.removeTimeObserver(periodicTimeObserver)
        self.periodicTimeObserver = nil
    }

    fileprivate func addTimeControlStatusObserver() {
        timeControlStatusKVOPublisher = avPlayer
            .publisher(for: \.timeControlStatus)
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] (newStatus) in
                guard let self = self else { return }
                self.timeControlStatus = newStatus
                }
        )
    }

    fileprivate func addItemDurationPublisher() {
        itemDurationKVOPublisher = avPlayer
            .publisher(for: \.currentItem?.duration)
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] (newStatus) in
                guard let newStatus = newStatus,
                    let self = self else { return }
                self.itemDuration = newStatus.seconds
                }
        )
    }

}

Solution

  • In a non-SwiftUI situation, I'd normally recommend making player an optional and loading it later, but, as you've probably discovered, you can't make an @ObservedObject or @StateObject optional.

    I'd recommend refactoring AudioPlayerAV so that it does the important work in a different function than init. That way, you're free to load the content at whatever point you want.

    For example:

    
    struct ContentView: View {
        @StateObject var player: AudioPlayerAV = AudioPlayerAV()
        @State private var urlText = "https://file-examples-com.github.io/uploads/2017/11/file_example_MP3_5MG.mp3"
        
        var body: some View {
            TextField("", text: $urlText)
            Button(action: {
                if let url = URL(string: urlText) {
                    player.loadPlayer(avPlayer: AVPlayer(url: url))
                    player.play()
                } else {
                    print("not valid")
                }
            }) {
                Text("Load")
            }
        }
    }
    
    struct Response: Codable {
        // ... some fields here
    }
    
    let timeScale = CMTimeScale(1000)
    let time = CMTime(seconds: 0.5, preferredTimescale: timeScale)
    
    enum PlayerScrubState {
        case reset
        case scrubStarted
        case scrubEnded(TimeInterval)
    }
    
    
    final class AudioPlayerAV: NSObject, ObservableObject {
        @Published var displayTime: TimeInterval = 0
        @Published var observedTime: TimeInterval = 0
        @Published var itemDuration: TimeInterval = 0
        @Published var audioFinishedPlaying: Bool = false
        @Published var timeControlStatus: AVPlayer.TimeControlStatus = .paused
        
        
        fileprivate var itemDurationKVOPublisher: AnyCancellable?
        fileprivate var timeControlStatusKVOPublisher: AnyCancellable?
        fileprivate var avPlayer: AVPlayer?
        fileprivate var periodicTimeObserver: Any?
        
        var scrubState: PlayerScrubState = .reset {
            didSet {
                switch scrubState {
                case .reset:
                    return
                case .scrubStarted:
                    return
                case .scrubEnded(let seekTime):
                    avPlayer?.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000))
                }
            }
        }
        
        func loadPlayer(avPlayer: AVPlayer) {
            removeObservers()
            self.avPlayer = avPlayer
            
            addPeriodicTimeObserver()
            addTimeControlStatusObserver()
            addItemDurationPublisher()
            
            NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil, queue: .main) { (_) in
                self.audioFinishedPlaying = true
                
            }
        }
        
        func removeObservers() {
            removePeriodicTimeObserver()
            timeControlStatusKVOPublisher?.cancel()
            itemDurationKVOPublisher?.cancel()
            
            NotificationCenter.default.removeObserver(self)
        }
        
        deinit {
            removeObservers()
        }
        
        func play() {
            self.avPlayer?.play()
        }
        
        func pause() {
            self.avPlayer?.pause()
        }
        
        fileprivate func addPeriodicTimeObserver() {
            self.periodicTimeObserver = avPlayer?.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] (time) in
                guard let self = self else { return }
                
                self.observedTime = time.seconds
                
                switch self.scrubState {
                case .reset:
                    self.displayTime = time.seconds
                case .scrubStarted:
                    break
                case .scrubEnded(let seekTime):
                    self.scrubState = .reset
                    self.displayTime = seekTime
                }
            }
        }
        
        fileprivate func removePeriodicTimeObserver() {
            guard let periodicTimeObserver = self.periodicTimeObserver else {
                return
            }
            avPlayer?.removeTimeObserver(periodicTimeObserver)
            self.periodicTimeObserver = nil
        }
        
        fileprivate func addTimeControlStatusObserver() {
            guard let avPlayer = avPlayer else {
                return
            }
            timeControlStatusKVOPublisher = avPlayer
                .publisher(for: \.timeControlStatus)
                .receive(on: DispatchQueue.main)
                .sink(receiveValue: { [weak self] (newStatus) in
                    guard let self = self else { return }
                    self.timeControlStatus = newStatus
                }
                )
        }
        
        fileprivate func addItemDurationPublisher() {
            guard let avPlayer = avPlayer else {
                return
            }
            itemDurationKVOPublisher = avPlayer
                .publisher(for: \.currentItem?.duration)
                .receive(on: DispatchQueue.main)
                .sink(receiveValue: { [weak self] (newStatus) in
                    guard let newStatus = newStatus,
                          let self = self else { return }
                    self.itemDuration = newStatus.seconds
                }
                )
        }
        
    }