diff --git a/submodules/AccountContext/Sources/OverlayMediaItemNode.swift b/submodules/AccountContext/Sources/OverlayMediaItemNode.swift index a635e5cca7..9ac807663a 100644 --- a/submodules/AccountContext/Sources/OverlayMediaItemNode.swift +++ b/submodules/AccountContext/Sources/OverlayMediaItemNode.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import AsyncDisplayKit +import AVKit public struct OverlayMediaItemNodeGroup: Hashable, RawRepresentable { public var rawValue: Int32 @@ -61,4 +62,9 @@ open class OverlayMediaItemNode: ASDisplayNode { open func performCustomTransitionOut() -> Bool { return false } + + @available(iOSApplicationExtension 15.0, iOS 15.0, *) + open func makeNativeContentSource() -> AVPictureInPictureController.ContentSource? { + return nil + } } diff --git a/submodules/AccountContext/Sources/OverlayMediaManager.swift b/submodules/AccountContext/Sources/OverlayMediaManager.swift index e5f860c704..8352db2673 100644 --- a/submodules/AccountContext/Sources/OverlayMediaManager.swift +++ b/submodules/AccountContext/Sources/OverlayMediaManager.swift @@ -1,6 +1,8 @@ import Foundation import UIKit import Display +import AVFoundation +import AsyncDisplayKit public final class OverlayMediaControllerEmbeddingItem { public let position: CGPoint @@ -15,6 +17,10 @@ public final class OverlayMediaControllerEmbeddingItem { } } +public protocol PictureInPictureContent: AnyObject { + var videoNode: ASDisplayNode { get } +} + public protocol OverlayMediaController: AnyObject { var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)? { get set } var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)? { get set } @@ -22,6 +28,10 @@ public protocol OverlayMediaController: AnyObject { var hasNodes: Bool { get } func addNode(_ 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 { diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index 0c7e9aa193..564f1b1852 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -7,6 +7,7 @@ import TelegramCore import Display import TelegramAudio import UniversalMediaPlayer +import AVFoundation public protocol UniversalVideoContentNode: AnyObject { var ready: Signal { get } @@ -29,6 +30,7 @@ public protocol UniversalVideoContentNode: AnyObject { func removePlaybackCompleted(_ index: Int) func fetchControl(_ control: UniversalVideoNodeFetchControl) func notifyPlaybackControlsHidden(_ hidden: Bool) + func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) } public protocol UniversalVideoContent { @@ -332,4 +334,36 @@ public final class UniversalVideoNode: ASDisplayNode { 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) + } + }) + } } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index e429632f09..207683625d 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -385,6 +385,8 @@ public class GalleryController: ViewController, StandalonePresentableController private var screenCaptureEventsDisposable: Disposable? public var centralItemUpdated: ((MessageId) -> Void)? + public var onDidAppear: (() -> Void)? + public var useSimpleAnimation: Bool = false private var initialOrientation: UIInterfaceOrientation? @@ -1037,11 +1039,11 @@ public class GalleryController: ViewController, StandalonePresentableController self?.presentingViewController?.dismiss(animated: false, completion: nil) } - self.galleryNode.beginCustomDismiss = { [weak self] in + self.galleryNode.beginCustomDismiss = { [weak self] simpleAnimation in if let strongSelf = self { strongSelf._hiddenMedia.set(.single(nil)) - let animatedOutNode = true + let animatedOutNode = !simpleAnimation strongSelf.galleryNode.animateOut(animateContent: animatedOutNode, completion: { }) @@ -1296,13 +1298,15 @@ public class GalleryController: ViewController, StandalonePresentableController } centralItemNode.activateAsInitial() } + + self.onDidAppear?() } if !self.isPresentedInPreviewingContext() { self.galleryNode.setControlsHidden(self.landscape, animated: false) if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { if presentationArguments.animated { - self.galleryNode.animateIn(animateContent: !nodeAnimatesItself) + self.galleryNode.animateIn(animateContent: !nodeAnimatesItself && !self.useSimpleAnimation) } } } diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 9243fb7c4a..b4b96aa43c 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -19,7 +19,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture public var scrollView: UIScrollView public var pager: GalleryPagerNode - public var beginCustomDismiss: () -> Void = { } + public var beginCustomDismiss: (Bool) -> Void = { _ in } public var completeCustomDismiss: () -> Void = { } public var baseNavigationController: () -> NavigationController? = { 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 { - strongSelf.beginCustomDismiss() + strongSelf.beginCustomDismiss(simpleAnimation) } } @@ -328,13 +328,15 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture } open func animateIn(animateContent: Bool) { + let duration: Double = animateContent ? 0.2 : 0.3 + self.backgroundNode.backgroundColor = self.backgroundNode.backgroundColor?.withAlphaComponent(0.0) self.statusBar?.alpha = 0.0 self.navigationBar?.alpha = 0.0 self.footerNode.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) if !self.areControlsHidden { self.statusBar?.alpha = 1.0 @@ -346,6 +348,8 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture 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) + } 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 intermediateCompletion() }) + } else { + self.scrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + contentAnimationCompleted = true + intermediateCompletion() + }) } } diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index 2dc6cf9672..c777929084 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -24,7 +24,7 @@ open class GalleryItemNode: ASDisplayNode { public var updateControlsVisibility: (Bool) -> Void = { _ in } public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } - public var beginCustomDismiss: () -> Void = { } + public var beginCustomDismiss: (Bool) -> Void = { _ in } public var completeCustomDismiss: () -> Void = { } public var baseNavigationController: () -> NavigationController? = { return nil } public var galleryController: () -> ViewController? = { return nil } diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index bc6f4c86a5..cbb6e27361 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -109,7 +109,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest public var updateControlsVisibility: (Bool) -> Void = { _ in } public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } - public var beginCustomDismiss: () -> Void = { } + public var beginCustomDismiss: (Bool) -> Void = { _ in } public var completeCustomDismiss: () -> Void = { } public var baseNavigationController: () -> NavigationController? = { return nil } public var galleryController: () -> ViewController? = { return nil } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index b8bbf7064b..65c065597e 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -20,6 +20,7 @@ import SaveToCameraRoll import UndoUI import TelegramUIPreferences import OpenInExternalAppUI +import AVKit public enum UniversalVideoGalleryItemContentInfo { 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 { private let context: AccountContext private let presentationData: PresentationData @@ -485,6 +656,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var videoNodeUserInteractionEnabled: Bool = false private var videoFramePreview: FramePreview? private var pictureInPictureNode: UniversalVideoGalleryItemPictureInPictureNode? + private var disablePictureInPicturePlaceholder: Bool = false private let statusButtonNode: HighlightableButtonNode private let statusNode: RadialStatusNode private var statusNodeShouldBeHidden = true @@ -531,6 +703,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var playbackCompleted: (() -> Void)? 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) { self.context = context @@ -1209,7 +1383,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } private func updateDisplayPlaceholder(_ displayPlaceholder: Bool) { - if displayPlaceholder { + if displayPlaceholder && !self.disablePictureInPicturePlaceholder { if self.pictureInPictureNode == nil { let pictureInPictureNode = UniversalVideoGalleryItemPictureInPictureNode(strings: self.presentationData.strings) pictureInPictureNode.isUserInteractionEnabled = false @@ -1856,7 +2030,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } if customUnembedWhenPortrait(overlayNode) { - self.beginCustomDismiss() + self.beginCustomDismiss(false) self.statusNode.isHidden = true self.animateOut(toOverlay: overlayNode, completion: { [weak self] in self?.completeCustomDismiss() @@ -1866,80 +2040,114 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } @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) - + let context = self.context let baseNavigationController = self.baseNavigationController() - let mediaManager = self.context.sharedContext.mediaManager - var expandImpl: (() -> Void)? - - let shouldBeDismissed: Signal - 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 { + if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() { + self.disablePictureInPicturePlaceholder = true + + 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) + 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): - let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic(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(value: nil)), playbackRate: playbackRate, replaceRootController: { [weak baseNavigationController] controller, ready in if let baseNavigationController = baseNavigationController { baseNavigationController.replaceTopController(controller, animated: false, ready: ready) } }, baseNavigationController: baseNavigationController) gallery.temporaryDoNotWaitForReady = true - + gallery.useSimpleAnimation = 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 { - 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) - } + + (baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { id, media in return nil })) - case let .webPage(_, _, expandFromPip): - if let expandFromPip = expandFromPip, let baseNavigationController = baseNavigationController { - expandFromPip({ [weak overlayNode] in + + gallery.onDidAppear = { + 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 + 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(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 { return GalleryTransitionArguments(transitionNode: (overlayNode, overlayNode.bounds, { [weak overlayNode] in return (overlayNode?.view.snapshotContentTree(), nil) @@ -1952,21 +2160,44 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } 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 - }, 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) - if overlayNode.supernode != nil { - self.beginCustomDismiss() - self.statusNode.isHidden = true - self.animateOut(toOverlay: overlayNode, completion: { [weak self] in - self?.completeCustomDismiss() - }) + context.sharedContext.mediaManager.setOverlayVideoNode(overlayNode) + if overlayNode.supernode != nil { + self.beginCustomDismiss(false) + self.statusNode.isHidden = true + self.animateOut(toOverlay: overlayNode, completion: { [weak self] in + self?.completeCustomDismiss() + }) + } } } } diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index de83c7efb2..82a41cf3f5 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -232,7 +232,7 @@ public final class SecretMediaPreviewController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) } - self.controllerNode.beginCustomDismiss = { [weak self] in + self.controllerNode.beginCustomDismiss = { [weak self] _ in if let strongSelf = self { strongSelf._hiddenMedia.set(.single(nil)) diff --git a/submodules/MediaPlayer/Sources/MediaPlayerNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerNode.swift index 7a4d8f5102..11cb7f2949 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerNode.swift @@ -61,6 +61,7 @@ private enum PollStatus: CustomStringConvertible { public final class MediaPlayerNode: ASDisplayNode { public var videoInHierarchy: Bool = false + var canPlaybackWithoutHierarchy: Bool = false public var updateVideoInHierarchy: ((Bool) -> Void)? private var videoNode: MediaPlayerNodeDisplayNode @@ -117,7 +118,7 @@ public final class MediaPlayerNode: ASDisplayNode { videoLayer.setAffineTransform(transform) } - if self.videoInHierarchy { + if self.videoInHierarchy || self.canPlaybackWithoutHierarchy { if requestFrames { self.startPolling() } @@ -137,7 +138,7 @@ public final class MediaPlayerNode: ASDisplayNode { switch status { case let .delay(delay): 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 { strongSelf.timer?.invalidate() strongSelf.timer = nil @@ -385,7 +386,7 @@ public final class MediaPlayerNode: ASDisplayNode { strongSelf.updateState() } } - strongSelf.updateVideoInHierarchy?(value) + strongSelf.updateVideoInHierarchy?(strongSelf.videoInHierarchy || strongSelf.canPlaybackWithoutHierarchy) } } self.addSubnode(self.videoNode) @@ -458,4 +459,14 @@ public final class MediaPlayerNode: ASDisplayNode { public func reset() { 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) + } } diff --git a/submodules/TelegramUI/Sources/OverlayMediaController.swift b/submodules/TelegramUI/Sources/OverlayMediaController.swift index 6cf0b311d1..cbed0ab07e 100644 --- a/submodules/TelegramUI/Sources/OverlayMediaController.swift +++ b/submodules/TelegramUI/Sources/OverlayMediaController.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import SwiftSignalKit import Postbox import AccountContext +import AVKit public final class OverlayMediaControllerImpl: ViewController, OverlayMediaController { private var controllerNode: OverlayMediaControllerNode { @@ -13,6 +14,9 @@ public final class OverlayMediaControllerImpl: ViewController, OverlayMediaContr public var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)? public var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)? + + private var pictureInPictureContainer: ASDisplayNode? + private var pictureInPictureContent: PictureInPictureContent? public init() { super.init(navigationBarPresentationData: nil) @@ -44,6 +48,31 @@ public final class OverlayMediaControllerImpl: ViewController, OverlayMediaContr public func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool) { 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) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 812bab8a88..1ec861766b 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -454,4 +454,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func notifyPlaybackControlsHidden(_ hidden: Bool) { } + + func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { + self.playerNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy) + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift index d8cc1a0299..a4f0aa66e3 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/OverlayUniversalVideoNode.swift @@ -7,8 +7,9 @@ import TelegramCore import Postbox import TelegramAudio import AccountContext +import AVKit -public final class OverlayUniversalVideoNode: OverlayMediaItemNode { +public final class OverlayUniversalVideoNode: OverlayMediaItemNode, AVPictureInPictureSampleBufferPlaybackDelegate { public let content: UniversalVideoContent private let videoNode: UniversalVideoNode private let decoration: OverlayVideoDecoration @@ -179,4 +180,35 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode { public func controlPlay() { 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 + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index 7f9466a28f..da8d5b08d3 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -450,4 +450,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte func notifyPlaybackControlsHidden(_ hidden: Bool) { } + + func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift index 93a4a58af6..9c5ec1cf34 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift @@ -289,5 +289,8 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent func notifyPlaybackControlsHidden(_ hidden: Bool) { } + + func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift index b2dff524a4..7bc03d92d3 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift @@ -224,4 +224,7 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate { self.webView.isUserInteractionEnabled = !hidden } } + + func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index 18fa2fe598..bb8bceacaf 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -188,4 +188,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { func notifyPlaybackControlsHidden(_ hidden: Bool) { self.playerNode.notifyPlaybackControlsHidden(hidden) } + + func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { + } }