forked from shadowfacts/Tusker
Enable picture-in-picture playback for video attachments
This commit is contained in:
parent
e0acb0f04a
commit
a805da9faa
|
@ -81,6 +81,10 @@
|
|||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
|
|
|
@ -8,9 +8,18 @@
|
|||
|
||||
import UIKit
|
||||
import AVKit
|
||||
import Pachyderm
|
||||
|
||||
class GalleryPlayerViewController: AVPlayerViewController {
|
||||
|
||||
var attachment: Attachment!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
allowsPictureInPicturePlayback = true
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
|
|
|
@ -12,11 +12,13 @@ import AVKit
|
|||
|
||||
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController {
|
||||
|
||||
weak var avPlayerViewControllerDelegate: AVPlayerViewControllerDelegate?
|
||||
|
||||
let attachments: [Attachment]
|
||||
let sourceViews: WeakArray<UIImageView>
|
||||
let startIndex: Int
|
||||
|
||||
let pages: [UIViewController]
|
||||
var pages: [UIViewController]!
|
||||
|
||||
var currentIndex: Int {
|
||||
guard let vc = viewControllers?.first,
|
||||
|
@ -72,6 +74,18 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
|||
self.sourceViews = WeakArray(sourceViews)
|
||||
self.startIndex = startIndex
|
||||
|
||||
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
|
||||
|
||||
modalPresentationStyle = .fullScreen
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.pages = attachments.enumerated().map { (index, attachment) in
|
||||
switch attachment.kind {
|
||||
case .image:
|
||||
|
@ -82,6 +96,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
|||
case .video, .audio:
|
||||
let vc = GalleryPlayerViewController()
|
||||
vc.player = AVPlayer(url: attachment.url)
|
||||
vc.delegate = avPlayerViewControllerDelegate
|
||||
vc.attachment = attachment
|
||||
return vc
|
||||
case .gifv:
|
||||
// Passing the source view to the LargeImageGifvContentView is a crappy workaround for not
|
||||
|
@ -101,19 +117,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
|||
}
|
||||
}
|
||||
|
||||
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
|
||||
|
||||
setViewControllers([pages[startIndex]], direction: .forward, animated: false)
|
||||
|
||||
modalPresentationStyle = .fullScreen
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.dataSource = self
|
||||
self.delegate = self
|
||||
|
|
|
@ -12,7 +12,7 @@ import Gifu
|
|||
import AVFoundation
|
||||
|
||||
protocol AttachmentViewDelegate: class {
|
||||
func attachmentViewGallery(startingAt index: Int) -> UIViewController?
|
||||
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
|
||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
import Combine
|
||||
import AVKit
|
||||
|
||||
protocol StatusTableViewCellDelegate: TuskerNavigationDelegate {
|
||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell)
|
||||
|
@ -70,6 +71,8 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
private var statusUpdater: Cancellable?
|
||||
private var accountUpdater: Cancellable?
|
||||
|
||||
private var currentPictureInPictureVideoStatusID: String?
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
|
@ -360,15 +363,74 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
extension BaseStatusTableViewCell: AttachmentViewDelegate {
|
||||
func attachmentViewGallery(startingAt index: Int) -> UIViewController? {
|
||||
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? {
|
||||
guard let delegate = delegate,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else { return nil }
|
||||
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
|
||||
return delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
|
||||
let gallery = delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
|
||||
gallery.avPlayerViewControllerDelegate = self
|
||||
return gallery
|
||||
}
|
||||
|
||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
||||
delegate?.show(vc)
|
||||
delegate?.present(vc, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
// todo: This is not ideal. It works when the original cell remains visible and when the cell is reused, but if the cell is dealloc'd
|
||||
// resuming from PiP won't work because AVPlayerViewController.delegate is a weak reference.
|
||||
extension BaseStatusTableViewCell: AVPlayerViewControllerDelegate {
|
||||
func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
||||
// We need to save the current statusID when PiP is initiated, because if the user restores from PiP after this cell has
|
||||
// been reused, the current value of statusID will not be correct.
|
||||
currentPictureInPictureVideoStatusID = statusID
|
||||
}
|
||||
|
||||
func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
||||
currentPictureInPictureVideoStatusID = nil
|
||||
}
|
||||
|
||||
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: AVPlayerViewController) -> Bool {
|
||||
// Ideally, when PiP is automatically initiated by app closing the gallery should not be dismissed
|
||||
// and when PiP is started because the user has tapped the button in the player controls the gallery
|
||||
// gallery should be dismissed. Unfortunately, this doesn't seem to be possible. Instead, the gallery is
|
||||
// always dismissed and is recreated when restoring the interface from PiP.
|
||||
return true
|
||||
}
|
||||
|
||||
func playerViewController(_ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
guard let delegate = delegate,
|
||||
let playerViewController = playerViewController as? GalleryPlayerViewController,
|
||||
let id = currentPictureInPictureVideoStatusID,
|
||||
let status = mastodonController.persistentContainer.status(for: id),
|
||||
let index = status.attachments.firstIndex(where: { $0.id == playerViewController.attachment?.id }) else {
|
||||
// returning without invoking completionHandler will dismiss the PiP window
|
||||
return
|
||||
}
|
||||
|
||||
// We create a new gallery view controller starting at the appropriate index and swap the
|
||||
// already-playing VC into the appropriate index so it smoothly continues playing.
|
||||
|
||||
let sourceViews: [UIImageView?]
|
||||
if self.statusID == id {
|
||||
sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
|
||||
} else {
|
||||
sourceViews = status.attachments.map { (_) in nil }
|
||||
}
|
||||
let gallery = delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
|
||||
gallery.avPlayerViewControllerDelegate = self
|
||||
|
||||
// ensure that all other page VCs are created
|
||||
gallery.loadViewIfNeeded()
|
||||
// replace the newly created player for the same attachment with the already-playing one
|
||||
gallery.pages[index] = playerViewController
|
||||
gallery.setViewControllers([playerViewController], direction: .forward, animated: false, completion: nil)
|
||||
|
||||
// this isn't animated, otherwise the animation plays first and then the PiP window expands
|
||||
// which looks even weirder than the black background appearing instantly and then the PiP window animating
|
||||
delegate.present(gallery, animated: false) {
|
||||
completionHandler(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue