import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import TelegramAudio import LegacyComponents import UniversalMediaPlayer import AccountContext import PhotoResources import RangeSet import CoreMedia import AVFoundation public final class SystemVideoContent: UniversalVideoContent { public let id: AnyHashable let userLocation: MediaResourceUserLocation let url: String let imageReference: ImageMediaReference public let dimensions: CGSize public let duration: Double public init(userLocation: MediaResourceUserLocation, url: String, imageReference: ImageMediaReference, dimensions: CGSize, duration: Double) { self.id = AnyHashable(url) self.url = url self.userLocation = userLocation self.imageReference = imageReference self.dimensions = dimensions self.duration = duration } public func makeContentNode(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { return SystemVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, url: self.url, imageReference: self.imageReference, intrinsicDimensions: self.dimensions, approximateDuration: self.duration) } } private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContentNode { private let url: String private let intrinsicDimensions: CGSize private let approximateDuration: Double private let audioSessionManager: ManagedAudioSession private let audioSessionDisposable = MetaDisposable() private var hasAudioSession = false private let playbackCompletedListeners = Bag<() -> Void>() private var initializedStatus = false private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .buffering(initial: true, whilePlaying: false, progress: 0.0, display: true), soundEnabled: true) private var isBuffering = true private let _status = ValuePromise() var status: Signal { return self._status.get() } private let _bufferingStatus = Promise<(RangeSet, Int64)?>() var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { return self._bufferingStatus.get() } var isNativePictureInPictureActive: Signal { return .single(false) } private let _ready = Promise() var ready: Signal { return self._ready.get() } private let _preloadCompleted = ValuePromise() var preloadCompleted: Signal { return self._preloadCompleted.get() } private let imageNode: TransformImageNode private let playerItem: AVPlayerItem private let player: AVPlayer private let playerNode: ASDisplayNode private var loadProgressDisposable: Disposable? private var statusDisposable: Disposable? private var didBeginPlaying = false private var didPlayToEndTimeObserver: NSObjectProtocol? private var timeObserver: Any? private var seekId: Int = 0 init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, url: String, imageReference: ImageMediaReference, intrinsicDimensions: CGSize, approximateDuration: Double) { self.audioSessionManager = audioSessionManager self.url = url self.intrinsicDimensions = intrinsicDimensions self.approximateDuration = approximateDuration self.imageNode = TransformImageNode() self.playerItem = AVPlayerItem(url: URL(string: url)!) let player = AVPlayer(playerItem: self.playerItem) self.player = player self.playerNode = ASDisplayNode() self.playerNode.setLayerBlock({ return AVPlayerLayer(player: player) }) self.playerNode.frame = CGRect(origin: CGPoint(), size: intrinsicDimensions) self.isBuffering = true super.init() self.imageNode.setSignal(chatMessagePhoto(postbox: postbox, userLocation: userLocation, photoReference: imageReference)) self.addSubnode(self.imageNode) self.addSubnode(self.playerNode) self.player.actionAtItemEnd = .pause self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: nil, using: { [weak self] notification in if let strongSelf = self { strongSelf.player.seek(to: CMTime(seconds: 0.0, preferredTimescale: 30)) strongSelf.play() } }) self.imageNode.imageUpdated = { [weak self] _ in self?._ready.set(.single(Void())) } self.player.addObserver(self, forKeyPath: "rate", options: [], context: nil) self.playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil) self.playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil) self.playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil) self._bufferingStatus.set(.single(nil)) self._status.set(self.statusValue) self.timeObserver = self.player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self] time in guard let strongSelf = self else { return } strongSelf.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: strongSelf.statusValue.duration, dimensions: CGSize(), timestamp: CMTimeGetSeconds(time), baseRate: 1.0, seekId: strongSelf.seekId, status: strongSelf.statusValue.status, soundEnabled: true) strongSelf._status.set(strongSelf.statusValue) } } deinit { if let timeObserver = self.timeObserver { self.player.removeTimeObserver(timeObserver) } self.player.removeObserver(self, forKeyPath: "rate") self.playerItem.removeObserver(self, forKeyPath: "playbackBufferEmpty") self.playerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") self.playerItem.removeObserver(self, forKeyPath: "playbackBufferFull") self.audioSessionDisposable.dispose() self.loadProgressDisposable?.dispose() self.statusDisposable?.dispose() if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) } } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { let duration: Double if let currentItem = self.player.currentItem { duration = CMTimeGetSeconds(currentItem.duration) } else { duration = self.approximateDuration } if keyPath == "rate" { let isPlaying = !self.player.rate.isZero let status: MediaPlayerPlaybackStatus if self.isBuffering { status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true) } else { status = isPlaying ? .playing : .paused } self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: duration, dimensions: CGSize(), timestamp: self.statusValue.timestamp, baseRate: 1.0, seekId: self.seekId, status: status, soundEnabled: true) self._status.set(self.statusValue) } else if keyPath == "playbackBufferEmpty" { let isPlaying = !self.player.rate.isZero let status: MediaPlayerPlaybackStatus self.isBuffering = true if self.isBuffering { status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true) } else { status = isPlaying ? .playing : .paused } self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: duration, dimensions: CGSize(), timestamp: self.statusValue.timestamp, baseRate: 1.0, seekId: self.seekId, status: status, soundEnabled: true) self._status.set(self.statusValue) } else if keyPath == "playbackLikelyToKeepUp" || keyPath == "playbackBufferFull" { let isPlaying = !self.player.rate.isZero let status: MediaPlayerPlaybackStatus self.isBuffering = false if self.isBuffering { status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true) } else { status = isPlaying ? .playing : .paused } self.statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: duration, dimensions: CGSize(), timestamp: self.statusValue.timestamp, baseRate: 1.0, seekId: self.seekId, status: status, soundEnabled: true) self._status.set(self.statusValue) if !self.didBeginPlaying { self.didBeginPlaying = true self.play() } } } func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) { transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width) transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size)) let makeImageLayout = self.imageNode.asyncLayout() let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets())) applyImageLayout() } func play() { assert(Queue.mainQueue().isCurrent()) if !self.initializedStatus { self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: self.approximateDuration, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)) } if !self.hasAudioSession { self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in self?.hasAudioSession = true self?.player.play() }, deactivate: { [weak self] _ in self?.hasAudioSession = false self?.player.pause() return .complete() })) } else { self.player.play() } } func pause() { assert(Queue.mainQueue().isCurrent()) if !self.initializedStatus { self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: self.approximateDuration, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: self.seekId, status: .paused, soundEnabled: true)) } self.player.pause() } func togglePlayPause() { assert(Queue.mainQueue().isCurrent()) if self.player.rate.isZero { self.play() } else { self.pause() } } func setSoundEnabled(_ value: Bool) { assert(Queue.mainQueue().isCurrent()) } func seek(_ timestamp: Double) { assert(Queue.mainQueue().isCurrent()) self.seekId += 1 self.playerItem.seek(to: CMTimeMake(value: Int64(timestamp) * 1000, timescale: 1000), completionHandler: nil) } func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } func setSoundMuted(soundMuted: Bool) { } func continueWithOverridingAmbientMode(isAmbient: Bool) { } func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { } func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { } func setBaseRate(_ baseRate: Double) { } func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { } func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { return nil } func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { return .single(nil) } func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } func removePlaybackCompleted(_ index: Int) { self.playbackCompletedListeners.remove(index) } func fetchControl(_ control: UniversalVideoNodeFetchControl) { } func notifyPlaybackControlsHidden(_ hidden: Bool) { } func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) { } func enterNativePictureInPicture() -> Bool { return false } func exitNativePictureInPicture() { } func setNativePictureInPictureIsActive(_ value: Bool) { } }