205 lines
7.0 KiB
Swift
205 lines
7.0 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
|
|
|
|
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
|
@Box private var playbackSpeed: Float = 1
|
|
|
|
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 {
|
|
player.rate = playbackSpeed
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: GalleryContentViewController
|
|
|
|
var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
|
|
|
var contentSize: CGSize {
|
|
item.presentationSize
|
|
}
|
|
|
|
var activityItemsForSharing: [Any] {
|
|
[VideoActivityItemSource(asset: item.asset, url: url)]
|
|
}
|
|
|
|
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
|
var contentOverlayAccessoryViewController: UIViewController? {
|
|
overlayVC
|
|
}
|
|
|
|
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
|
|
|
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
|
overlayVC.setVisible(visible)
|
|
hideControlsWorkItem?.cancel()
|
|
}
|
|
|
|
func galleryContentDidAppear() {
|
|
if isFirstAppearance {
|
|
isFirstAppearance = false
|
|
player.play()
|
|
}
|
|
}
|
|
|
|
func galleryContentWillDisappear() {
|
|
player.pause()
|
|
}
|
|
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|