import AVKit import Logging import SwiftUI final class PlayerViewController: UIViewController { var playerLoaded = false var navigationModel: NavigationModel! var playerModel: PlayerModel! var playerViewController = AVPlayerViewController() #if !os(tvOS) var aspectRatio: Double? { let ratio = Double(playerViewController.videoBounds.width) / Double(playerViewController.videoBounds.height) guard ratio.isFinite else { return VideoPlayerView.defaultAspectRatio // swiftlint:disable:this implicit_return } return [ratio, 1.0].max()! } #endif override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) loadPlayer() #if os(tvOS) if !playerViewController.isBeingPresented, !playerViewController.isBeingDismissed { present(playerViewController, animated: false) } #endif } func loadPlayer() { guard !playerLoaded else { return } playerModel.controller = self playerViewController.player = playerModel.player playerViewController.allowsPictureInPicturePlayback = true playerViewController.delegate = self #if os(tvOS) playerModel.avPlayerViewController = playerViewController playerViewController.customInfoViewControllers = [ infoViewController([.related], title: "Related"), infoViewController([.playingNext, .playedPreviously], title: "Playing Next") ] #else embedViewController() #endif } #if os(tvOS) func infoViewController( _ sections: [NowPlayingView.ViewSection], title: String ) -> UIHostingController { let controller = UIHostingController(rootView: AnyView( NowPlayingView(sections: sections, inInfoViewController: true) .frame(maxHeight: 600) .environmentObject(playerModel) ) ) controller.title = title return controller } #else func embedViewController() { playerViewController.view.frame = view.bounds addChild(playerViewController) view.addSubview(playerViewController.view) playerViewController.didMove(toParent: self) } #endif } extension PlayerViewController: AVPlayerViewControllerDelegate { func playerViewControllerShouldDismiss(_: AVPlayerViewController) -> Bool { true } func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool { true } func playerViewControllerWillBeginDismissalTransition(_: AVPlayerViewController) {} func playerViewControllerDidEndDismissalTransition(_: AVPlayerViewController) { dismiss(animated: false) } func playerViewController( _: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator ) {} func playerViewController( _: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator ) { coordinator.animate(alongsideTransition: nil) { context in if !context.isCancelled { #if os(iOS) if self.traitCollection.verticalSizeClass == .compact { self.dismiss(animated: true) } #endif } } } func playerViewController( _ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void ) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if self.navigationModel.presentingChannel { self.playerModel.playerNavigationLinkActive = true } else { self.playerModel.presentPlayer() } #if os(tvOS) if self.playerModel.playingInPictureInPicture { self.present(playerViewController, animated: false) { completionHandler(true) } } #else completionHandler(true) #endif } } func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) { playerModel.playingInPictureInPicture = true playerModel.playerNavigationLinkActive = false } func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) { playerModel.playingInPictureInPicture = false } }