Enable picture-in-picture playback for video attachments
This commit is contained in:
parent
e0acb0f04a
commit
a805da9faa
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,19 +117,7 @@ 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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue