201 lines
7.1 KiB
Swift
201 lines
7.1 KiB
Swift
//
|
|
// 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<Float>) {
|
|
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
|
|
}
|
|
}
|