Native picture in picture support

This commit is contained in:
Ali 2021-11-02 16:04:39 +04:00
parent cf624bcad3
commit 17c84b85a0
17 changed files with 469 additions and 87 deletions

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import AVKit
public struct OverlayMediaItemNodeGroup: Hashable, RawRepresentable { public struct OverlayMediaItemNodeGroup: Hashable, RawRepresentable {
public var rawValue: Int32 public var rawValue: Int32
@ -61,4 +62,9 @@ open class OverlayMediaItemNode: ASDisplayNode {
open func performCustomTransitionOut() -> Bool { open func performCustomTransitionOut() -> Bool {
return false return false
} }
@available(iOSApplicationExtension 15.0, iOS 15.0, *)
open func makeNativeContentSource() -> AVPictureInPictureController.ContentSource? {
return nil
}
} }

View File

@ -1,6 +1,8 @@
import Foundation import Foundation
import UIKit import UIKit
import Display import Display
import AVFoundation
import AsyncDisplayKit
public final class OverlayMediaControllerEmbeddingItem { public final class OverlayMediaControllerEmbeddingItem {
public let position: CGPoint public let position: CGPoint
@ -15,6 +17,10 @@ public final class OverlayMediaControllerEmbeddingItem {
} }
} }
public protocol PictureInPictureContent: AnyObject {
var videoNode: ASDisplayNode { get }
}
public protocol OverlayMediaController: AnyObject { public protocol OverlayMediaController: AnyObject {
var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)? { get set } var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)? { get set }
var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)? { get set } var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)? { get set }
@ -22,6 +28,10 @@ public protocol OverlayMediaController: AnyObject {
var hasNodes: Bool { get } var hasNodes: Bool { get }
func addNode(_ node: OverlayMediaItemNode, customTransition: Bool) func addNode(_ node: OverlayMediaItemNode, customTransition: Bool)
func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool) func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool)
func setPictureInPictureContent(content: PictureInPictureContent, absoluteRect: CGRect)
func setPictureInPictureContentHidden(content: PictureInPictureContent, isHidden value: Bool)
func removePictureInPictureContent(content: PictureInPictureContent)
} }
public final class OverlayMediaManager { public final class OverlayMediaManager {

View File

@ -7,6 +7,7 @@ import TelegramCore
import Display import Display
import TelegramAudio import TelegramAudio
import UniversalMediaPlayer import UniversalMediaPlayer
import AVFoundation
public protocol UniversalVideoContentNode: AnyObject { public protocol UniversalVideoContentNode: AnyObject {
var ready: Signal<Void, NoError> { get } var ready: Signal<Void, NoError> { get }
@ -29,6 +30,7 @@ public protocol UniversalVideoContentNode: AnyObject {
func removePlaybackCompleted(_ index: Int) func removePlaybackCompleted(_ index: Int)
func fetchControl(_ control: UniversalVideoNodeFetchControl) func fetchControl(_ control: UniversalVideoNodeFetchControl)
func notifyPlaybackControlsHidden(_ hidden: Bool) func notifyPlaybackControlsHidden(_ hidden: Bool)
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool)
} }
public protocol UniversalVideoContent { public protocol UniversalVideoContent {
@ -332,4 +334,36 @@ public final class UniversalVideoNode: ASDisplayNode {
self.decoration.tap() self.decoration.tap()
} }
} }
public func getVideoLayer() -> AVSampleBufferDisplayLayer? {
guard let contentNode = self.contentNode else {
return nil
}
func findVideoLayer(layer: CALayer) -> AVSampleBufferDisplayLayer? {
if let layer = layer as? AVSampleBufferDisplayLayer {
return layer
}
if let sublayers = layer.sublayers {
for sublayer in sublayers {
if let result = findVideoLayer(layer: sublayer) {
return result
}
}
}
return nil
}
return findVideoLayer(layer: contentNode.layer)
}
public func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy)
}
})
}
} }

View File

