327 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			327 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
//
 | 
						|
//  ContentPlayManager.swift
 | 
						|
//  SodaLive
 | 
						|
//
 | 
						|
//  Created by klaus on 2023/08/11.
 | 
						|
//
 | 
						|
 | 
						|
import Foundation
 | 
						|
import AVKit
 | 
						|
import MediaPlayer
 | 
						|
import ObjectBox
 | 
						|
 | 
						|
final class ContentPlayManager: NSObject, ObservableObject {
 | 
						|
    static let shared = ContentPlayManager()
 | 
						|
    
 | 
						|
    var creatorId = 0
 | 
						|
    @Published var contentId: Int = 0
 | 
						|
    
 | 
						|
    @Published private(set) var duration: TimeInterval = 0
 | 
						|
    
 | 
						|
    @Published var title = ""
 | 
						|
    @Published var nickname = ""
 | 
						|
    @Published var coverImage = ""
 | 
						|
    
 | 
						|
    @Published var isFree: Bool? = nil
 | 
						|
    @Published var isPreview: Bool? = nil
 | 
						|
    @Published private(set) var isShowingMiniPlayer = false
 | 
						|
    @Published private(set) var isPlaying = false
 | 
						|
    
 | 
						|
    @Published var isLoading = false
 | 
						|
    @Published var errorMessage = ""
 | 
						|
    @Published var isShowPopup = false
 | 
						|
    
 | 
						|
    var player: AVAudioPlayer!
 | 
						|
    
 | 
						|
    var startTimer: (() -> Void)?
 | 
						|
    var stopTimer: (() -> Void)?
 | 
						|
    
 | 
						|
    private var playbackTrackingId: Id = 0
 | 
						|
    private let repository = PlaybackTrackingRepository()
 | 
						|
}
 | 
						|
 | 
						|
