mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-10 04:11:15 +02:00
167 lines
6.3 KiB
Swift
167 lines
6.3 KiB
Swift
import AVKit
|
|
import UIKit
|
|
|
|
final class T3NativeVideoPresentation: NSObject, AVPlayerViewControllerDelegate,
|
|
UIAdaptivePresentationControllerDelegate {
|
|
let identifier: String
|
|
private let controller = AVPlayerViewController()
|
|
private let completion: (Error?) -> Void
|
|
private var itemObservation: NSKeyValueObservation?
|
|
private var backgroundObserver: NSObjectProtocol?
|
|
private var playbackError: Error?
|
|
private var presented = false
|
|
private var dismissRequested = false
|
|
private var finished = false
|
|
private struct AudioSessionConfiguration {
|
|
let category: AVAudioSession.Category
|
|
let mode: AVAudioSession.Mode
|
|
let options: AVAudioSession.CategoryOptions
|
|
|
|
init(_ session: AVAudioSession) {
|
|
category = session.category
|
|
mode = session.mode
|
|
options = session.categoryOptions
|
|
}
|
|
}
|
|
private var previousAudioSession: AudioSessionConfiguration?
|
|
private weak var fullScreenController: UIViewController?
|
|
private var embedded = false
|
|
|
|
init(identifier: String, url: URL, title: String, completion: @escaping (Error?) -> Void) {
|
|
self.identifier = identifier
|
|
self.completion = completion
|
|
super.init()
|
|
|
|
let item = AVPlayerItem(url: url)
|
|
let metadata = AVMutableMetadataItem()
|
|
metadata.identifier = .commonIdentifierTitle
|
|
metadata.value = title as NSString
|
|
item.externalMetadata = [metadata]
|
|
controller.player = AVPlayer(playerItem: item)
|
|
controller.delegate = self
|
|
controller.overrideUserInterfaceStyle = .dark
|
|
controller.allowsPictureInPicturePlayback = false
|
|
|
|
itemObservation = item.observe(\.status, options: [.initial, .new]) { [weak self] item, _ in
|
|
guard item.status == .failed else { return }
|
|
DispatchQueue.main.async {
|
|
guard let self else { return }
|
|
self.playbackError = item.error ?? NSError(
|
|
domain: "T3NativeVideo",
|
|
code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "This video couldn't be played on this device."]
|
|
)
|
|
self.dismiss()
|
|
}
|
|
}
|
|
backgroundObserver = NotificationCenter.default.addObserver(
|
|
forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main
|
|
) { [weak self] _ in self?.controller.player?.pause() }
|
|
}
|
|
|
|
func present(from presenter: UIViewController, sources: T3PresentationSources, sourceIdentifier: String) {
|
|
let audioSession = AVAudioSession.sharedInstance()
|
|
previousAudioSession = AudioSessionConfiguration(audioSession)
|
|
do {
|
|
try audioSession.setCategory(.playback, mode: .moviePlayback)
|
|
} catch {
|
|
NSLog("T3 video audio session: %@", error.localizedDescription)
|
|
}
|
|
// AVKit exposes programmatic inline-to-full-screen entry through this selector.
|
|
// This is the same guarded entry point used by expo-video's enterFullscreen().
|
|
let enterFullScreen = NSSelectorFromString("enterFullScreenAnimated:completionHandler:")
|
|
if let source = sources.view(for: sourceIdentifier), source.window != nil,
|
|
controller.responds(to: enterFullScreen) {
|
|
// AVKit owns the transition from its inline view to full screen. Using a
|
|
// separate UIKit zoom transition prevents its native Close action from exiting.
|
|
var responder: UIResponder? = source
|
|
while let current = responder, !(current is UIViewController) { responder = current.next }
|
|
let parent = responder as? UIViewController ?? presenter
|
|
embedded = true
|
|
parent.addChild(controller)
|
|
controller.view.frame = source.bounds
|
|
controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
source.addSubview(controller.view)
|
|
controller.didMove(toParent: parent)
|
|
controller.view.layoutIfNeeded()
|
|
controller.perform(enterFullScreen, with: true, with: nil)
|
|
controller.player?.play()
|
|
} else {
|
|
presenter.present(controller, animated: true) { [self] in
|
|
presented = true
|
|
if dismissRequested {
|
|
dismiss()
|
|
} else if UIApplication.shared.applicationState == .active {
|
|
controller.player?.play()
|
|
}
|
|
}
|
|
controller.presentationController?.delegate = self
|
|
}
|
|
}
|
|
|
|
func dismiss() {
|
|
dismissRequested = true
|
|
guard !finished else { return }
|
|
guard presented else {
|
|
if embedded && fullScreenController == nil { finish() }
|
|
return
|
|
}
|
|
(fullScreenController ?? controller).dismiss(animated: true) { [self] in finish() }
|
|
}
|
|
|
|
func playerViewController(
|
|
_ playerViewController: AVPlayerViewController,
|
|
willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
|
) {
|
|
fullScreenController = coordinator.viewController(forKey: .to)
|
|
coordinator.animate(alongsideTransition: nil) { [weak self] context in
|
|
guard let self else { return }
|
|
if context.isCancelled {
|
|
finish()
|
|
} else {
|
|
presented = true
|
|
if dismissRequested { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
|
|
func playerViewController(
|
|
_ playerViewController: AVPlayerViewController,
|
|
willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
|
) {
|
|
coordinator.animate(alongsideTransition: nil) { [weak self] context in
|
|
if !context.isCancelled { self?.finish() }
|
|
}
|
|
}
|
|
|
|
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
|
finish()
|
|
}
|
|
|
|
private func finish() {
|
|
guard !finished else { return }
|
|
finished = true
|
|
controller.player?.pause()
|
|
if embedded {
|
|
controller.willMove(toParent: nil)
|
|
controller.view.removeFromSuperview()
|
|
controller.removeFromParent()
|
|
}
|
|
itemObservation = nil
|
|
controller.player = nil
|
|
if let backgroundObserver { NotificationCenter.default.removeObserver(backgroundObserver) }
|
|
backgroundObserver = nil
|
|
let audioSession = AVAudioSession.sharedInstance()
|
|
if let previousAudioSession, audioSession.category == .playback,
|
|
audioSession.mode == .moviePlayback, audioSession.categoryOptions.isEmpty {
|
|
// AVPlayer owns activation. Deactivating the shared session here could
|
|
// stop another player or recorder that was active before this preview.
|
|
try? audioSession.setCategory(
|
|
previousAudioSession.category,
|
|
mode: previousAudioSession.mode,
|
|
options: previousAudioSession.options
|
|
)
|
|
}
|
|
completion(playbackError)
|
|
}
|
|
}
|