// // 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 weak 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() { 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") } }