@ -385,6 +385,8 @@ public class GalleryController: ViewController, StandalonePresentableController
private var screenCaptureEventsDisposable: Disposable? private var screenCaptureEventsDisposable: Disposable?
public var centralItemUpdated: ((MessageId) -> Void)? public var centralItemUpdated: ((MessageId) -> Void)?
public var onDidAppear: (() -> Void)?
public var useSimpleAnimation: Bool = false
private var initialOrientation: UIInterfaceOrientation? private var initialOrientation: UIInterfaceOrientation?
@ -1037,11 +1039,11 @@ public class GalleryController: ViewController, StandalonePresentableController
self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.presentingViewController?.dismiss(animated: false, completion: nil)
} }
self.galleryNode.beginCustomDismiss = { [weak self] in self.galleryNode.beginCustomDismiss = { [weak self] simpleAnimation in
if let strongSelf = self { if let strongSelf = self {
strongSelf._hiddenMedia.set(.single(nil)) strongSelf._hiddenMedia.set(.single(nil))
let animatedOutNode = true let animatedOutNode = !simpleAnimation
strongSelf.galleryNode.animateOut(animateContent: animatedOutNode, completion: { strongSelf.galleryNode.animateOut(animateContent: animatedOutNode, completion: {
}) })
@ -1296,13 +1298,15 @@ public class GalleryController: ViewController, StandalonePresentableController
} }
centralItemNode.activateAsInitial() centralItemNode.activateAsInitial()
} }
self.onDidAppear?()
} }
if !self.isPresentedInPreviewingContext() { if !self.isPresentedInPreviewingContext() {
self.galleryNode.setControlsHidden(self.landscape, animated: false) self.galleryNode.setControlsHidden(self.landscape, animated: false)
if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments {
if presentationArguments.animated { if presentationArguments.animated {
self.galleryNode.animateIn(animateContent: !nodeAnimatesItself) self.galleryNode.animateIn(animateContent: !nodeAnimatesItself && !self.useSimpleAnimation)
} }
} }
} }

View File

@ -19,7 +19,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
public var scrollView: UIScrollView public var scrollView: UIScrollView
public var pager: GalleryPagerNode public var pager: GalleryPagerNode
public var beginCustomDismiss: () -> Void = { } public var beginCustomDismiss: (Bool) -> Void = { _ in }
public var completeCustomDismiss: () -> Void = { } public var completeCustomDismiss: () -> Void = { }
public var baseNavigationController: () -> NavigationController? = { return nil } public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil } public var galleryController: () -> ViewController? = { return nil }
@ -106,9 +106,9 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
} }
} }
self.pager.beginCustomDismiss = { [weak self] in self.pager.beginCustomDismiss = { [weak self] simpleAnimation in
if let strongSelf = self { if let strongSelf = self {
strongSelf.beginCustomDismiss() strongSelf.beginCustomDismiss(simpleAnimation)
} }
} }
@ -328,13 +328,15 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
} }
open func animateIn(animateContent: Bool) { open func animateIn(animateContent: Bool) {
let duration: Double = animateContent ? 0.2 : 0.3
self.backgroundNode.backgroundColor = self.backgroundNode.backgroundColor?.withAlphaComponent(0.0) self.backgroundNode.backgroundColor = self.backgroundNode.backgroundColor?.withAlphaComponent(0.0)
self.statusBar?.alpha = 0.0 self.statusBar?.alpha = 0.0
self.navigationBar?.alpha = 0.0 self.navigationBar?.alpha = 0.0
self.footerNode.alpha = 0.0 self.footerNode.alpha = 0.0
self.currentThumbnailContainerNode?.alpha = 0.0 self.currentThumbnailContainerNode?.alpha = 0.0
UIView.animate(withDuration: 0.2, animations: { UIView.animate(withDuration: duration, animations: {
self.backgroundNode.backgroundColor = self.backgroundNode.backgroundColor?.withAlphaComponent(1.0) self.backgroundNode.backgroundColor = self.backgroundNode.backgroundColor?.withAlphaComponent(1.0)
if !self.areControlsHidden { if !self.areControlsHidden {
self.statusBar?.alpha = 1.0 self.statusBar?.alpha = 1.0
@ -346,6 +348,8 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
if animateContent { if animateContent {
self.scrollView.layer.animateBounds(from: self.scrollView.layer.bounds.offsetBy(dx: 0.0, dy: -self.scrollView.layer.bounds.size.height), to: self.scrollView.layer.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) self.scrollView.layer.animateBounds(from: self.scrollView.layer.bounds.offsetBy(dx: 0.0, dy: -self.scrollView.layer.bounds.size.height), to: self.scrollView.layer.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
} else {
self.scrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
} }
} }
@ -384,6 +388,11 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
contentAnimationCompleted = true contentAnimationCompleted = true
intermediateCompletion() intermediateCompletion()
}) })
} else {
self.scrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
contentAnimationCompleted = true
intermediateCompletion()
})
} }
} }

