import Foundation import UIKit import AsyncDisplayKit import Postbox import SwiftSignalKit import TelegramCore import Display import TelegramAudio import UniversalMediaPlayer public protocol UniversalVideoContentNode: class { var ready: Signal { get } var status: Signal { get } var bufferingStatus: Signal<(IndexSet, Int)?, NoError> { get } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) func play() func pause() func togglePlayPause() func setSoundEnabled(_ value: Bool) func seek(_ timestamp: Double) func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) func setBaseRate(_ baseRate: Double) func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int func removePlaybackCompleted(_ index: Int) func fetchControl(_ control: UniversalVideoNodeFetchControl) func notifyPlaybackControlsHidden(_ hidden: Bool) } public protocol UniversalVideoContent { var id: AnyHashable { get } var dimensions: CGSize { get } var duration: Int32 { get } func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode func isEqual(to other: UniversalVideoContent) -> Bool } public extension UniversalVideoContent { func isEqual(to other: UniversalVideoContent) -> Bool { return false } } public protocol UniversalVideoDecoration: class { var backgroundNode: ASDisplayNode? { get } var contentContainerNode: ASDisplayNode { get } var foregroundNode: ASDisplayNode? { get } func setStatus(_ status: Signal) func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) func updateContentNodeSnapshot(_ snapshot: UIView?) func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) func tap() } public enum UniversalVideoPriority: Int32, Comparable { case secondaryOverlay = 0 case embedded = 1 case gallery = 2 case overlay = 3 public static func <(lhs: UniversalVideoPriority, rhs: UniversalVideoPriority) -> Bool { return lhs.rawValue < rhs.rawValue } } public enum UniversalVideoNodeFetchControl { case fetch case cancel } public final class UniversalVideoNode: ASDisplayNode { private let postbox: Postbox private let audioSession: ManagedAudioSession private let manager: UniversalVideoManager private let content: UniversalVideoContent private let priority: UniversalVideoPriority public let decoration: UniversalVideoDecoration private let autoplay: Bool private let snapshotContentWhenGone: Bool private var contentNode: (UniversalVideoContentNode & ASDisplayNode)? private var contentNodeId: Int32? private var playbackCompletedIndex: Int? private var contentRequestIndex: (AnyHashable, Int32)? public var playbackCompleted: (() -> Void)? public private(set) var ownsContentNode: Bool = false public var ownsContentNodeUpdated: ((Bool) -> Void)? private let _status = Promise() public var status: Signal { return self._status.get() } private let _bufferingStatus = Promise<(IndexSet, Int)?>() public var bufferingStatus: Signal<(IndexSet, Int)?, NoError> { return self._bufferingStatus.get() } private let _ready = Promise() public var ready: Signal { return self._ready.get() } public var canAttachContent: Bool = false { didSet { if self.canAttachContent != oldValue { if self.canAttachContent { assert(self.contentRequestIndex == nil) let content = self.content let postbox = self.postbox let audioSession = self.audioSession self.contentRequestIndex = self.manager.attachUniversalVideoContent(content: self.content, priority: self.priority, create: { return content.makeContentNode(postbox: postbox, audioSession: audioSession) }, update: { [weak self] contentNodeAndFlags in if let strongSelf = self { strongSelf.updateContentNode(contentNodeAndFlags) } }) } else { assert(self.contentRequestIndex != nil) if let (id, index) = self.contentRequestIndex { self.contentRequestIndex = nil self.manager.detachUniversalVideoContent(id: id, index: index) } } } } } public var hasAttachedContext: Bool { return self.contentNode != nil } public init(postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) { self.postbox = postbox self.audioSession = audioSession self.manager = manager self.content = content self.priority = priority self.decoration = decoration self.autoplay = autoplay self.snapshotContentWhenGone = snapshotContentWhenGone super.init() self.playbackCompletedIndex = self.manager.addPlaybackCompleted(id: self.content.id, { [weak self] in self?.playbackCompleted?() }) self._status.set(self.manager.statusSignal(content: self.content)) self._bufferingStatus.set(self.manager.bufferingStatusSignal(content: self.content)) self.decoration.setStatus(self.status) if let backgroundNode = self.decoration.backgroundNode { self.addSubnode(backgroundNode) } self.addSubnode(self.decoration.contentContainerNode) if let foregroundNode = self.decoration.foregroundNode { self.addSubnode(foregroundNode) } } override public func didLoad() { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } deinit { assert(Queue.mainQueue().isCurrent()) if let playbackCompletedIndex = self.playbackCompletedIndex { self.manager.removePlaybackCompleted(id: self.content.id, index: playbackCompletedIndex) } if let (id, index) = self.contentRequestIndex { self.contentRequestIndex = nil self.manager.detachUniversalVideoContent(id: id, index: index) } } private func updateContentNode(_ contentNode: ((UniversalVideoContentNode & ASDisplayNode), Bool)?) { let previous = self.contentNode self.contentNode = contentNode?.0 if previous !== contentNode?.0 { if let previous = previous, contentNode?.0 == nil && self.snapshotContentWhenGone { if let snapshotView = previous.view.snapshotView(afterScreenUpdates: false) { self.decoration.updateContentNodeSnapshot(snapshotView) } } if let (contentNode, initiatedCreation) = contentNode { contentNode.layer.removeAllAnimations() self._ready.set(contentNode.ready) if initiatedCreation && self.autoplay { self.play() } } if contentNode?.0 != nil && self.snapshotContentWhenGone { self.decoration.updateContentNodeSnapshot(nil) } self.decoration.updateContentNode(contentNode?.0) let ownsContentNode = contentNode?.0 !== nil if self.ownsContentNode != ownsContentNode { self.ownsContentNode = ownsContentNode self.ownsContentNodeUpdated?(ownsContentNode) } } if contentNode == nil { self._ready.set(.single(Void())) } } public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { self.decoration.updateLayout(size: size, transition: transition) } public func play() { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.play() } }) } public func pause() { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.pause() } }) } public func togglePlayPause() { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.togglePlayPause() } }) } public func setSoundEnabled(_ value: Bool) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.setSoundEnabled(value) } }) } public func seek(_ timestamp: Double) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.seek(timestamp) } }) } public func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.playOnceWithSound(playAndRecord: playAndRecord, seek: seek, actionAtEnd: actionAtEnd) } }) } public func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.setContinuePlayingWithoutSoundOnLostAudioSession(value) } }) } public func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.setForceAudioToSpeaker(forceAudioToSpeaker) } }) } public func setBaseRate(_ baseRate: Double) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.setBaseRate(baseRate) } }) } public func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.continuePlayingWithoutSound(actionAtEnd: actionAtEnd) } }) } public func fetchControl(_ control: UniversalVideoNodeFetchControl) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.fetchControl(control) } }) } public func notifyPlaybackControlsHidden(_ hidden: Bool) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { contentNode.notifyPlaybackControlsHidden(hidden) } }) } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.decoration.tap() } } }