Files
Woka_Native_iOS/WOKA/Karaoke/ViewModel/AVPlayerVM.swift
2024-07-11 20:31:25 +05:30

248 lines
9.6 KiB
Swift

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