Newer
Older
bremer-ios-app / BremerApp / ViewModel / AudioPlayerViewModel.swift
//
//  ContentView.swift
//  Bremer
//
//  Created by yhornisse on 2023/05/06.
//

import MediaPlayer
import AVFoundation
import Combine

public class AudioPlayerViewModel : NSObject, AVAudioPlayerDelegate, ObservableObject {
    
    var musicPlayer : AVAudioPlayer? = nil
    var audioQueue : [Audio] = []
    private var audioFileCache : Dictionary<String, URL> = [:] // slug -> URL
    @Published
    var playingMusic : Audio? = nil
    @Published
    var isPlaying = false
    private var maxSize = 4
    @Published
    private var audioList : [Audio] = []
    @Published
    private var errorText : String = ""
    @Published
    private var usedBy = ""
    @Published
    private var hasError = false
    @Published
    private var messageText = ""
    @Published
    private var bremerApiBaseUrl = ""
    private var disposables = [AnyCancellable]()
    
    func onInterrupted(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
              let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
              let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
            return
        }
        switch type {
        case .began:
            if isPlaying {
                pauseAudio()
            }
            break
        case .ended:
            if !isPlaying {
                playAudio()
            }
            break
        @unknown default:
            break
        }
    }
    
    func onChangeAudioSessionRoute(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
              let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
              let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
            return
        }
        switch reason {
        case .newDeviceAvailable:
            if !isPlaying {
                playAudio()
            }
        case .oldDeviceUnavailable:
            if isPlaying {
                pauseAudio()
            }
        default:
            break
        }
    }
    
    func setupAudioPlayer() {
        // MPRemoteCommandCenter
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.playCommand.isEnabled = true
        commandCenter.playCommand.addTarget { [unowned self] event in
            self.isPlaying = true
            musicPlayer?.play()
            return .success
        }
        commandCenter.pauseCommand.isEnabled = true
        commandCenter.pauseCommand.addTarget { [unowned self] event in
            self.isPlaying = false
            musicPlayer?.stop()
            return .success
        }
        // MPNowPlayingInfoCenter
        var nowPlayingInfo: [String: Any] = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
        if let audio = self.playingMusic {
            nowPlayingInfo[MPMediaItemPropertyTitle] = "\(audio.name) - \(audio.album ?? "")"
            nowPlayingInfo[MPMediaItemPropertyArtist] = audio.artist ?? ""
        }
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = "1"
        if let duration = self.musicPlayer?.duration {
            nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
        }
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
        // etc
        do {
            try AVAudioSession.sharedInstance()
                .setCategory(.playAndRecord, options: [.defaultToSpeaker,
                                                       .allowAirPlay,
                                                       .allowBluetoothA2DP])
            try AVAudioSession.sharedInstance().setActive(true)
        } catch {
            // TODO 後から直す
            print(error.localizedDescription)
        }
        // on Stop a music
        self.musicPlayer?.delegate = self
    }
    
    func downloadAudio(audio:Audio, onDownload:(() -> Void)?) {
        if let location = self.audioFileCache[audio.slug]
            , FileManager.default.fileExists(atPath: location.path) {
            if let onDownload = onDownload {
                onDownload()
                return
            }
        }
        let cookie = UserDefaults.standard.string(forKey: "JSESSIONID")
        if cookie == nil {
            errorText = "Connection Error!"
            return
        }
        var request:URLRequest = URLRequest(url: URL(string: "https://pied-piper.net/bremer/audio/\(audio.slug)")!)
        request.setValue("JSESSIONID=\(cookie!)", forHTTPHeaderField: "Cookie")
        URLSession.shared.downloadTask(with: request) { (location, response, error) in
            if error != nil {
                self.errorText = "Download Error!"
                return
            }
            
            do {
                if let location = location {
                    if self.audioFileCache.count >= self.maxSize {
                        if let lrpAudio = self.audioQueue.first {
                            self.audioQueue.removeFirst()
                            if let lrpFileLocation = self.audioFileCache[lrpAudio.slug] {
                                self.audioFileCache.removeValue(forKey: lrpAudio.slug)
                                do {
                                    if FileManager.default.fileExists(atPath: lrpFileLocation.path) {
                                        try FileManager.default.removeItem(atPath: lrpFileLocation.path)
                                    }
                                } catch {
                                    // TODO print 以外にする
                                    print(error.localizedDescription)
                                }
                            }
                        }
                    }
                    if !self.audioQueue.contains(where: { $0.slug == audio.slug }) {
                        self.audioQueue.append(audio)
                    }
                    let file = "\(NSTemporaryDirectory())/\(UUID().uuidString).tmp"
                    try FileManager.default.copyItem(
                        atPath: location.path,
                        toPath: file
                    )
                    self.audioFileCache[audio.slug] = URL(string: file)!
                    if let onDownload = onDownload {
                        onDownload()
                    }
                }
            } catch {
                print(error.localizedDescription)
            }
        }
        .resume()
    }
    
    func play(audio:Audio) -> Bool {
        if !self.audioFileCache.keys.contains(audio.slug) {
            return false
        }
        
        if self.musicPlayer?.isPlaying == true {
            self.isPlaying = false
            self.musicPlayer?.stop()
        }
        do {
            setupAudioPlayer()
            if let audioFile = self.audioFileCache[audio.slug] {
                if FileManager.default.fileExists(atPath: audioFile.path) {
                    if self.musicPlayer?.isPlaying == true {
                        self.isPlaying = false
                        self.musicPlayer?.stop()
                    }
                    if let queueIdx = self.audioQueue.firstIndex(where: { $0.slug == audio.slug }) {
                        self.audioQueue.remove(at: queueIdx)
                        self.audioQueue.append(audio)
                    }
                    self.playingMusic = audio
                    self.musicPlayer = try AVAudioPlayer(contentsOf: audioFile)
                    self.setupAudioPlayer()
                    self.musicPlayer?.play()
                    self.isPlaying = true
                    sendApi(url: URL(string: "\(bremerApiBaseUrl)/audio/history/\(audio.slug)"
                        .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)!,
                            httpMethod: "POST")
                    .sink { completion in
                        switch completion {
                        case .finished:
                            break
                        case .failure(let error):
                            switch (error) {
                            case is BremerConnectionError:
                                self.hasError = true
                                self.messageText = "Connection error"
                                break
                            default:
                                self.hasError = true
                                self.messageText = error.localizedDescription
                            }
                        }
                    } receiveValue: { (response : Any) in
                        self.hasError = false
                        self.messageText = ""
                    }
                    .store(in: &disposables)
                    return true;
                }
            }
        } catch {
            // TODO 後から直す
            print(error.localizedDescription)
        }
        return false
    }
    
    
    func playAudio() {
        self.isPlaying = true
        self.musicPlayer?.play()
    }
    
    func pauseAudio() {
        self.isPlaying = false
        self.musicPlayer?.pause()
    }
    
    func setAudioSeq(seq : Double) {
        self.musicPlayer?.currentTime = seq
    }
    
    func musicTime() -> String {
        let a = Int(musicPlayer?.currentTime ?? 0.0)
        let b = Int(musicPlayer?.duration ?? 0.0)
        return "\(String(format: "%02d", a / 60)):\(String(format: "%02d", a % 60)) / \(String(format: "%02d", b / 60)):\(String(format: "%02d", b % 60))"
    }
    
    func musicTimeRate() -> Double {
        let a = musicPlayer?.currentTime ?? 0.0
        let b = musicPlayer?.duration ?? 0.0
        return a / b
    }
    
    func playAudio(idx: Int) {
        if idx >= audioList.count  {
            return
        }
        let audio = audioList[idx]
        if self.play(audio: audio) {
            return;
        }
        
        downloadAudio(audio: audio, onDownload: {
            self.play(audio: audio)
        })
    }
    
    func playAudioList(audioList: [Audio], audio: Audio, usedBy: String) {
        self.usedBy = usedBy
        self.audioList = audioList
        if let idx = self.audioList.firstIndex(where: { $0.slug == audio.slug }) {
            self.playAudio(idx: idx)
            self.prefetch(idx: idx)
        }
    }
    
    func playAudio(audio: Audio) {
        if let idx = self.audioList.firstIndex(where: { $0.slug == audio.slug }) {
            self.playAudio(idx: idx)
            self.prefetch(idx: idx)
        }
    }
    
    func reorderAudioList(from: IndexSet, to: Int, usedBy: String) {
        if self.usedBy != usedBy {
            return
        }
        if to < self.audioList.count {
            self.audioList.move(fromOffsets: from, toOffset: to)
        } else {
            self.audioList.move(fromOffsets: from, toOffset: self.audioList.count - 1)
        }
    }
    
    func addAudio(audio: Audio, usedBy: String) {
        if self.usedBy != usedBy {
            return
        }
        if self.audioList.contains(where: { $0.slug == audio.slug }) == true {
            return
        }
        self.audioList.append(audio)
    }
    
    public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        self.isPlaying = false
        if let slug = self.playingMusic?.slug {
            if let idx = self.audioList.firstIndex(where: { $0.slug == slug }) {
                if idx + 1 < self.audioList.count {
                    playAudio(idx: idx + 1)
                    prefetch(idx: idx + 1)
                }
            }
        }
    }
    
    func prefetch(idx: Int) {
        if idx + 1 < self.audioList.count {
            self.downloadAudio(audio: self.audioList[idx + 1], onDownload: nil)
        }
    }
    
    func exists(_ slug: String) -> Bool {
        if let location = self.audioFileCache[slug],
           FileManager.default.fileExists(atPath: location.path) {
            return true
        } else {
            return false
        }
    }
}