248 lines
9.6 KiB
Swift
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)"
|
|
}
|
|
}
|