324 lines
12 KiB
Swift
324 lines
12 KiB
Swift
//
|
|
// AVPlayerVC.swift
|
|
// WOKA
|
|
//
|
|
// Created by Bilal on 05/07/2024.
|
|
//
|
|
|
|
import UIKit
|
|
import AVFoundation
|
|
|
|
class AVPlayerVC: UIViewController {
|
|
|
|
@IBOutlet weak var videoPlayer: UIView!
|
|
// @IBOutlet weak var videoPlayerHeight: NSLayoutConstraint!
|
|
@IBOutlet weak var viewControll: UIView!
|
|
@IBOutlet weak var stackCtrView: UIStackView!
|
|
|
|
@IBOutlet weak var sliderStack: UIStackView!
|
|
@IBOutlet weak var tintView: UIView!
|
|
@IBOutlet weak var videoTitle: UILabel!
|
|
|
|
@IBOutlet weak var img10SecBack: UIImageView! {
|
|
didSet {
|
|
self.img10SecBack.isUserInteractionEnabled = true
|
|
self.img10SecBack.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap10SecBack)))
|
|
}
|
|
}
|
|
@IBOutlet weak var imgPlay: UIImageView! {
|
|
didSet {
|
|
self.imgPlay.isUserInteractionEnabled = true
|
|
self.imgPlay.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTapPlayPause)))
|
|
}
|
|
}
|
|
@IBOutlet weak var img10SecFor: UIImageView! {
|
|
didSet {
|
|
self.img10SecFor.isUserInteractionEnabled = true
|
|
self.img10SecFor.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap10SecNext)))
|
|
}
|
|
}
|
|
|
|
@IBOutlet weak var lbCurrentTime: UILabel!
|
|
@IBOutlet weak var lbTotalTime: UILabel!
|
|
|
|
|
|
@IBOutlet weak var seekSlider: UISlider! {
|
|
didSet {
|
|
self.seekSlider.addTarget(self, action: #selector(onTapToSlide), for: .valueChanged)
|
|
}
|
|
}
|
|
|
|
|
|
var videoURL : String?
|
|
var timer : Timer?
|
|
var titleVideo : String?
|
|
var vm = AVPlayerVM()
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
vm.vc = self
|
|
vm.initView()
|
|
self.videoTitle.text = titleVideo
|
|
startTimer()
|
|
self.setObserverToPlayer()
|
|
|
|
viewControll.addTapGesture {
|
|
self.timer?.invalidate()
|
|
self.showHideControls()
|
|
}
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
self.setVideoPlayer()
|
|
}
|
|
|
|
@IBAction func closeBtnTapped(_ sender: UIButton) {
|
|
self.dismiss(animated: true)
|
|
}
|
|
|
|
func startTimer(){
|
|
timer = Timer.scheduledTimer(withTimeInterval: 3.5, repeats: false) { _ in
|
|
self.showHideControls()
|
|
}
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
self.player?.pause()
|
|
self.player = nil
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
player?.currentItem?.removeObserver(self, forKeyPath: "status")
|
|
}
|
|
|
|
// MARK: - ShowHideControls
|
|
|
|
func showHideControls(){
|
|
stackCtrView.isHidden.toggle()
|
|
sliderStack.isHidden.toggle()
|
|
tintView.isHidden.toggle()
|
|
|
|
if !stackCtrView.isHidden{
|
|
startTimer()
|
|
}
|
|
}
|
|
|
|
|
|
private var player : AVPlayer? = nil
|
|
private var playerLayer : AVPlayerLayer? = nil
|
|
|
|
private func setVideoPlayer() {
|
|
guard let videoURL, let url = URL(string: videoURL) else { return }
|
|
|
|
if self.player == nil {
|
|
// 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,
|
|
forKeyPath: "status",
|
|
options: [.new, .initial],
|
|
context: nil)
|
|
|
|
// Set up the player layer
|
|
playerLayer = AVPlayerLayer(player: player)
|
|
playerLayer?.videoGravity = .resizeAspectFill
|
|
playerLayer?.frame = videoPlayer.bounds
|
|
|
|
if let playerLayer = playerLayer {
|
|
videoPlayer.layer.addSublayer(playerLayer)
|
|
}
|
|
|
|
|
|
// self.player = AVPlayer(url: url)
|
|
// self.playerLayer = AVPlayerLayer(player: self.player)
|
|
// self.playerLayer?.videoGravity = .resizeAspectFill
|
|
// self.playerLayer?.frame = self.videoPlayer.bounds
|
|
// self.playerLayer?.addSublayer(self.viewControll.layer)
|
|
// if let playerLayer = self.playerLayer {
|
|
// self.videoPlayer.layer.addSublayer(playerLayer)
|
|
// }
|
|
// self.player?.play()
|
|
// self.imgPlay.image = UIImage(systemName: "pause.circle")
|
|
}
|
|
}
|
|
|
|
private var timeObserver : Any? = nil
|
|
|
|
private 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)
|
|
}
|
|
}
|
|
|
|
// Observe for player item status changes
|
|
override func observeValue(forKeyPath keyPath: String?,
|
|
of object: Any?,
|
|
change: [NSKeyValueChangeKey : Any]?,
|
|
context: UnsafeMutableRawPointer?) {
|
|
if keyPath == "status" {
|
|
if let playerItem = object as? AVPlayerItem {
|
|
switch playerItem.status {
|
|
case .readyToPlay:
|
|
// Ready to play
|
|
print("Player item is ready to play.")
|
|
player?.play()
|
|
imgPlay.image = UIImage(systemName: "pause.circle")
|
|
case .failed:
|
|
// Failed
|
|
if let error = playerItem.error {
|
|
print("Player item error: \(error.localizedDescription)")
|
|
handlePlayerError(error)
|
|
}
|
|
case .unknown:
|
|
// Unknown status
|
|
print("Player item status is unknown.")
|
|
@unknown default:
|
|
// Handle unexpected cases
|
|
print("Player item status is unknown.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func handlePlayerError(_ error: Error) {
|
|
// Update the UI or show an alert to the user
|
|
Utilities.alertWithBtnCompletion(title: "Error", msgBody: error.localizedDescription, okBtnStr: "Retry?", vc: self) { [weak self] isDone in
|
|
guard let self else{return}
|
|
player?.play()
|
|
}
|
|
}
|
|
|
|
|
|
|
|
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.seekSlider.value = Float(currentTimeInSecond/durationTimeInSecond)
|
|
}
|
|
|
|
let value = Float64(self.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.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.lbTotalTime.text = "\(hoursStr):\(minsStr):\(secsStr)"
|
|
}
|
|
|
|
|
|
@objc private func onTap10SecNext() {
|
|
guard let currentTime = self.player?.currentTime() else { return }
|
|
let seekTime10Sec = CMTimeGetSeconds(currentTime).advanced(by: 10)
|
|
let seekTime = CMTime(value: CMTimeValue(seekTime10Sec), timescale: 1)
|
|
self.player?.seek(to: seekTime, completionHandler: { completed in
|
|
|
|
})
|
|
}
|
|
|
|
@objc private func onTap10SecBack() {
|
|
guard let currentTime = self.player?.currentTime() else { return }
|
|
let seekTime10Sec = CMTimeGetSeconds(currentTime).advanced(by: -10)
|
|
let seekTime = CMTime(value: CMTimeValue(seekTime10Sec), timescale: 1)
|
|
self.player?.seek(to: seekTime, completionHandler: { completed in
|
|
|
|
})
|
|
}
|
|
|
|
@objc private func onTapPlayPause() {
|
|
if self.player?.timeControlStatus == .playing {
|
|
self.imgPlay.image = UIImage(systemName: "play.circle")
|
|
self.player?.pause()
|
|
} else {
|
|
self.imgPlay.image = UIImage(systemName: "pause.circle")
|
|
self.player?.play()
|
|
}
|
|
}
|
|
|
|
private var isThumbSeek : Bool = false
|
|
@objc private func onTapToSlide() {
|
|
if timer != nil{
|
|
timer?.invalidate()
|
|
timer = nil
|
|
}
|
|
self.isThumbSeek = true
|
|
guard let duration = self.player?.currentItem?.duration else { return }
|
|
let value = Float64(self.seekSlider.value) * CMTimeGetSeconds(duration)
|
|
if value.isNaN == false {
|
|
let seekTime = CMTime(value: CMTimeValue(value), timescale: 1)
|
|
self.player?.seek(to: seekTime, completionHandler: { completed in
|
|
if completed {
|
|
self.isThumbSeek = false
|
|
self.startTimer()
|
|
// print("Completed")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
@objc private func onTapToggleScreen() {
|
|
if #available(iOS 16.0, *) {
|
|
guard let windowSceen = self.view.window?.windowScene else { return }
|
|
if windowSceen.interfaceOrientation == .portrait {
|
|
windowSceen.requestGeometryUpdate(.iOS(interfaceOrientations: .landscape)) { error in
|
|
print(error.localizedDescription)
|
|
}
|
|
} else {
|
|
windowSceen.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait)) { error in
|
|
print(error.localizedDescription)
|
|
}
|
|
}
|
|
} else {
|
|
if UIDevice.current.orientation == .portrait {
|
|
let orientation = UIInterfaceOrientation.landscapeRight.rawValue
|
|
UIDevice.current.setValue(orientation, forKey: "orientation")
|
|
} else {
|
|
let orientation = UIInterfaceOrientation.portrait.rawValue
|
|
UIDevice.current.setValue(orientation, forKey: "orientation")
|
|
}
|
|
}
|
|
}
|
|
}
|