// // 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 } } }