Tusker/Tusker/Screens/Gallery/VideoGalleryContentViewCont...

232 lines
7.9 KiB
Swift

//
// VideoGalleryContentViewController.swift
// Tusker
//
// Created by Shadowfacts on 3/19/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
import GalleryVC
import AVFoundation
import CoreImage
class VideoGalleryContentViewController: UIViewController, GalleryContentViewController {
private let url: URL
let caption: String?
private var item: AVPlayerItem
let player: AVPlayer
#if !os(visionOS)
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
@Box private var playbackSpeed: Float = 1
#endif
private var isGrayscale: Bool
private var presentationSizeObservation: NSKeyValueObservation?
private var statusObservation: NSKeyValueObservation?
private var rateObservation: NSKeyValueObservation?
private var isFirstAppearance = true
private var hideControlsWorkItem: DispatchWorkItem?
init(url: URL, caption: String?) {
self.url = url
self.caption = caption
self.isGrayscale = Preferences.shared.grayscaleImages
let asset = AVAsset(url: url)
self.item = VideoGalleryContentViewController.createItem(asset: asset)
self.player = AVPlayer(playerItem: item)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private static func createItem(asset: AVAsset) -> AVPlayerItem {
let item = AVPlayerItem(asset: asset)
if Preferences.shared.grayscaleImages {
#if os(visionOS)
#warning("Use async AVVideoComposition CIFilter initializer")
#else
let filter = CIFilter(name: "CIColorMonochrome")!
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
filter.setValue(1.0, forKey: "inputIntensity")
item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in
filter.setValue(request.sourceImage, forKey: "inputImage")
request.finish(with: filter.outputImage!, context: nil)
})
#endif
}
return item
}
override func viewDidLoad() {
super.viewDidLoad()
container?.setGalleryContentLoading(true)
let playerView = PlayerView(item: item, player: player)
playerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(playerView)
NSLayoutConstraint.activate([
playerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
playerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
playerView.topAnchor.constraint(equalTo: view.topAnchor),
playerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
preferredContentSize = item.presentationSize
updateItemObservations()
rateObservation = player.observe(\.rate, options: .old, changeHandler: { [unowned self] player, info in
hideControlsWorkItem?.cancel()
if player.rate > 0 && info.oldValue == 0 {
hideControlsWorkItem = DispatchWorkItem { [weak self] in
guard let self,
let container = self.container,
container.galleryControlsVisible else {
return
}
container.setGalleryControlsVisible(false, animated: true)
}
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!)
}
})
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
private func updateItemObservations() {
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
MainActor.runUnsafely {
self.preferredContentSize = item.presentationSize
self.container?.galleryContentChanged()
}
})
statusObservation = item.observe(\.status, changeHandler: { [unowned self] item, _ in
MainActor.runUnsafely {
if item.status == .readyToPlay {
self.container?.setGalleryContentLoading(false)
statusObservation = nil
}
}
})
}
@objc private func preferencesChanged() {
if isGrayscale != Preferences.shared.grayscaleImages {
let isPlaying = player.rate > 0
isGrayscale = Preferences.shared.grayscaleImages
item = VideoGalleryContentViewController.createItem(asset: item.asset)
player.replaceCurrentItem(with: item)
updateItemObservations()
if isPlaying {
#if os(visionOS)
player.play()
#else
player.rate = playbackSpeed
#endif
}
}
}
// MARK: GalleryContentViewController
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
var contentSize: CGSize {
item.presentationSize
}
var activityItemsForSharing: [Any] {
[VideoActivityItemSource(asset: item.asset, url: url)]
}
#if os(visionOS)
private lazy var overlayVC = VideoOverlayViewController(player: player)
#else
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
#endif
var contentOverlayAccessoryViewController: UIViewController? {
overlayVC
}
#if os(visionOS)
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player)
#else
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
#endif
func setControlsVisible(_ visible: Bool, animated: Bool) {
overlayVC.setVisible(visible)
hideControlsWorkItem?.cancel()
}
func galleryContentDidAppear() {
let wasFirstAppearance = isFirstAppearance
isFirstAppearance = false
DispatchQueue.global(qos: .userInitiated).async {
try? AVAudioSession.sharedInstance().setCategory(.playback)
try? AVAudioSession.sharedInstance().setActive(true)
if wasFirstAppearance {
DispatchQueue.main.async {
self.player.play()
}
}
}
}
func galleryContentWillDisappear() {
player.pause()
DispatchQueue.global(qos: .userInitiated).async {
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
}
}
}
private class PlayerView: UIView {
override class var layerClass: AnyClass {
AVPlayerLayer.self
}
private var playerLayer: AVPlayerLayer {
layer as! AVPlayerLayer
}
private let player: AVPlayer
private var presentationSizeObservation: NSKeyValueObservation?
override var intrinsicContentSize: CGSize {
player.currentItem?.presentationSize ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
init(item: AVPlayerItem, player: AVPlayer) {
self.player = player
super.init(frame: .zero)
playerLayer.player = player
playerLayer.videoGravity = .resizeAspect
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] _, _ in
MainActor.runUnsafely {
self.invalidateIntrinsicContentSize()
}
})
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}