View File

@ -24,7 +24,7 @@ open class GalleryItemNode: ASDisplayNode {
public var updateControlsVisibility: (Bool) -> Void = { _ in } public var updateControlsVisibility: (Bool) -> Void = { _ in }
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
public var dismiss: () -> Void = { } public var dismiss: () -> Void = { }
public var beginCustomDismiss: () -> Void = { } public var beginCustomDismiss: (Bool) -> Void = { _ in }
public var completeCustomDismiss: () -> Void = { } public var completeCustomDismiss: () -> Void = { }
public var baseNavigationController: () -> NavigationController? = { return nil } public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil } public var galleryController: () -> ViewController? = { return nil }

View File

@ -109,7 +109,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
public var updateControlsVisibility: (Bool) -> Void = { _ in } public var updateControlsVisibility: (Bool) -> Void = { _ in }
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
public var dismiss: () -> Void = { } public var dismiss: () -> Void = { }
public var beginCustomDismiss: () -> Void = { } public var beginCustomDismiss: (Bool) -> Void = { _ in }
public var completeCustomDismiss: () -> Void = { } public var completeCustomDismiss: () -> Void = { }
public var baseNavigationController: () -> NavigationController? = { return nil } public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil } public var galleryController: () -> ViewController? = { return nil }

View File

