// // AVPlayerVM.swift // WOKA // // Created by Bilal on 05/07/2024. // import UIKit import AVKit class AVPlayerVM{ weak var vc : AVPlayerVC! var player : AVPlayer? = nil var isThumbSeek : Bool = false var timer : Timer? var isFinished = false{ didSet{ if isFinished{ timer?.invalidate() timer = nil vc.reloadBtn.isHidden = false vc.stackCtrView.isHidden = true vc.tintView.isHidden = false vc.sliderStack.isHidden = true }else{ vc.reloadBtn.isHidden = true vc.stackCtrView.isHidden = false vc.sliderStack.isHidden = false } } } private var playerLayer : AVPlayerLayer? = nil private var timeObserver : Any? = nil func initView(){ vc.seekSlider.transform = CGAffineTransform(scaleX: 0.85, y: 0.85) self.vc.videoTitle.text = vc.titleVideo setupKaraoke() } @objc func videoDidFinish() { self.isFinished = true } func setupKaraoke(){ guard let url = vc.videoURL else{return} hideShowKaraoke(isLoading: true) let avURL = URL(string: url)! let asset = AVAsset(url: avURL) let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("\(vc.titleVideo?.trimmingCharacters(in: .whitespaces) ?? "extractedAudio").m4a") FileManager.default.clearTmpDirectory() asset.writeAudioTrackToURL(outputURL) { [weak self] isDone, error,url in guard let self else{return} print(isDone, error , url) if error == nil{ hideShowKaraoke(isLoading: false) } } } func hideShowKaraoke(isLoading : Bool){ DispatchQueue.main.async { [weak self] in guard let self else{return} if isLoading{ vc.karaokeLoading.startAnimating() vc.karaokeStack.isHidden = false vc.playPauseRecordStack.isHidden = true }else{ vc.karaokeLoading.stopAnimating() vc.karaokeLoading.hidesWhenStopped = true vc.karaokeStack.isHidden = true vc.playPauseRecordStack.isHidden = false } } } // MARK: - Setup Video Player func setVideoPlayer() { guard let videoURL = vc.videoURL, let url = URL(string: videoURL) else { return } if self.player == nil { // Create a subview for the video player let playerView = UIView(frame: vc.viewControll.bounds) playerView.backgroundColor = .clear // Make sure the background is clear vc.viewControll.addSubview(playerView) vc.viewControll.sendSubviewToBack(playerView) // Ensure the player view is below other UI elements // Initialize the player let playerItem = AVPlayerItem(url: url) player = AVPlayer(playerItem: playerItem) // Add observer for AVPlayerItemFailedToPlayToEndTimeNotification NotificationCenter.default.addObserver(self, selector: #selector(playerItemFailedToPlayToEndTime(_:)), name: .AVPlayerItemFailedToPlayToEndTime, object: playerItem) // Add observer for AVPlayerItem's status playerItem.addObserver(self.vc, forKeyPath: "status", options: [.new, .initial], context: nil) // Set up the player layer playerLayer = AVPlayerLayer(player: player) playerLayer?.videoGravity = .resizeAspectFill playerLayer?.frame = playerView.bounds if let playerLayer = playerLayer { playerView.layer.addSublayer(playerLayer) } // Add observer for play/pause player?.addObserver(self.vc, forKeyPath: "rate", options: [.new, .initial], context: nil) // Add observer for buffering playerItem.addObserver(self.vc, forKeyPath: "isPlaybackBufferEmpty", options: [.new, .initial], context: nil) playerItem.addObserver(self.vc, forKeyPath: "isPlaybackLikelyToKeepUp", options: [.new, .initial], context: nil) // Add observer for video finished playing NotificationCenter.default.addObserver(self, selector: #selector(videoDidFinish), name: .AVPlayerItemDidPlayToEndTime, object: playerItem) self.setObserverToPlayer() startTimer() player?.play() } } func reloadVideo() { guard let videoURL = vc.videoURL, let url = URL(string: videoURL) else { return } // Remove existing observers NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem) player?.currentItem?.removeObserver(vc, forKeyPath: "status") player?.currentItem?.removeObserver(vc, forKeyPath: "isPlaybackBufferEmpty") player?.currentItem?.removeObserver(vc, forKeyPath: "isPlaybackLikelyToKeepUp") // Create a new player item let playerItem = AVPlayerItem(url: url) player?.replaceCurrentItem(with: playerItem) // Add observers again NotificationCenter.default.addObserver(self, selector: #selector(videoDidFinish), name: .AVPlayerItemDidPlayToEndTime, object: playerItem) playerItem.addObserver(vc, forKeyPath: "status", options: [.new, .initial], context: nil) playerItem.addObserver(vc, forKeyPath: "isPlaybackBufferEmpty", options: [.new, .initial], context: nil) playerItem.addObserver(vc, forKeyPath: "isPlaybackLikelyToKeepUp", options: [.new, .initial], context: nil) player?.play() } func setObserverToPlayer() { let interval = CMTime(seconds: 0.3, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { elapsed in self.updatePlayerTime() }) } // Handle notification @objc func playerItemFailedToPlayToEndTime(_ notification: Notification) { if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error { print("Error: \(error.localizedDescription)") handlePlayerError(error) } } func handlePlayerError(_ error: Error) { // Update the UI or show an alert to the user Utilities.alertWithBtnCompletion(title: "Error", msgBody: error.localizedDescription, okBtnStr: "Retry?", vc: vc.self) { [weak self] isDone in guard let self else{return} player?.play() } } // MARK: - ShowHideControls func startTimer(){ timer = Timer.scheduledTimer(withTimeInterval: 4.5, repeats: false) { _ in self.showHideControls() } } func showHideControls(){ if isFinished{ vc.stackCtrView.isHidden = true vc.reloadBtn.isHidden.toggle() vc.sliderStack.isHidden = true }else{ vc.reloadBtn.isHidden = true vc.stackCtrView.isHidden.toggle() vc.sliderStack.isHidden.toggle() } vc.tintView.isHidden.toggle() if !vc.stackCtrView.isHidden{ startTimer() } } // MARK: - Update the player time Label with Slider private func updatePlayerTime() { guard let currentTime = self.player?.currentTime() else { return } guard let duration = self.player?.currentItem?.duration else { return } let currentTimeInSecond = CMTimeGetSeconds(currentTime) let durationTimeInSecond = CMTimeGetSeconds(duration) if self.isThumbSeek == false { self.vc.seekSlider.value = Float(currentTimeInSecond/durationTimeInSecond) } let value = Float64(self.vc.seekSlider.value) * CMTimeGetSeconds(duration) var hours = value / 3600 var mins = (value / 60).truncatingRemainder(dividingBy: 60) var secs = value.truncatingRemainder(dividingBy: 60) var timeformatter = NumberFormatter() timeformatter.minimumIntegerDigits = 2 timeformatter.minimumFractionDigits = 0 timeformatter.roundingMode = .down guard let hoursStr = timeformatter.string(from: NSNumber(value: hours)), let minsStr = timeformatter.string(from: NSNumber(value: mins)), let secsStr = timeformatter.string(from: NSNumber(value: secs)) else { return } self.vc.lbCurrentTime.text = "\(hoursStr):\(minsStr):\(secsStr)" hours = durationTimeInSecond / 3600 mins = (durationTimeInSecond / 60).truncatingRemainder(dividingBy: 60) secs = durationTimeInSecond.truncatingRemainder(dividingBy: 60) timeformatter = NumberFormatter() timeformatter.minimumIntegerDigits = 2 timeformatter.minimumFractionDigits = 0 timeformatter.roundingMode = .down guard let hoursStr = timeformatter.string(from: NSNumber(value: hours)), let minsStr = timeformatter.string(from: NSNumber(value: mins)), let secsStr = timeformatter.string(from: NSNumber(value: secs)) else { return } self.vc.lbTotalTime.text = "\(hoursStr):\(minsStr):\(secsStr)" } }