Enable picture-in-picture playback for video attachments

This commit is contained in:
Shadowfacts 2020-10-17 12:56:13 -04:00
parent e0acb0f04a
commit a805da9faa
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
5 changed files with 96 additions and 17 deletions

View File

@ -81,6 +81,10 @@
</array> </array>
</dict> </dict>
</dict> </dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>

View File

@ -8,9 +8,18 @@
import UIKit import UIKit
import AVKit import AVKit
import Pachyderm
class GalleryPlayerViewController: AVPlayerViewController { class GalleryPlayerViewController: AVPlayerViewController {
var attachment: Attachment!
override func viewDidLoad() {
super.viewDidLoad()
allowsPictureInPicturePlayback = true
}
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)

View File

@ -12,11 +12,13 @@ import AVKit
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController { class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController {
weak var avPlayerViewControllerDelegate: AVPlayerViewControllerDelegate?
let attachments: [Attachment] let attachments: [Attachment]
let sourceViews: WeakArray<UIImageView> let sourceViews: WeakArray<UIImageView>
let startIndex: Int let startIndex: Int
let pages: [UIViewController] var pages: [UIViewController]!
var currentIndex: Int { var currentIndex: Int {
guard let vc = viewControllers?.first, guard let vc = viewControllers?.first,
@ -72,6 +74,18 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
self.sourceViews = WeakArray(sourceViews) self.sourceViews = WeakArray(sourceViews)
self.startIndex = startIndex 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 self.pages = attachments.enumerated().map { (index, attachment) in
switch attachment.kind { switch attachment.kind {
case .image: case .image:
@ -82,6 +96,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
case .video, .audio: case .video, .audio:
let vc = GalleryPlayerViewController() let vc = GalleryPlayerViewController()
vc.player = AVPlayer(url: attachment.url) vc.player = AVPlayer(url: attachment.url)
vc.delegate = avPlayerViewControllerDelegate
vc.attachment = attachment
return vc return vc
case .gifv: case .gifv:
// Passing the source view to the LargeImageGifvContentView is a crappy workaround for not // Passing the source view to the LargeImageGifvContentView is a crappy workaround for not
@ -101,20 +117,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
} }
} }
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
setViewControllers([pages[startIndex]], direction: .forward, animated: false) 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.dataSource = self
self.delegate = self self.delegate = self

View File

@ -12,7 +12,7 @@ import Gifu
import AVFoundation import AVFoundation
protocol AttachmentViewDelegate: class { protocol AttachmentViewDelegate: class {
func attachmentViewGallery(startingAt index: Int) -> UIViewController? func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
} }

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import Combine import Combine
import AVKit
protocol StatusTableViewCellDelegate: TuskerNavigationDelegate { protocol StatusTableViewCellDelegate: TuskerNavigationDelegate {
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell)
@ -70,6 +71,8 @@ class BaseStatusTableViewCell: UITableViewCell {
private var statusUpdater: Cancellable? private var statusUpdater: Cancellable?
private var accountUpdater: Cancellable? private var accountUpdater: Cancellable?
private var currentPictureInPictureVideoStatusID: String?
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -360,15 +363,74 @@ class BaseStatusTableViewCell: UITableViewCell {
} }
extension BaseStatusTableViewCell: AttachmentViewDelegate { extension BaseStatusTableViewCell: AttachmentViewDelegate {
func attachmentViewGallery(startingAt index: Int) -> UIViewController? { func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? {
guard let delegate = delegate, guard let delegate = delegate,
let status = mastodonController.persistentContainer.status(for: statusID) else { return nil } let status = mastodonController.persistentContainer.status(for: statusID) else { return nil }
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:)) 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) { 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)
}
} }
} }