// // VideoOverlayViewController.swift // Tusker // // Created by Shadowfacts on 3/26/24. // Copyright © 2024 Shadowfacts. All rights reserved. // import UIKit import AVFoundation class VideoOverlayViewController: UIViewController { private static let playImage = UIImage(systemName: "play.fill")! private static let pauseImage = UIImage(systemName: "pause.fill")! private let player: AVPlayer @Box private var playbackSpeed: Float private var dimmingView: UIView! private var controlsStack: UIStackView! private var skipBackButton: VideoOverlayButton! private var skipForwardButton: VideoOverlayButton! private var rateObservation: NSKeyValueObservation? init(player: AVPlayer, playbackSpeed: Box) { self.player = player self._playbackSpeed = playbackSpeed super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() dimmingView = UIView() dimmingView.backgroundColor = .black dimmingView.alpha = 0.2 dimmingView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(dimmingView) NSLayoutConstraint.activate([ dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor), dimmingView.topAnchor.constraint(equalTo: view.topAnchor), dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) skipBackButton = VideoOverlayButton(image: UIImage(systemName: "gobackward.10")!) skipBackButton.addTarget(self, action: #selector(skipBackPressed), for: .touchUpInside) let playPauseButton = VideoOverlayButton(image: VideoOverlayViewController.pauseImage) playPauseButton.addTarget(self, action: #selector(playPausePressed), for: .touchUpInside) skipForwardButton = VideoOverlayButton(image: UIImage(systemName: "goforward.10")!) skipForwardButton.addTarget(self, action: #selector(skipForwardPressed), for: .touchUpInside) controlsStack = UIStackView(arrangedSubviews: [ skipBackButton, playPauseButton, skipForwardButton, ]) controlsStack.axis = .horizontal controlsStack.alignment = .center controlsStack.spacing = 24 controlsStack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(controlsStack) NSLayoutConstraint.activate([ controlsStack.centerXAnchor.constraint(equalTo: view.centerXAnchor), controlsStack.centerYAnchor.constraint(equalTo: view.centerYAnchor), skipBackButton.widthAnchor.constraint(equalToConstant: 50), skipBackButton.heightAnchor.constraint(equalToConstant: 50), playPauseButton.widthAnchor.constraint(equalToConstant: 66), playPauseButton.heightAnchor.constraint(equalToConstant: 66), skipForwardButton.widthAnchor.constraint(equalToConstant: 50), skipForwardButton.heightAnchor.constraint(equalToConstant: 50), ]) rateObservation = player.observe(\.rate, changeHandler: { player, _ in MainActor.runUnsafely { playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage } }) } func setVisible(_ visible: Bool) { loadViewIfNeeded() view.alpha = visible ? 1 : 0 } @objc private func playPausePressed() { if player.rate > 0 { player.rate = 0 } else { player.rate = playbackSpeed } } @objc private func skipBackPressed() { player.seek(to: player.currentTime() - CMTime(value: 10, timescale: 1)) } @objc private func skipForwardPressed() { player.seek(to: player.currentTime() + CMTime(value: 10, timescale: 1)) } } private class VideoOverlayButton: UIControl { var image: UIImage? { get { imageView.image } set { imageView.image = newValue } } private let backgroundView = UIView() private let imageView = UIImageView() private var animator: UIViewPropertyAnimator? override var isEnabled: Bool { didSet { imageView.tintColor = isEnabled ? .white : .lightGray } } init(image: UIImage) { super.init(frame: .zero) backgroundView.alpha = 0 backgroundView.backgroundColor = .lightGray.withAlphaComponent(0.5) backgroundView.translatesAutoresizingMaskIntoConstraints = false addSubview(backgroundView) NSLayoutConstraint.activate([ backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor), backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor), backgroundView.topAnchor.constraint(equalTo: topAnchor), backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) imageView.image = image imageView.tintColor = .white imageView.contentMode = .scaleAspectFit imageView.preferredSymbolConfiguration = .init(scale: .large) imageView.translatesAutoresizingMaskIntoConstraints = false addSubview(imageView) NSLayoutConstraint.activate([ imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), imageView.topAnchor.constraint(equalTo: topAnchor, constant: 8), imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8), ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() backgroundView.layer.cornerRadius = bounds.height / 2 } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { bounds.contains(point) ? self : nil } override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { UIView.animate(withDuration: 0.2) { self.backgroundView.alpha = 1 self.backgroundView.transform = CGAffineTransform(scaleX: 1/0.8, y: 1/0.8) self.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) } return super.beginTracking(touch, with: event) } override func endTracking(_ touch: UITouch?, with event: UIEvent?) { UIView.animate(withDuration: 0.2) { self.backgroundView.alpha = 0 self.backgroundView.transform = .identity self.transform = .identity } } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer is UITapGestureRecognizer { return false } return true } }