mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Native picture in picture support
This commit is contained in:
parent
cf624bcad3
commit
17c84b85a0
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -7,6 +7,7 @@ import TelegramCore
|
||||
import Display
|
||||
import TelegramAudio
|
||||
import UniversalMediaPlayer
|
||||
import AVFoundation
|
||||
|
||||
public protocol UniversalVideoContentNode: AnyObject {
|
||||
var ready: Signal<Void, NoError> { 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
@ -532,6 +704,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
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
|
||||
self.presentationData = presentationData
|
||||
@ -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,9 +2040,65 @@ 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 playbackRate = self.playbackRate
|
||||
|
||||
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<ChatLocationContextHolder?>(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: { id, media in
|
||||
return nil
|
||||
}))
|
||||
|
||||
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
|
||||
@ -1962,7 +2192,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
context.sharedContext.mediaManager.setOverlayVideoNode(overlayNode)
|
||||
if overlayNode.supernode != nil {
|
||||
self.beginCustomDismiss()
|
||||
self.beginCustomDismiss(false)
|
||||
self.statusNode.isHidden = true
|
||||
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
|
||||
self?.completeCustomDismiss()
|
||||
@ -1970,6 +2200,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func contentInfo() -> (message: Message, file: TelegramMediaFile?, isWebpage: Bool)? {
|
||||
guard let item = self.item else {
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import AccountContext
|
||||
import AVKit
|
||||
|
||||
public final class OverlayMediaControllerImpl: ViewController, OverlayMediaController {
|
||||
private var controllerNode: OverlayMediaControllerNode {
|
||||
@ -14,6 +15,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)
|
||||
|
||||
@ -45,6 +49,31 @@ public final class OverlayMediaControllerImpl: ViewController, OverlayMediaContr
|
||||
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)
|
||||
|
||||
|
@ -454,4 +454,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
|
||||
func notifyPlaybackControlsHidden(_ hidden: Bool) {
|
||||
}
|
||||
|
||||
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
|
||||
self.playerNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -450,4 +450,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
|
||||
|
||||
func notifyPlaybackControlsHidden(_ hidden: Bool) {
|
||||
}
|
||||
|
||||
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
|
||||
}
|
||||
}
|
||||
|
@ -289,5 +289,8 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
||||
|
||||
func notifyPlaybackControlsHidden(_ hidden: Bool) {
|
||||
}
|
||||
|
||||
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,4 +224,7 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
|
||||
self.webView.isUserInteractionEnabled = !hidden
|
||||
}
|
||||
}
|
||||
|
||||
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
|
||||
}
|
||||
}
|
||||
|
@ -188,4 +188,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
|
||||
func notifyPlaybackControlsHidden(_ hidden: Bool) {
|
||||
self.playerNode.notifyPlaybackControlsHidden(hidden)
|
||||
}
|
||||
|
||||
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user