@ -20,6 +20,7 @@ import SaveToCameraRoll
import UndoUI import UndoUI
import TelegramUIPreferences import TelegramUIPreferences
import OpenInExternalAppUI import OpenInExternalAppUI
import AVKit
public enum UniversalVideoGalleryItemContentInfo { public enum UniversalVideoGalleryItemContentInfo {
case message(Message) case message(Message)
@ -463,6 +464,176 @@ private final class MoreHeaderButton: HighlightableButtonNode {
} }
} }
@available(iOS 15.0, *)
private final class PictureInPictureContentImpl: NSObject, PictureInPictureContent, AVPictureInPictureControllerDelegate {
private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate {
private let node: UniversalVideoNode
private var statusDisposable: Disposable?
private var status: MediaPlayerStatus?
weak var pictureInPictureController: AVPictureInPictureController?
init(node: UniversalVideoNode) {
self.node = node
super.init()
self.statusDisposable = (self.node.status
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let strongSelf = self else {
return
}
strongSelf.status = status
strongSelf.pictureInPictureController?.invalidatePlaybackState()
})
}
deinit {
self.statusDisposable?.dispose()
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) {
self.node.togglePlayPause()
}
public func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange {
guard let status = self.status else {
return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)))
}
return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: status.duration, preferredTimescale: CMTimeScale(30.0)))
}
public func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
guard let status = self.status else {
return false
}
switch status.status {
case .playing:
return false
case .buffering, .paused:
return true
}
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) {
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) {
completionHandler()
}
public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
return false
}
}
private weak var overlayController: OverlayMediaController?
private var pictureInPictureController: AVPictureInPictureController?
private var contentDelegate: PlaybackDelegate?
private let node: UniversalVideoNode
private let willBegin: (PictureInPictureContentImpl) -> Void
private let didEnd: (PictureInPictureContentImpl) -> Void
private let expand: (@escaping () -> Void) -> Void
private var pictureInPictureTimer: SwiftSignalKit.Timer?
private var didExpand: Bool = false
init(overlayController: OverlayMediaController, videoNode: UniversalVideoNode, willBegin: @escaping (PictureInPictureContentImpl) -> Void, didEnd: @escaping (PictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) {
self.overlayController = overlayController
self.node = videoNode
self.willBegin = willBegin
self.didEnd = didEnd
self.expand = expand
self.node.setCanPlaybackWithoutHierarchy(true)
super.init()
let contentDelegate = PlaybackDelegate(node: self.node)
self.contentDelegate = contentDelegate
let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoNode.getVideoLayer()!, playbackDelegate: contentDelegate))
self.pictureInPictureController = pictureInPictureController
contentDelegate.pictureInPictureController = pictureInPictureController
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false
pictureInPictureController.requiresLinearPlayback = true
pictureInPictureController.delegate = self
self.pictureInPictureController = pictureInPictureController
let timer = SwiftSignalKit.Timer(timeout: 0.005, repeat: true, completion: { [weak self] in
guard let strongSelf = self, let pictureInPictureController = strongSelf.pictureInPictureController else {
return
}
if pictureInPictureController.isPictureInPicturePossible {
strongSelf.pictureInPictureTimer?.invalidate()
strongSelf.pictureInPictureTimer = nil
pictureInPictureController.startPictureInPicture()
}
}, queue: .mainQueue())
self.pictureInPictureTimer = timer
timer.start()
}
deinit {
self.pictureInPictureTimer?.invalidate()
self.node.setCanPlaybackWithoutHierarchy(false)
}
var videoNode: ASDisplayNode {
return self.node
}
public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
Queue.mainQueue().after(0.1, { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.willBegin(strongSelf)
if let overlayController = strongSelf.overlayController {
overlayController.setPictureInPictureContentHidden(content: strongSelf, isHidden: true)
}
})
}
public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.didEnd(self)
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
}
public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
}
public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
guard let overlayController = self.overlayController else {
return
}
overlayController.removePictureInPictureContent(content: self)
if self.didExpand {
return
}
self.node.continuePlayingWithoutSound()
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
self.expand { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.didExpand = true
if let overlayController = strongSelf.overlayController {
overlayController.setPictureInPictureContentHidden(content: strongSelf, isHidden: false)
strongSelf.node.alpha = 0.02
}
completionHandler(true)
}
}
}
final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext private let context: AccountContext
private let presentationData: PresentationData private let presentationData: PresentationData
@ -485,6 +656,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private var videoNodeUserInteractionEnabled: Bool = false private var videoNodeUserInteractionEnabled: Bool = false
private var videoFramePreview: FramePreview? private var videoFramePreview: FramePreview?
private var pictureInPictureNode: UniversalVideoGalleryItemPictureInPictureNode? private var pictureInPictureNode: UniversalVideoGalleryItemPictureInPictureNode?
private var disablePictureInPicturePlaceholder: Bool = false
private let statusButtonNode: HighlightableButtonNode private let statusButtonNode: HighlightableButtonNode
private let statusNode: RadialStatusNode private let statusNode: RadialStatusNode
private var statusNodeShouldBeHidden = true private var statusNodeShouldBeHidden = true
@ -531,6 +703,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var playbackCompleted: (() -> Void)? var playbackCompleted: (() -> Void)?
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)? private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
private var pictureInPictureContent: AnyObject?
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context self.context = context
@ -1209,7 +1383,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
private func updateDisplayPlaceholder(_ displayPlaceholder: Bool) { private func updateDisplayPlaceholder(_ displayPlaceholder: Bool) {
if displayPlaceholder { if displayPlaceholder && !self.disablePictureInPicturePlaceholder {
if self.pictureInPictureNode == nil { if self.pictureInPictureNode == nil {
let pictureInPictureNode = UniversalVideoGalleryItemPictureInPictureNode(strings: self.presentationData.strings) let pictureInPictureNode = UniversalVideoGalleryItemPictureInPictureNode(strings: self.presentationData.strings)
pictureInPictureNode.isUserInteractionEnabled = false pictureInPictureNode.isUserInteractionEnabled = false
@ -1856,7 +2030,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
} }
if customUnembedWhenPortrait(overlayNode) { if customUnembedWhenPortrait(overlayNode) {
self.beginCustomDismiss() self.beginCustomDismiss(false)
self.statusNode.isHidden = true self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
self?.completeCustomDismiss() self?.completeCustomDismiss()
@ -1866,80 +2040,114 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
@objc func pictureInPictureButtonPressed() { @objc func pictureInPictureButtonPressed() {
if let item = self.item, let videoNode = self.videoNode { if let item = self.item, let videoNode = self.videoNode, let overlayController = self.context.sharedContext.mediaManager.overlayMediaManager.controller {
videoNode.setContinuePlayingWithoutSoundOnLostAudioSession(false) videoNode.setContinuePlayingWithoutSoundOnLostAudioSession(false)
let context = self.context let context = self.context
let baseNavigationController = self.baseNavigationController() let baseNavigationController = self.baseNavigationController()
let mediaManager = self.context.sharedContext.mediaManager
var expandImpl: (() -> Void)?
let shouldBeDismissed: Signal<Bool, NoError>
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
let viewKey = PostboxViewKey.messages(Set([message.id]))
shouldBeDismissed = context.account.postbox.combinedView(keys: [viewKey])
|> map { views -> Bool in
guard let view = views.views[viewKey] as? MessagesView else {
return false
}
if view.messages.isEmpty {
return true
} else {
return false
}
}
|> distinctUntilChanged
} else {
shouldBeDismissed = .single(false)
}
let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: {
expandImpl?()
}, close: { [weak mediaManager] in
mediaManager?.setOverlayVideoNode(nil)
})
let playbackRate = self.playbackRate let playbackRate = self.playbackRate
expandImpl = { [weak overlayNode] in if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() {
guard let contentInfo = item.contentInfo, let overlayNode = overlayNode else { self.disablePictureInPicturePlaceholder = true
return
} let overlayVideoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay)
let absoluteRect = videoNode.view.convert(videoNode.view.bounds, to: nil)
switch contentInfo { overlayVideoNode.frame = absoluteRect
overlayVideoNode.updateLayout(size: absoluteRect.size, transition: .immediate)
overlayVideoNode.canAttachContent = true
let content = PictureInPictureContentImpl(overlayController: overlayController, videoNode: overlayVideoNode, willBegin: { [weak self] content in
guard let strongSelf = self else {
return
}
strongSelf.beginCustomDismiss(true)
}, didEnd: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.completeCustomDismiss()
}, expand: { [weak baseNavigationController] completion in
guard let contentInfo = item.contentInfo else {
return
}
switch contentInfo {
case let .message(message): case let .message(message):
let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil)), playbackRate: playbackRate, replaceRootController: { controller, ready in let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil)), playbackRate: playbackRate, replaceRootController: { [weak baseNavigationController] controller, ready in
if let baseNavigationController = baseNavigationController { if let baseNavigationController = baseNavigationController {
baseNavigationController.replaceTopController(controller, animated: false, ready: ready) baseNavigationController.replaceTopController(controller, animated: false, ready: ready)
} }
}, baseNavigationController: baseNavigationController) }, baseNavigationController: baseNavigationController)
gallery.temporaryDoNotWaitForReady = true gallery.temporaryDoNotWaitForReady = true
gallery.useSimpleAnimation = true
baseNavigationController?.view.endEditing(true) baseNavigationController?.view.endEditing(true)
(baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak overlayNode] id, media in (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { id, media in
if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode {
return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in
return (overlayNode?.view.snapshotContentTree(), nil)
}), addToTransitionSurface: { [weak context, weak overlaySupernode, weak overlayNode] view in
guard let context = context, let overlayNode = overlayNode else {
return
}
if context.sharedContext.mediaManager.hasOverlayVideoNode(overlayNode) {
overlaySupernode?.view.addSubview(view)
}
overlayNode.canAttachContent = false
})
} else if let info = context.sharedContext.mediaManager.galleryHiddenMediaManager.findTarget(messageId: id, media: media) {
return GalleryTransitionArguments(transitionNode: (info.1, info.1.bounds, {
return info.2()
}), addToTransitionSurface: info.0)
}
return nil return nil
})) }))
case let .webPage(_, _, expandFromPip):
if let expandFromPip = expandFromPip, let baseNavigationController = baseNavigationController { gallery.onDidAppear = {
expandFromPip({ [weak overlayNode] in completion()
}
case .webPage:
break
}
})
self.pictureInPictureContent = content
self.context.sharedContext.mediaManager.overlayMediaManager.controller?.setPictureInPictureContent(content: content, absoluteRect: absoluteRect)
} else {
let context = self.context
let baseNavigationController = self.baseNavigationController()
let mediaManager = self.context.sharedContext.mediaManager
var expandImpl: (() -> Void)?
let shouldBeDismissed: Signal<Bool, NoError>
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
let viewKey = PostboxViewKey.messages(Set([message.id]))
shouldBeDismissed = context.account.postbox.combinedView(keys: [viewKey])
|> map { views -> Bool in
guard let view = views.views[viewKey] as? MessagesView else {
return false
}
if view.messages.isEmpty {
return true
} else {
return false
}
}
|> distinctUntilChanged
} else {
shouldBeDismissed = .single(false)
}
let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: {
expandImpl?()
}, close: { [weak mediaManager] in
mediaManager?.setOverlayVideoNode(nil)
})
let playbackRate = self.playbackRate
expandImpl = { [weak overlayNode] in
guard let contentInfo = item.contentInfo, let overlayNode = overlayNode else {
return
}
switch contentInfo {
case let .message(message):
let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil)), playbackRate: playbackRate, replaceRootController: { controller, ready in
if let baseNavigationController = baseNavigationController {
baseNavigationController.replaceTopController(controller, animated: false, ready: ready)
}
}, baseNavigationController: baseNavigationController)
gallery.temporaryDoNotWaitForReady = true
baseNavigationController?.view.endEditing(true)
(baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { [weak overlayNode] id, media in
if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode { if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode {
return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in
return (overlayNode?.view.snapshotContentTree(), nil) return (overlayNode?.view.snapshotContentTree(), nil)
@ -1952,21 +2160,44 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
overlayNode.canAttachContent = false overlayNode.canAttachContent = false
}) })
} else if let info = context.sharedContext.mediaManager.galleryHiddenMediaManager.findTarget(messageId: id, media: media) {
return GalleryTransitionArguments(transitionNode: (info.1, info.1.bounds, {
return info.2()
}), addToTransitionSurface: info.0)
} }
return nil return nil
}, baseNavigationController, { [weak baseNavigationController] c, a in }))
(baseNavigationController?.topViewController as? ViewController)?.present(c, in: .window(.root), with: a) case let .webPage(_, _, expandFromPip):
}) if let expandFromPip = expandFromPip, let baseNavigationController = baseNavigationController {
expandFromPip({ [weak overlayNode] in
if let overlayNode = overlayNode, let overlaySupernode = overlayNode.supernode {
return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in
return (overlayNode?.view.snapshotContentTree(), nil)
}), addToTransitionSurface: { [weak context, weak overlaySupernode, weak overlayNode] view in
guard let context = context, let overlayNode = overlayNode else {
return
}
if context.sharedContext.mediaManager.hasOverlayVideoNode(overlayNode) {
overlaySupernode?.view.addSubview(view)
}
overlayNode.canAttachContent = false
})
}
return nil
}, baseNavigationController, { [weak baseNavigationController] c, a in
(baseNavigationController?.topViewController as? ViewController)?.present(c, in: .window(.root), with: a)
})
}
} }
} }
} context.sharedContext.mediaManager.setOverlayVideoNode(overlayNode)
context.sharedContext.mediaManager.setOverlayVideoNode(overlayNode) if overlayNode.supernode != nil {
if overlayNode.supernode != nil { self.beginCustomDismiss(false)
self.beginCustomDismiss() self.statusNode.isHidden = true
self.statusNode.isHidden = true self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in self?.completeCustomDismiss()
self?.completeCustomDismiss() })
}) }
} }
} }
} }

View File

@ -232,7 +232,7 @@ public final class SecretMediaPreviewController: ViewController {
self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.presentingViewController?.dismiss(animated: false, completion: nil)
} }
self.controllerNode.beginCustomDismiss = { [weak self] in self.controllerNode.beginCustomDismiss = { [weak self] _ in
if let strongSelf = self { if let strongSelf = self {
strongSelf._hiddenMedia.set(.single(nil)) strongSelf._hiddenMedia.set(.single(nil))

View File

@ -61,6 +61,7 @@ private enum PollStatus: CustomStringConvertible {
public final class MediaPlayerNode: ASDisplayNode { public final class MediaPlayerNode: ASDisplayNode {
public var videoInHierarchy: Bool = false public var videoInHierarchy: Bool = false
var canPlaybackWithoutHierarchy: Bool = false
public var updateVideoInHierarchy: ((Bool) -> Void)? public var updateVideoInHierarchy: ((Bool) -> Void)?
private var videoNode: MediaPlayerNodeDisplayNode private var videoNode: MediaPlayerNodeDisplayNode
@ -117,7 +118,7 @@ public final class MediaPlayerNode: ASDisplayNode {
videoLayer.setAffineTransform(transform) videoLayer.setAffineTransform(transform)
} }
if self.videoInHierarchy { if self.videoInHierarchy || self.canPlaybackWithoutHierarchy {
if requestFrames { if requestFrames {
self.startPolling() self.startPolling()
} }
@ -137,7 +138,7 @@ public final class MediaPlayerNode: ASDisplayNode {
switch status { switch status {
case let .delay(delay): case let .delay(delay):
strongSelf.timer = SwiftSignalKit.Timer(timeout: delay, repeat: true, completion: { strongSelf.timer = SwiftSignalKit.Timer(timeout: delay, repeat: true, completion: {
if let strongSelf = self, let videoLayer = strongSelf.videoLayer, let (_, requestFrames, _, _) = strongSelf.state, requestFrames, strongSelf.videoInHierarchy { if let strongSelf = self, let videoLayer = strongSelf.videoLayer, let (_, requestFrames, _, _) = strongSelf.state, requestFrames, (strongSelf.videoInHierarchy || strongSelf.canPlaybackWithoutHierarchy) {
if videoLayer.isReadyForMoreMediaData { if videoLayer.isReadyForMoreMediaData {
strongSelf.timer?.invalidate() strongSelf.timer?.invalidate()
strongSelf.timer = nil strongSelf.timer = nil
@ -385,7 +386,7 @@ public final class MediaPlayerNode: ASDisplayNode {
strongSelf.updateState() strongSelf.updateState()
} }
} }
strongSelf.updateVideoInHierarchy?(value) strongSelf.updateVideoInHierarchy?(strongSelf.videoInHierarchy || strongSelf.canPlaybackWithoutHierarchy)
} }
} }
self.addSubnode(self.videoNode) self.addSubnode(self.videoNode)
@ -458,4 +459,14 @@ public final class MediaPlayerNode: ASDisplayNode {
public func reset() { public func reset() {
self.videoLayer?.flush() self.videoLayer?.flush()
} }
public func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
if self.canPlaybackWithoutHierarchy != canPlaybackWithoutHierarchy {
self.canPlaybackWithoutHierarchy = canPlaybackWithoutHierarchy
if canPlaybackWithoutHierarchy {
self.updateState()
}
}
self.updateVideoInHierarchy?(self.videoInHierarchy || self.canPlaybackWithoutHierarchy)
}
} }

View File

@ -5,6 +5,7 @@ import AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
import Postbox import Postbox
import AccountContext import AccountContext
import AVKit
public final class OverlayMediaControllerImpl: ViewController, OverlayMediaController { public final class OverlayMediaControllerImpl: ViewController, OverlayMediaController {
private var controllerNode: OverlayMediaControllerNode { private var controllerNode: OverlayMediaControllerNode {
@ -13,6 +14,9 @@ public final class OverlayMediaControllerImpl: ViewController, OverlayMediaContr
public var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)? public var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)?
public var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)? public var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)?
private var pictureInPictureContainer: ASDisplayNode?
private var pictureInPictureContent: PictureInPictureContent?
public init() { public init() {
super.init(navigationBarPresentationData: nil) super.init(navigationBarPresentationData: nil)
@ -44,6 +48,31 @@ public final class OverlayMediaControllerImpl: ViewController, OverlayMediaContr
public func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool) { public func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool) {
self.controllerNode.removeNode(node, customTransition: customTransition) self.controllerNode.removeNode(node, customTransition: customTransition)
} }
public func setPictureInPictureContent(content: PictureInPictureContent, absoluteRect: CGRect) {
if self.pictureInPictureContainer == nil {
let pictureInPictureContainer = ASDisplayNode()
pictureInPictureContainer.clipsToBounds = false
self.pictureInPictureContainer = pictureInPictureContainer
self.controllerNode.addSubnode(pictureInPictureContainer)
}
self.pictureInPictureContainer?.clipsToBounds = false
self.pictureInPictureContent = content
self.pictureInPictureContainer?.addSubnode(content.videoNode)
}
public func setPictureInPictureContentHidden(content: PictureInPictureContent, isHidden value: Bool) {
if self.pictureInPictureContent === content {
self.pictureInPictureContainer?.clipsToBounds = value
}
}
public func removePictureInPictureContent(content: PictureInPictureContent) {
if self.pictureInPictureContent === content {
self.pictureInPictureContent = nil
content.videoNode.removeFromSupernode()
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition) super.containerLayoutUpdated(layout, transition: transition)

View File

@ -454,4 +454,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
func notifyPlaybackControlsHidden(_ hidden: Bool) { func notifyPlaybackControlsHidden(_ hidden: Bool) {
} }
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
self.playerNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy)
}
} }

View File

@ -7,8 +7,9 @@ import TelegramCore
import Postbox import Postbox
import TelegramAudio import TelegramAudio
import AccountContext import AccountContext
import AVKit
public final class OverlayUniversalVideoNode: OverlayMediaItemNode { public final class OverlayUniversalVideoNode: OverlayMediaItemNode, AVPictureInPictureSampleBufferPlaybackDelegate {
public let content: UniversalVideoContent public let content: UniversalVideoContent
private let videoNode: UniversalVideoNode private let videoNode: UniversalVideoNode
private let decoration: OverlayVideoDecoration private let decoration: OverlayVideoDecoration
@ -179,4 +180,35 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
public func controlPlay() { public func controlPlay() {
self.videoNode.play() self.videoNode.play()
} }
@available(iOSApplicationExtension 15.0, iOS 15.0, *)
override public func makeNativeContentSource() -> AVPictureInPictureController.ContentSource? {
guard let videoLayer = self.videoNode.getVideoLayer() else {
return nil
}
return AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoLayer, playbackDelegate: self)
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) {
self.controlPlay()
}
public func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange {
return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: 10.0, preferredTimescale: CMTimeScale(30.0)))
}
public func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
return false
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) {
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) {
completionHandler()
}
public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
return false
}
} }

View File

@ -450,4 +450,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
func notifyPlaybackControlsHidden(_ hidden: Bool) { func notifyPlaybackControlsHidden(_ hidden: Bool) {
} }
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
}
} }

View File

@ -289,5 +289,8 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
func notifyPlaybackControlsHidden(_ hidden: Bool) { func notifyPlaybackControlsHidden(_ hidden: Bool) {
} }
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
}
} }

View File

@ -224,4 +224,7 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
self.webView.isUserInteractionEnabled = !hidden self.webView.isUserInteractionEnabled = !hidden
} }
} }
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
}
} }

View File

@ -188,4 +188,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
func notifyPlaybackControlsHidden(_ hidden: Bool) { func notifyPlaybackControlsHidden(_ hidden: Bool) {
self.playerNode.notifyPlaybackControlsHidden(hidden) self.playerNode.notifyPlaybackControlsHidden(hidden)
} }
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
}
} }