import Foundation import UIKit import AsyncDisplayKit import SwiftSignalKit import Display import TelegramCore import Postbox import TelegramAudio import AccountContext import AVKit import UniversalMediaPlayer public final class OverlayUniversalVideoNode: OverlayMediaItemNode, AVPictureInPictureSampleBufferPlaybackDelegate { public let content: UniversalVideoContent private let videoNode: UniversalVideoNode private let decoration: OverlayVideoDecoration private var validLayoutSize: CGSize? private var shouldBeDismissedDisposable: Disposable? override public var group: OverlayMediaItemNodeGroup? { return OverlayMediaItemNodeGroup(rawValue: 0) } override public var isMinimizeable: Bool { return true } public var canAttachContent: Bool = true { didSet { self.videoNode.canAttachContent = self.canAttachContent } } private let defaultExpand: () -> Void public var customExpand: (() -> Void)? public var customClose: (() -> Void)? public var controlsAreShowingUpdated: ((Bool) -> Void)? private var statusDisposable: Disposable? private var status: MediaPlayerStatus? public init(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, content: UniversalVideoContent, shouldBeDismissed: Signal = .single(false), expand: @escaping () -> Void, close: @escaping () -> Void) { self.content = content self.defaultExpand = expand var expandImpl: (() -> Void)? var controlsAreShowingUpdatedImpl: ((Bool) -> Void)? var unminimizeImpl: (() -> Void)? var togglePlayPauseImpl: (() -> Void)? var closeImpl: (() -> Void)? let decoration = OverlayVideoDecoration(contentDimensions: content.dimensions, unminimize: { unminimizeImpl?() }, togglePlayPause: { togglePlayPauseImpl?() }, expand: { expandImpl?() }, close: { closeImpl?() }, controlsAreShowingUpdated: { value in controlsAreShowingUpdatedImpl?(value) }) self.videoNode = UniversalVideoNode(context: context, postbox: postbox, audioSession: audioSession, manager: manager, decoration: decoration, content: content, priority: .overlay) self.decoration = decoration super.init() expandImpl = { [weak self] in self?.expand() } unminimizeImpl = { [weak self] in self?.unminimize?() } togglePlayPauseImpl = { [weak self] in self?.videoNode.togglePlayPause() } closeImpl = { [weak self] in if let strongSelf = self { if let customClose = strongSelf.customClose { customClose() return } if strongSelf.videoNode.hasAttachedContext { strongSelf.videoNode.continuePlayingWithoutSound() } strongSelf.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false, completion: { _ in self?.dismiss() close() }) strongSelf.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) } } controlsAreShowingUpdatedImpl = { [weak self] value in self?.controlsAreShowingUpdated?(value) } self.clipsToBounds = true self.cornerRadius = 4.0 self.addSubnode(self.videoNode) self.videoNode.ownsContentNodeUpdated = { [weak self] value in if let strongSelf = self { let previous = strongSelf.hasAttachedContext strongSelf.hasAttachedContext = value strongSelf.hasAttachedContextUpdated?(value) if previous != value { if !value { strongSelf.dismiss() closeImpl?() } } } } self.videoNode.canAttachContent = true self.shouldBeDismissedDisposable = (shouldBeDismissed |> filter { $0 } |> deliverOnMainQueue).start(next: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.dismiss() closeImpl?() }) self.statusDisposable = (self.videoNode.status |> deliverOnMainQueue).start(next: { [weak self] status in self?.status = status }) } deinit { self.shouldBeDismissedDisposable?.dispose() self.statusDisposable?.dispose() } override public func didLoad() { super.didLoad() } override public func layout() { self.updateLayout(self.bounds.size) } override public func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize { return self.content.dimensions.aspectFitted(CGSize(width: 300.0, height: 300.0)) } override public func updateLayout(_ size: CGSize) { if size != self.validLayoutSize { self.updateLayoutImpl(size, transition: .immediate) } } public func updateLayout(_ size: CGSize, transition: ContainedViewLayoutTransition) { if size != self.validLayoutSize { self.updateLayoutImpl(size, transition: transition) } } private func updateLayoutImpl(_ size: CGSize, transition: ContainedViewLayoutTransition) { self.validLayoutSize = size transition.updateFrame(node: self.videoNode, frame: CGRect(origin: CGPoint(), size: size)) self.videoNode.updateLayout(size: size, transition: transition) } override public func updateMinimizedEdge(_ edge: OverlayMediaItemMinimizationEdge?, adjusting: Bool) { self.decoration.updateMinimizedEdge(edge, adjusting: adjusting) } public func updateRoundCorners(_ value: Bool, transition: ContainedViewLayoutTransition) { transition.updateCornerRadius(node: self, cornerRadius: value ? 4.0 : 0.0) } public func showControls() { self.decoration.showControls() } public func expand() { if let customExpand = self.customExpand { customExpand() } else { self.defaultExpand() } } 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 { 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 { 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 } }