extension ContentPlayManager {
 | 
						|
    func playAudio(
 | 
						|
        creatorId: Int = 0,
 | 
						|
        contentId: Int = 0,
 | 
						|
        title: String = "",
 | 
						|
        nickname: String = "",
 | 
						|
        coverImage: String = "",
 | 
						|
        contentUrl: String = "",
 | 
						|
        isFree: Bool? = nil,
 | 
						|
        isPreview: Bool? = nil
 | 
						|
    ) {
 | 
						|
        if contentId <= 0 {
 | 
						|
            return
 | 
						|
        }
 | 
						|
        
 | 
						|
        if let startTimer = startTimer {
 | 
						|
            startTimer()
 | 
						|
        }
 | 
						|
        
 | 
						|
        if self.contentId > 0 && self.contentId == contentId {
 | 
						|
            player?.play()
 | 
						|
            isPlaying = player.isPlaying
 | 
						|
        } else {
 | 
						|
            isLoading = true
 | 
						|
            stopAudio()
 | 
						|
            
 | 
						|
            self.creatorId = creatorId
 | 
						|
            self.contentId = contentId
 | 
						|
            self.title = title
 | 
						|
            self.nickname = nickname
 | 
						|
            self.coverImage = coverImage
 | 
						|
            self.isFree = isFree
 | 
						|
            self.isPreview = isPreview
 | 
						|
            
 | 
						|
            guard let url = URL(string: contentUrl) else {
 | 
						|
                showError()
 | 
						|
                return
 | 
						|
            }
 | 
						|
            
 | 
						|
            URLSession.shared.dataTask(with: url) { [unowned self] data, response, error in
 | 
						|
                guard let audioData = data else {
 | 
						|
                    self.isLoading = false
 | 
						|
                    return
 | 
						|
                }
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let audioSession = AVAudioSession.sharedInstance()
 | 
						|
                    try audioSession.setCategory(.playback, mode: .moviePlayback)
 | 
						|
                    try audioSession.setActive(true)
 | 
						|
                    
 | 
						|
                    self.player = try AVAudioPlayer(data: audioData)
 | 
						|
                    saveNewPlaybackTracking(totalDuration: Int(player.duration), progress: 0)
 | 
						|
                    
 | 
						|
                    DispatchQueue.main.async {
 | 
						|
                        self.player?.volume = 1
 | 
						|
                        self.player?.delegate = self
 | 
						|
                        self.player?.prepareToPlay()
 | 
						|
                        
 | 
						|
                        self.duration = self.player.duration
 | 
						|
                        self.player?.play()
 | 
						|
                        self.isPlaying = self.player.isPlaying
 | 
						|
                        self.isShowingMiniPlayer = true
 | 
						|
                        UIApplication.shared.beginReceivingRemoteControlEvents()
 | 
						|
                    }
 | 
						|
                    
 | 
						|
                    self.registerRemoteControlEvents()
 | 
						|
                    self.fetchAlbumArtAndUpdateNowPlayingInfo()
 | 
						|
                } catch {
 | 
						|
                    DispatchQueue.main.async {
 | 
						|
                        self.showError()
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                
 | 
						|
                DispatchQueue.main.async {
 | 
						|
                    self.isLoading = false
 | 
						|
                }
 | 
						|
            }.resume()
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func stopAudio() {
 | 
						|
        if let player = player {
 | 
						|
            player.stop()
 | 
						|
            setEndPositionPlaybackTracking(progress: Int(player.currentTime))
 | 
						|
            
 | 
						|
            player.currentTime = 0
 | 
						|
            isPlaying = player.isPlaying
 | 
						|
        }
 | 
						|
        
 | 
						|
        resetAudioData()
 | 
						|
        unRegisterRemoteControlEvents()
 | 
						|
    }
 | 
						|
    
 | 
						|
    func conditionalStopAudio(contentId: Int) {
 | 
						|
        if self.contentId == contentId {
 | 
						|
            stopAudio()
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func pauseAudio() {
 | 
						|
        if let player = player {
 | 
						|
            player.pause()
 | 
						|
            isPlaying = player.isPlaying
 | 
						|
            if let stopTimer = stopTimer {
 | 
						|
                stopTimer()
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func resetAudioData() {
 | 
						|
        title = ""
 | 
						|
        nickname = ""
 | 
						|
        coverImage = ""
 | 
						|
        contentId = 0
 | 
						|
        duration = 0
 | 
						|
        
 | 
						|
        isPreview = false
 | 
						|
        isShowingMiniPlayer = false
 | 
						|
        player = nil
 | 
						|
        startTimer = nil
 | 
						|
        stopTimer = nil
 | 
						|
    }
 | 
						|
    
 | 
						|
    func setCurrentTime(_ progress: TimeInterval) {
 | 
						|
        if let player = player, contentId > 0 {
 | 
						|
            player.currentTime = progress
 | 
						|
            saveNewPlaybackTracking(totalDuration: Int(player.duration), progress: Int(progress))
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func seekBackward10Seconds() {
 | 
						|
        guard let player = player else { return }
 | 
						|
        let newTime = max(player.currentTime - 10, 0)
 | 
						|
        player.currentTime = newTime
 | 
						|
    }
 | 
						|
    
 | 
						|
    func seekForward10Seconds() {
 | 
						|
        guard let player = player else { return }
 | 
						|
        let newTime = min(player.currentTime + 10, player.duration)
 | 
						|
        player.currentTime = newTime
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func repeatAudio() {
 | 
						|
        if let stopTimer = stopTimer {
 | 
						|
            stopTimer()
 | 
						|
        }
 | 
						|
        
 | 
						|
        player.stop()
 | 
						|
        setEndPositionPlaybackTracking(progress: Int(player.currentTime))
 | 
						|
        player.currentTime = 0
 | 
						|
        
 | 
						|
        saveNewPlaybackTracking(totalDuration: Int(player.duration), progress: 0)
 | 
						|
        player.play()
 | 
						|
        
 | 
						|
        if let startTimer = startTimer {
 | 
						|
            startTimer()
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func showError() {
 | 
						|
        self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
 | 
						|
        self.isShowPopup = true
 | 
						|
        self.resetAudioData()
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func fetchAlbumArtAndUpdateNowPlayingInfo() {
 | 
						|
        guard let url = URL(string: coverImage) else {
 | 
						|
            print("잘못된 이미지 URL")
 | 
						|
            registerNowPlayingInfoCenter(with: nil) // 앨범 아트 없이 업데이트
 | 
						|
            return
 | 
						|
        }
 | 
						|
        
 | 
						|
        // URLSession을 사용하여 비동기적으로 이미지 다운로드
 | 
						|
        URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
 | 
						|
            if let error = error {
 | 
						|
                print("앨범 아트 다운로드 실패: \(error.localizedDescription)")
 | 
						|
                DispatchQueue.main.async {
 | 
						|
                    self?.registerNowPlayingInfoCenter(with: nil) // 앨범 아트 없이 업데이트
 | 
						|
                }
 | 
						|
                return
 | 
						|
            }
 | 
						|
            
 | 
						|
            guard let data = data, let image = UIImage(data: data) else {
 | 
						|
                print("이미지 데이터를 가져오지 못했습니다.")
 | 
						|
                DispatchQueue.main.async {
 | 
						|
                    self?.registerNowPlayingInfoCenter(with: nil) // 앨범 아트 없이 업데이트
 | 
						|
                }
 | 
						|
                return
 | 
						|
            }
 | 
						|
            
 | 
						|
            // 성공적으로 다운로드된 이미지를 사용
 | 
						|
            DispatchQueue.main.async {
 | 
						|
                self?.registerNowPlayingInfoCenter(with: image)
 | 
						|
            }
 | 
						|
        }.resume()
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func registerNowPlayingInfoCenter(with albumArtImage: UIImage?) {
 | 
						|
        let center = MPNowPlayingInfoCenter.default()
 | 
						|
        var nowPlayingInfo = center.nowPlayingInfo ?? [String: Any]()
 | 
						|
        
 | 
						|
        nowPlayingInfo[MPMediaItemPropertyTitle] = title
 | 
						|
        nowPlayingInfo[MPMediaItemPropertyArtist] = nickname
 | 
						|
        if let artworkURL = URL(string: coverImage), let imageData = try? Data(contentsOf: artworkURL), let artworkImage = UIImage(data: imageData) {
 | 
						|
            let artwork = MPMediaItemArtwork(boundsSize: artworkImage.size) { size in
 | 
						|
                return artworkImage
 | 
						|
            }
 | 
						|
            nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
 | 
						|
        }
 | 
						|
        
 | 
						|
        // 콘텐츠 총 길이
 | 
						|
        nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = player.duration
 | 
						|
        // 콘텐츠 재생 시간에 따른 progressBar 초기화
 | 
						|
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate
 | 
						|
        // 콘텐츠 현재 재생시간
 | 
						|
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentTime
 | 
						|
        
 | 
						|
        center.nowPlayingInfo = nowPlayingInfo
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func registerRemoteControlEvents() {
 | 
						|
        let center = MPRemoteCommandCenter.shared()
 | 
						|
        
 | 
						|
        center.playCommand.addTarget { [unowned self] (commandEvent) -> MPRemoteCommandHandlerStatus in
 | 
						|
            if let player = player {
 | 
						|
                player.play()
 | 
						|
                self.isPlaying = player.isPlaying
 | 
						|
                if let startTimer = self.startTimer {
 | 
						|
                    startTimer()
 | 
						|
                }
 | 
						|
            }
 | 
						|
            
 | 
						|
            return .success
 | 
						|
        }
 | 
						|
        
 | 
						|
        center.pauseCommand.addTarget { [unowned self] (commandEvent) -> MPRemoteCommandHandlerStatus in
 | 
						|
            self.pauseAudio()
 | 
						|
            return .success
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func unRegisterRemoteControlEvents() {
 | 
						|
        let center = MPRemoteCommandCenter.shared()
 | 
						|
        center.playCommand.removeTarget(nil)
 | 
						|
        center.pauseCommand.removeTarget(nil)
 | 
						|
        UIApplication.shared.endReceivingRemoteControlEvents()
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
extension ContentPlayManager {
 | 
						|
    private func saveNewPlaybackTracking(totalDuration: Int, progress: Int) {
 | 
						|
        if creatorId != UserDefaults.int(forKey: .userId) {
 | 
						|
            playbackTrackingId = repository
 | 
						|
                .savePlaybackTracking(data: PlaybackTracking(
 | 
						|
                    audioContentId: contentId,
 | 
						|
                    totalDuration: totalDuration,
 | 
						|
                    startPosition: progress,
 | 
						|
                    isFree: isFree ?? true,
 | 
						|
                    isPreview: isPreview ?? true)
 | 
						|
                )
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func setEndPositionPlaybackTracking(progress: Int) {
 | 
						|
        if creatorId != UserDefaults.int(forKey: .userId) && playbackTrackingId > 0 {
 | 
						|
            if let playbackTracking = repository.getPlaybackTracking(id: playbackTrackingId) {
 | 
						|
                playbackTracking.endPosition = progress
 | 
						|
                _ = repository.savePlaybackTracking(data: playbackTracking)
 | 
						|
            }
 | 
						|
            
 | 
						|
            playbackTrackingId = 0
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
extension ContentPlayManager: AVAudioPlayerDelegate {
 | 
						|
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
 | 
						|
        if UserDefaults.bool(forKey: .isContentPlayLoop) {
 | 
						|
            repeatAudio()
 | 
						|
        } else {
 | 
						|
            stopAudio()
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |