Swiftgram/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift
2020-09-21 20:54:32 +03:00

1563 lines
84 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import SyncCore
import Display
import Postbox
import TelegramPresentationData
import UniversalMediaPlayer
import AccountContext
import RadialStatusNode
import TelegramUniversalVideoContent
import PresentationDataUtils
import OverlayStatusController
import StickerPackPreviewUI
import AppBundle
public enum UniversalVideoGalleryItemContentInfo {
case message(Message)
case webPage(TelegramMediaWebpage, Media, ((@escaping () -> GalleryTransitionArguments?, NavigationController?, (ViewController, Any?) -> Void) -> Void)?)
}
public class UniversalVideoGalleryItem: GalleryItem {
public var id: AnyHashable {
return self.content.id
}
let context: AccountContext
let presentationData: PresentationData
let content: UniversalVideoContent
let originData: GalleryItemOriginData?
let indexData: GalleryItemIndexData?
let contentInfo: UniversalVideoGalleryItemContentInfo?
let caption: NSAttributedString
let credit: NSAttributedString?
let displayInfoOnTop: Bool
let hideControls: Bool
let fromPlayingVideo: Bool
let landscape: Bool
let timecode: Double?
let configuration: GalleryConfiguration?
let playbackCompleted: () -> Void
let performAction: (GalleryControllerInteractionTapAction) -> Void
let openActionOptions: (GalleryControllerInteractionTapAction) -> Void
let storeMediaPlaybackState: (MessageId, Double?) -> Void
let present: (ViewController, Any?) -> Void
public init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, displayInfoOnTop: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, configuration: GalleryConfiguration? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
self.content = content
self.originData = originData
self.indexData = indexData
self.contentInfo = contentInfo
self.caption = caption
self.credit = credit
self.displayInfoOnTop = displayInfoOnTop
self.hideControls = hideControls
self.fromPlayingVideo = fromPlayingVideo
self.landscape = landscape
self.timecode = timecode
self.configuration = configuration
self.playbackCompleted = playbackCompleted
self.performAction = performAction
self.openActionOptions = openActionOptions
self.storeMediaPlaybackState = storeMediaPlaybackState
self.present = present
}
public func node(synchronous: Bool) -> GalleryItemNode {
let node = UniversalVideoGalleryItemNode(context: self.context, presentationData: self.presentationData, performAction: self.performAction, openActionOptions: self.openActionOptions, present: self.present)
if let indexData = self.indexData {
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").0))
}
node.setupItem(self)
if self.displayInfoOnTop, case let .message(message) = self.contentInfo {
node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId)
}
return node
}
public func updateNode(node: GalleryItemNode, synchronous: Bool) {
if let node = node as? UniversalVideoGalleryItemNode {
if let indexData = self.indexData {
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").0))
}
node.setupItem(self)
if self.displayInfoOnTop, case let .message(message) = self.contentInfo {
node.titleContentView?.setMessage(message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId)
}
}
}
public func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
guard let contentInfo = self.contentInfo else {
return nil
}
if case let .message(message) = contentInfo {
if let id = message.groupInfo?.stableId {
var mediaReference: AnyMediaReference?
for m in message.media {
if let m = m as? TelegramMediaImage {
mediaReference = .message(message: MessageReference(message), media: m)
} else if let m = m as? TelegramMediaFile, m.isVideo {
mediaReference = .message(message: MessageReference(message), media: m)
}
}
if let mediaReference = mediaReference {
if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, mediaReference: mediaReference) {
return (Int64(id), item)
}
}
}
} else if case let .webPage(webPage, media, _) = contentInfo, let file = media as? TelegramMediaFile {
if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, mediaReference: .webPage(webPage: WebpageReference(webPage), media: file)) {
return (0, item)
}
}
return nil
}
}
private let pictureInPictureImage = UIImage(bundleImageName: "Media Gallery/PictureInPictureIcon")?.precomposed()
private let pictureInPictureButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PictureInPictureButton"), color: .white)
private let placeholderFont = Font.regular(16.0)
private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode {
private let iconNode: ASImageNode
private let textNode: ASTextNode
init(strings: PresentationStrings) {
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.iconNode.image = pictureInPictureImage
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: strings.Embed_PlayingInPIP, font: placeholderFont, textColor: UIColor(rgb: 0x8e8e93))
super.init()
self.addSubnode(self.iconNode)
self.addSubnode(self.textNode)
}
func updateLayout(_ size: CGSize, transition: ContainedViewLayoutTransition) {
let iconSize = self.iconNode.image?.size ?? CGSize()
let textSize = self.textNode.measure(CGSize(width: max(0.0, size.width - 20.0), height: CGFloat.greatestFiniteMagnitude))
let spacing: CGFloat = 10.0
let contentHeight = iconSize.height + spacing + textSize.height
let contentVerticalOrigin = floor((size.height - contentHeight) / 2.0)
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: contentVerticalOrigin), size: iconSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: contentVerticalOrigin + iconSize.height + spacing), size: textSize))
}
}
private let soundOnImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/SoundOn"), color: .white)
private let soundOffImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/SoundOff"), color: .white)
private var roundButtonBackgroundImage = {
return generateImage(CGSize(width: 42.0, height: 42), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor)
context.fillEllipse(in: bounds)
})
}()
private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentNode {
private let soundButtonNode: HighlightableButtonNode
private var validLayout: (CGSize, LayoutMetrics, CGFloat, CGFloat, CGFloat)?
override init() {
self.soundButtonNode = HighlightableButtonNode()
self.soundButtonNode.alpha = 0.0
self.soundButtonNode.setBackgroundImage(roundButtonBackgroundImage, for: .normal)
self.soundButtonNode.setImage(soundOffImage, for: .normal)
self.soundButtonNode.setImage(soundOnImage, for: .selected)
self.soundButtonNode.setImage(soundOnImage, for: [.selected, .highlighted])
super.init()
self.soundButtonNode.addTarget(self, action: #selector(self.soundButtonPressed), forControlEvents: .touchUpInside)
self.addSubnode(self.soundButtonNode)
}
func hide() {
self.soundButtonNode.isHidden = true
}
override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, metrics, leftInset, rightInset, bottomInset)
let soundButtonDiameter: CGFloat = 42.0
let inset: CGFloat = 12.0
let effectiveBottomInset = self.visibilityAlpha < 1.0 ? 0.0 : bottomInset
let soundButtonFrame = CGRect(origin: CGPoint(x: size.width - soundButtonDiameter - inset - rightInset, y: size.height - soundButtonDiameter - inset - effectiveBottomInset), size: CGSize(width: soundButtonDiameter, height: soundButtonDiameter))
transition.updateFrame(node: self.soundButtonNode, frame: soundButtonFrame)
}
override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.soundButtonNode, alpha: 1.0)
}
override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
transition.updateAlpha(node: self.soundButtonNode, alpha: 0.0)
}
override func setVisibilityAlpha(_ alpha: CGFloat) {
super.setVisibilityAlpha(alpha)
self.updateSoundButtonVisibility()
}
func updateSoundButtonVisibility() {
if self.soundButtonNode.isSelected {
self.soundButtonNode.alpha = self.visibilityAlpha
} else {
self.soundButtonNode.alpha = 1.0
}
if let validLayout = self.validLayout {
self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, transition: .animated(duration: 0.3, curve: .easeInOut))
}
}
@objc func soundButtonPressed() {
self.soundButtonNode.isSelected = !self.soundButtonNode.isSelected
self.updateSoundButtonVisibility()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.soundButtonNode.frame.contains(point) {
return nil
}
return super.hitTest(point, with: event)
}
}
private struct FetchControls {
let fetch: () -> Void
let cancel: () -> Void
}
final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext
private let presentationData: PresentationData
fileprivate let _ready = Promise<Void>()
fileprivate let _title = Promise<String>()
fileprivate let _titleView = Promise<UIView?>()
fileprivate let _rightBarButtonItems = Promise<[UIBarButtonItem]?>()
fileprivate var titleContentView: GalleryTitleView?
private let scrubberView: ChatVideoGalleryItemScrubberView
private let footerContentNode: ChatItemGalleryFooterContentNode
private let overlayContentNode: UniversalVideoGalleryItemOverlayNode
private var videoNode: UniversalVideoNode?
private var videoFramePreview: FramePreview?
private var pictureInPictureNode: UniversalVideoGalleryItemPictureInPictureNode?
private let statusButtonNode: HighlightableButtonNode
private let statusNode: RadialStatusNode
private var statusNodeShouldBeHidden = true
private var isCentral = false
private var _isVisible: Bool?
private var initiallyActivated = false
private var hideStatusNodeUntilCentrality = false
private var playOnContentOwnership = false
private var skipInitialPause = false
private var validLayout: (ContainerViewLayout, CGFloat)?
private var didPause = false
private var isPaused = true
private var dismissOnOrientationChange = false
private var keepSoundOnDismiss = false
private var hasPictureInPicture = false
private var requiresDownload = false
private var item: UniversalVideoGalleryItem?
private let statusDisposable = MetaDisposable()
private let mediaPlaybackStateDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
private var fetchStatus: MediaResourceStatus?
private var fetchControls: FetchControls?
private var scrubbingFrame = Promise<FramePreviewResult?>(nil)
private var scrubbingFrames = false
private var scrubbingFrameDisposable: Disposable?
var playbackCompleted: (() -> Void)?
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
self.scrubberView = ChatVideoGalleryItemScrubberView()
self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData, present: present)
self.footerContentNode.scrubberView = self.scrubberView
self.footerContentNode.performAction = performAction
self.footerContentNode.openActionOptions = openActionOptions
self.overlayContentNode = UniversalVideoGalleryItemOverlayNode()
self.statusButtonNode = HighlightableButtonNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
self.statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0))
self._title.set(.single(""))
super.init()
self.scrubberView.seek = { [weak self] timecode in
self?.videoNode?.seek(timecode)
}
self.scrubberView.updateScrubbing = { [weak self] timecode in
guard let strongSelf = self, let videoFramePreview = strongSelf.videoFramePreview else {
return
}
if let timecode = timecode {
if !strongSelf.scrubbingFrames {
strongSelf.scrubbingFrames = true
strongSelf.scrubbingFrame.set(videoFramePreview.generatedFrames
|> map(Optional.init))
}
videoFramePreview.generateFrame(at: timecode)
} else {
strongSelf.scrubbingFrame.set(.single(nil))
videoFramePreview.cancelPendingFrames()
strongSelf.scrubbingFrames = false
}
}
self.statusButtonNode.addSubnode(self.statusNode)
self.statusButtonNode.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside)
self.addSubnode(self.statusButtonNode)
self.footerContentNode.playbackControl = { [weak self] in
if let strongSelf = self {
if !strongSelf.isPaused {
strongSelf.didPause = true
}
strongSelf.videoNode?.togglePlayPause()
}
}
self.footerContentNode.seekBackward = { [weak self] in
if let strongSelf = self, let videoNode = strongSelf.videoNode {
let _ = (videoNode.status |> take(1)).start(next: { [weak videoNode] status in
if let strongVideoNode = videoNode, let timestamp = status?.timestamp {
strongVideoNode.seek(max(0.0, timestamp - 15.0))
}
})
}
}
self.footerContentNode.seekForward = { [weak self] in
if let strongSelf = self, let videoNode = strongSelf.videoNode {
let _ = (videoNode.status |> take(1)).start(next: { [weak videoNode] status in
if let strongVideoNode = videoNode, let timestamp = status?.timestamp, let duration = status?.duration {
let nextTimestamp = timestamp + 15.0
if nextTimestamp > duration {
strongVideoNode.seek(0.0)
strongVideoNode.pause()
} else {
strongVideoNode.seek(min(duration, timestamp + 15.0))
}
}
})
}
}
self.footerContentNode.fetchControl = { [weak self] in
guard let strongSelf = self, let fetchStatus = strongSelf.fetchStatus, let fetchControls = strongSelf.fetchControls else {
return
}
switch fetchStatus {
case .Fetching:
fetchControls.cancel()
case .Remote:
fetchControls.fetch()
case .Local:
break
}
}
self.scrubbingFrameDisposable = (self.scrubbingFrame.get()
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
if let result = result, strongSelf.scrubbingFrames {
switch result {
case .waitingForData:
strongSelf.footerContentNode.setFramePreviewImageIsLoading()
case let .image(image):
strongSelf.footerContentNode.setFramePreviewImage(image: image)
}
} else {
strongSelf.footerContentNode.setFramePreviewImage(image: nil)
}
})
self.alternativeDismiss = { [weak self] in
guard let strongSelf = self, strongSelf.hasPictureInPicture else {
return false
}
strongSelf.pictureInPictureButtonPressed()
return true
}
self.titleContentView = GalleryTitleView(frame: CGRect())
self._titleView.set(.single(self.titleContentView))
}
deinit {
self.statusDisposable.dispose()
self.mediaPlaybackStateDisposable.dispose()
self.scrubbingFrameDisposable?.dispose()
}
override func ready() -> Signal<Void, NoError> {
return self._ready.get()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
if let _ = self.customUnembedWhenPortrait, layout.size.width < layout.size.height {
self.expandIntoCustomPiP()
}
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
var dismiss = false
if let (previousLayout, _) = self.validLayout, self.dismissOnOrientationChange, previousLayout.size.width > previousLayout.size.height && previousLayout.size.height == layout.size.width {
dismiss = true
}
self.validLayout = (layout, navigationBarHeight)
let statusDiameter: CGFloat = 50.0
let statusFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusDiameter) / 2.0), y: floor((layout.size.height - statusDiameter) / 2.0)), size: CGSize(width: statusDiameter, height: statusDiameter))
transition.updateFrame(node: self.statusButtonNode, frame: statusFrame)
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusFrame.size))
if let pictureInPictureNode = self.pictureInPictureNode {
if let item = self.item {
let placeholderSize = item.content.dimensions.fitted(layout.size)
transition.updateFrame(node: pictureInPictureNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - placeholderSize.width) / 2.0), y: floor((layout.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
pictureInPictureNode.updateLayout(placeholderSize, transition: transition)
}
}
if dismiss {
self.dismiss()
}
}
private var controlsTimer: SwiftSignalKit.Timer?
private var previousPlaying: Bool?
private func setupControlsTimer() {
}
func setupItem(_ item: UniversalVideoGalleryItem) {
if self.item?.content.id != item.content.id {
self.previousPlaying = nil
if item.hideControls {
self.statusButtonNode.isHidden = true
}
self.dismissOnOrientationChange = item.landscape
var hasLinkedStickers = false
if let content = item.content as? NativeVideoContent {
hasLinkedStickers = content.fileReference.media.hasLinkedStickers
}
var disablePictureInPicture = false
var disablePlayerControls = false
var forceEnablePiP = false
var isAnimated = false
if let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
self.videoFramePreview = MediaPlayerFramePreview(postbox: item.context.account.postbox, fileReference: content.fileReference)
} else if let _ = item.content as? SystemVideoContent {
self._title.set(.single(item.presentationData.strings.Message_Video))
} else if let content = item.content as? WebEmbedVideoContent {
let type = webEmbedType(content: content.webpageContent)
switch type {
case .youtube:
disablePictureInPicture = !(item.configuration?.youtubePictureInPictureEnabled ?? false)
self.videoFramePreview = YoutubeEmbedFramePreview(context: item.context, content: content)
case .iframe:
disablePlayerControls = true
default:
break
}
} else if let _ = item.content as? PlatformVideoContent {
disablePlayerControls = true
forceEnablePiP = true
}
if let videoNode = self.videoNode {
videoNode.canAttachContent = false
videoNode.removeFromSupernode()
}
if isAnimated || disablePlayerControls {
self.footerContentNode.scrubberView = nil
}
let mediaManager = item.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery)
let videoSize = CGSize(width: item.content.dimensions.width * 2.0, height: item.content.dimensions.height * 2.0)
videoNode.updateLayout(size: videoSize, transition: .immediate)
videoNode.ownsContentNodeUpdated = { [weak self] value in
if let strongSelf = self {
strongSelf.updateDisplayPlaceholder(!value)
if strongSelf.playOnContentOwnership {
strongSelf.playOnContentOwnership = false
strongSelf.initiallyActivated = true
strongSelf.skipInitialPause = true
if let item = strongSelf.item, let _ = item.content as? PlatformVideoContent {
strongSelf.videoNode?.play()
} else {
strongSelf.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: isAnimated ? .loop : strongSelf.actionAtEnd)
}
}
}
}
self.videoNode = videoNode
videoNode.isUserInteractionEnabled = disablePlayerControls
videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335)
if item.fromPlayingVideo {
videoNode.canAttachContent = false
self.overlayContentNode.hide()
} else {
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
}
self.scrubberView.setStatusSignal(videoNode.status |> map { value -> MediaPlayerStatus in
if let value = value, !value.duration.isZero {
return value
} else {
return MediaPlayerStatus(generationTimestamp: 0.0, duration: max(Double(item.content.duration), 0.01), dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
}
})
self.scrubberView.setBufferingStatusSignal(videoNode.bufferingStatus)
self.requiresDownload = true
var mediaFileStatus: Signal<MediaResourceStatus?, NoError> = .single(nil)
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
if Namespaces.Message.allScheduled.contains(message.id.namespace) {
disablePictureInPicture = true
} else {
let throttledSignal = videoNode.status
|> mapToThrottled { next -> Signal<MediaPlayerStatus?, NoError> in
return .single(next) |> then(.complete() |> delay(2.0, queue: Queue.concurrentDefaultQueue()))
}
self.mediaPlaybackStateDisposable.set(throttledSignal.start(next: { status in
if let status = status, status.duration >= 60.0 * 20.0 {
var timestamp: Double?
if status.timestamp > 5.0 && status.timestamp < status.duration - 5.0 {
timestamp = status.timestamp
}
item.storeMediaPlaybackState(message.id, timestamp)
}
}))
}
var file: TelegramMediaFile?
var isWebpage = false
for m in message.media {
if let m = m as? TelegramMediaFile, m.isVideo {
file = m
break
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo {
file = f
isWebpage = true
break
}
}
if let file = file {
let status = messageMediaFileStatus(context: item.context, messageId: message.id, file: file)
if !isWebpage {
self.scrubberView.setFetchStatusSignal(status, strings: self.presentationData.strings, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator, fileSize: file.size)
}
self.requiresDownload = !isMediaStreamable(message: message, media: file)
mediaFileStatus = status |> map(Optional.init)
self.fetchControls = FetchControls(fetch: { [weak self] in
if let strongSelf = self {
strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: item.context, message: message, file: file, userInitiated: true).start())
}
}, cancel: {
messageMediaFileCancelInteractiveFetch(context: item.context, messageId: message.id, file: file)
})
}
}
self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus)
|> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in
if let strongSelf = self {
var initialBuffering = false
var playing = false
var isPaused = true
var seekable = false
var hasStarted = false
if let value = value {
hasStarted = value.timestamp > 0
if let zoomableContent = strongSelf.zoomableContent, !value.dimensions.width.isZero && !value.dimensions.height.isZero {
let videoSize = CGSize(width: value.dimensions.width * 2.0, height: value.dimensions.height * 2.0)
if !zoomableContent.0.equalTo(videoSize) {
strongSelf.zoomableContent = (videoSize, zoomableContent.1)
strongSelf.videoNode?.updateLayout(size: videoSize, transition: .immediate)
}
}
switch value.status {
case .playing:
isPaused = false
playing = true
case let .buffering(_, whilePlaying, _):
initialBuffering = true
isPaused = !whilePlaying
var isStreaming = false
if let fetchStatus = strongSelf.fetchStatus {
switch fetchStatus {
case .Local:
break
default:
isStreaming = true
}
} else {
switch fetchStatus {
case .Local:
break
default:
isStreaming = true
}
}
if let content = item.content as? NativeVideoContent, !isStreaming {
initialBuffering = false
if !content.enableSound {
isPaused = false
}
}
default:
if let content = item.content as? NativeVideoContent, !content.streamVideo.enabled {
if !content.enableSound {
isPaused = false
}
}
}
seekable = value.duration >= 30.0
}
if strongSelf.isCentral && playing && strongSelf.previousPlaying != true && !disablePlayerControls {
strongSelf.controlsTimer?.invalidate()
let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
self?.updateControlsVisibility(false)
self?.controlsTimer = nil
}, queue: Queue.mainQueue())
timer.start()
strongSelf.controlsTimer = timer
} else if !playing {
strongSelf.controlsTimer?.invalidate()
strongSelf.controlsTimer = nil
}
strongSelf.previousPlaying = playing
var fetching = false
if initialBuffering {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false), animated: false, completion: {})
} else {
var state: RadialStatusNodeState = .play(.white)
if let fetchStatus = fetchStatus {
if strongSelf.requiresDownload {
switch fetchStatus {
case .Remote:
state = .download(.white)
case let .Fetching(_, progress):
if !playing {
fetching = true
isPaused = true
}
state = .progress(color: .white, lineWidth: nil, value: CGFloat(progress), cancelEnabled: true)
default:
break
}
}
}
strongSelf.statusNode.transitionToState(state, animated: false, completion: {})
}
strongSelf.isPaused = isPaused
strongSelf.fetchStatus = fetchStatus
if !item.hideControls {
strongSelf.statusNodeShouldBeHidden = (!initialBuffering && (strongSelf.didPause || !isPaused) && !fetching)
strongSelf.statusButtonNode.isHidden = strongSelf.hideStatusNodeUntilCentrality || strongSelf.statusNodeShouldBeHidden
}
if isAnimated || disablePlayerControls {
strongSelf.footerContentNode.content = .info
} else if isPaused {
if hasStarted || strongSelf.didPause {
strongSelf.footerContentNode.content = .playback(paused: true, seekable: seekable)
} else if let fetchStatus = fetchStatus, !strongSelf.requiresDownload {
strongSelf.footerContentNode.content = .fetch(status: fetchStatus)
}
} else {
strongSelf.footerContentNode.content = .playback(paused: false, seekable: seekable)
}
}
}))
self.zoomableContent = (videoSize, videoNode)
var barButtonItems: [UIBarButtonItem] = []
if hasLinkedStickers {
let rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Stickers"), color: .white), style: .plain, target: self, action: #selector(self.openStickersButtonPressed))
barButtonItems.append(rightBarButtonItem)
}
if forceEnablePiP || (!isAnimated && !disablePlayerControls && !disablePictureInPicture) {
let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed))
barButtonItems.append(rightBarButtonItem)
self.hasPictureInPicture = true
} else {
self.hasPictureInPicture = false
}
self._rightBarButtonItems.set(.single(barButtonItems))
videoNode.playbackCompleted = { [weak videoNode] in
Queue.mainQueue().async {
item.playbackCompleted()
if !isAnimated {
videoNode?.seek(0.0)
}
}
}
self._ready.set(videoNode.ready)
}
self.item = item
if let contentInfo = item.contentInfo {
switch contentInfo {
case let .message(message):
self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop)
case let .webPage(webPage, media, _):
self.footerContentNode.setWebPage(webPage, media: media)
}
}
self.footerContentNode.setup(origin: item.originData, caption: item.caption)
}
override func controlsVisibilityUpdated(isVisible: Bool) {
self.controlsTimer?.invalidate()
self.controlsTimer = nil
}
private func updateDisplayPlaceholder(_ displayPlaceholder: Bool) {
if displayPlaceholder {
if self.pictureInPictureNode == nil {
let pictureInPictureNode = UniversalVideoGalleryItemPictureInPictureNode(strings: self.presentationData.strings)
pictureInPictureNode.isUserInteractionEnabled = false
self.pictureInPictureNode = pictureInPictureNode
self.insertSubnode(pictureInPictureNode, aboveSubnode: self.scrollNode)
if let validLayout = self.validLayout {
if let item = self.item {
let placeholderSize = item.content.dimensions.fitted(validLayout.0.size)
pictureInPictureNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.0.size.width - placeholderSize.width) / 2.0), y: floor((validLayout.0.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)
pictureInPictureNode.updateLayout(placeholderSize, transition: .immediate)
}
}
self.videoNode?.backgroundColor = UIColor(rgb: 0x333335)
}
} else if let pictureInPictureNode = self.pictureInPictureNode {
self.pictureInPictureNode = nil
pictureInPictureNode.removeFromSupernode()
self.videoNode?.backgroundColor = .black
}
}
private func shouldAutoplayOnCentrality() -> Bool {
if let item = self.item, let content = item.content as? NativeVideoContent {
var isLocal = false
if let fetchStatus = self.fetchStatus, case .Local = fetchStatus {
isLocal = true
}
var isStreamable = false
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
isStreamable = isMediaStreamable(message: message, media: content.fileReference.media)
} else {
isStreamable = isMediaStreamable(media: content.fileReference.media)
}
if isLocal || isStreamable {
return true
}
} else if let item = self.item, let _ = item.content as? PlatformVideoContent {
return true
}
return false
}
override func centralityUpdated(isCentral: Bool) {
super.centralityUpdated(isCentral: isCentral)
if self.isCentral != isCentral {
self.isCentral = isCentral
if let videoNode = self.videoNode {
if isCentral {
var isAnimated = false
if let item = self.item, let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
}
self.hideStatusNodeUntilCentrality = false
self.statusButtonNode.isHidden = self.hideStatusNodeUntilCentrality || self.statusNodeShouldBeHidden
if videoNode.ownsContentNode {
if isAnimated {
videoNode.seek(0.0)
videoNode.play()
} else if self.shouldAutoplayOnCentrality() {
self.initiallyActivated = true
videoNode.playOnceWithSound(playAndRecord: false, actionAtEnd: self.actionAtEnd)
}
} else {
if self.shouldAutoplayOnCentrality() {
self.playOnContentOwnership = true
}
}
} else {
self.dismissOnOrientationChange = false
if videoNode.ownsContentNode {
videoNode.pause()
}
}
}
}
}
override func visibilityUpdated(isVisible: Bool) {
super.visibilityUpdated(isVisible: isVisible)
if self._isVisible != isVisible {
let hadPreviousValue = self._isVisible != nil
self._isVisible = isVisible
if let item = self.item, let videoNode = self.videoNode {
if hadPreviousValue {
videoNode.canAttachContent = isVisible
if isVisible {
if self.skipInitialPause {
self.skipInitialPause = false
} else {
videoNode.pause()
videoNode.seek(0.0)
}
} else {
videoNode.continuePlayingWithoutSound()
}
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
} else if !item.fromPlayingVideo {
videoNode.canAttachContent = isVisible
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
}
if self.shouldAutoplayOnCentrality() {
self.hideStatusNodeUntilCentrality = true
self.statusButtonNode.isHidden = true
}
}
}
}
override func processAction(_ action: GalleryControllerItemNodeAction) {
guard let videoNode = self.videoNode else {
return
}
switch action {
case let .timecode(timecode):
videoNode.seek(timecode)
}
}
override func activateAsInitial() {
if let videoNode = self.videoNode, self.isCentral {
self.initiallyActivated = true
var isAnimated = false
var seek = MediaPlayerSeek.start
if let item = self.item {
if let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
if let time = item.timecode {
seek = .timecode(time)
}
} else if let _ = item.content as? WebEmbedVideoContent {
if let time = item.timecode {
seek = .timecode(time)
}
}
}
if isAnimated {
videoNode.seek(0.0)
videoNode.play()
} else {
self.hideStatusNodeUntilCentrality = false
self.statusButtonNode.isHidden = self.hideStatusNodeUntilCentrality || self.statusNodeShouldBeHidden
videoNode.playOnceWithSound(playAndRecord: false, seek: seek, actionAtEnd: self.actionAtEnd)
}
}
}
private var actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd {
if let item = self.item {
if let content = item.content as? NativeVideoContent, content.duration <= 30 {
return .loop
}
}
return .stop
}
override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
guard let videoNode = self.videoNode else {
return
}
if let node = node.0 as? OverlayMediaItemNode {
self.customUnembedWhenPortrait = node.customUnembedWhenPortrait
node.customUnembedWhenPortrait = nil
}
if let node = node.0 as? OverlayMediaItemNode, self.context.sharedContext.mediaManager.hasOverlayVideoNode(node) {
var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view)
let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview)
videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0)
videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
videoNode.canAttachContent = true
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
self.context.sharedContext.mediaManager.setOverlayVideoNode(nil)
} else {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view)
var transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview)
var transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view)
let (maybeSurfaceCopyView, _) = node.2()
let (maybeCopyView, copyViewBackground) = node.2()
copyViewBackground?.alpha = 0.0
let surfaceCopyView = maybeSurfaceCopyView!
let copyView = maybeCopyView!
addToTransitionSurface(surfaceCopyView)
var transformedSurfaceFrame: CGRect?
var transformedSurfaceFinalFrame: CGRect?
if let contentSurface = surfaceCopyView.superview {
transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface)
transformedSurfaceFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface)
if let frame = transformedSurfaceFrame, frame.minY < 0.0 {
transformedSurfaceFrame = CGRect(x: frame.minX, y: 0.0, width: frame.width, height: frame.height)
}
}
if transformedSelfFrame.maxY < 0.0 {
transformedSelfFrame = CGRect(x: transformedSelfFrame.minX, y: 0.0, width: transformedSelfFrame.width, height: transformedSelfFrame.height)
}
if transformedSuperFrame.maxY < 0.0 {
transformedSuperFrame = CGRect(x: transformedSuperFrame.minX, y: 0.0, width: transformedSuperFrame.width, height: transformedSuperFrame.height)
}
if let transformedSurfaceFrame = transformedSurfaceFrame {
surfaceCopyView.frame = transformedSurfaceFrame
}
self.view.insertSubview(copyView, belowSubview: self.scrollNode.view)
copyView.frame = transformedSelfFrame
copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false)
surfaceCopyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
let scale = CGSize(width: transformedCopyViewFinalFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewFinalFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceFinalFrame = transformedSurfaceFinalFrame {
surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedSurfaceFinalFrame.midX, y: transformedSurfaceFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in
surfaceCopyView?.removeFromSuperview()
})
let scale = CGSize(width: transformedSurfaceFinalFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceFinalFrame.size.height / transformedSurfaceFrame.size.height)
surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
}
if surfaceCopyView.superview != nil {
videoNode.allowsGroupOpacity = true
videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak videoNode] _ in
videoNode?.allowsGroupOpacity = false
})
}
videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0)
videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
if self.item?.fromPlayingVideo ?? false {
Queue.mainQueue().after(0.001) {
videoNode.canAttachContent = true
self.updateDisplayPlaceholder(!videoNode.ownsContentNode)
}
}
if let pictureInPictureNode = self.pictureInPictureNode {
let transformedPlaceholderFrame = node.0.view.convert(node.0.view.bounds, to: pictureInPictureNode.view)
let transform = CATransform3DScale(pictureInPictureNode.layer.transform, transformedPlaceholderFrame.size.width / pictureInPictureNode.layer.bounds.size.width, transformedPlaceholderFrame.size.height / pictureInPictureNode.layer.bounds.size.height, 1.0)
pictureInPictureNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: pictureInPictureNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
pictureInPictureNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
pictureInPictureNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: pictureInPictureNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
}
self.statusButtonNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusButtonNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusButtonNode.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
}
}
override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
guard let videoNode = self.videoNode else {
completion()
return
}
let transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view)
var transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view)
var positionCompleted = false
var transformCompleted = false
var boundsCompleted = true
var copyCompleted = false
let (maybeSurfaceCopyView, _) = node.2()
let (maybeCopyView, copyViewBackground) = node.2()
copyViewBackground?.alpha = 0.0
let surfaceCopyView = maybeSurfaceCopyView!
let copyView = maybeCopyView!
addToTransitionSurface(surfaceCopyView)
var transformedSurfaceFrame: CGRect?
var transformedSurfaceCopyViewInitialFrame: CGRect?
if let contentSurface = surfaceCopyView.superview {
transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface)
transformedSurfaceCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface)
}
self.view.insertSubview(copyView, belowSubview: self.scrollNode.view)
copyView.frame = transformedSelfFrame
let intermediateCompletion = { [weak copyView, weak surfaceCopyView] in
if positionCompleted && transformCompleted && boundsCompleted && copyCompleted {
copyView?.removeFromSuperview()
surfaceCopyView?.removeFromSuperview()
videoNode.canAttachContent = false
videoNode.removeFromSupernode()
completion()
}
}
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
copyCompleted = true
intermediateCompletion()
})
if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedCopyViewInitialFrame = transformedSurfaceCopyViewInitialFrame {
surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSurfaceFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSurfaceFrame.size.height)
surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
}
self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
})
self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false)
let fromTransform: CATransform3D
let toTransform: CATransform3D
if let instantNode = node.0 as? GalleryItemTransitionNode, instantNode.isAvailableForInstantPageTransition(), videoNode.hasAttachedContext {
copyView.removeFromSuperview()
let previousFrame = videoNode.frame
let previousSuperview = videoNode.view.superview
addToTransitionSurface(videoNode.view)
videoNode.view.superview?.bringSubviewToFront(videoNode.view)
if let previousSuperview = previousSuperview {
videoNode.frame = previousSuperview.convert(previousFrame, to: videoNode.view.superview)
transformedSuperFrame = transformedSuperFrame.offsetBy(dx: videoNode.position.x - previousFrame.center.x, dy: videoNode.position.y - previousFrame.center.y)
}
let initialScale: CGFloat = 1.0
let targetScale = max(transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height)
videoNode.backgroundColor = .clear
let transformScale: CGFloat = initialScale * targetScale
fromTransform = CATransform3DScale(videoNode.layer.transform, initialScale, initialScale, 1.0)
toTransform = CATransform3DScale(videoNode.layer.transform, transformScale, transformScale, 1.0)
if videoNode.hasAttachedContext {
if self.isPaused || !self.keepSoundOnDismiss {
videoNode.continuePlayingWithoutSound()
}
}
} else if let interactiveMediaNode = node.0 as? GalleryItemTransitionNode, interactiveMediaNode.isAvailableForGalleryTransition(), videoNode.hasAttachedContext {
copyView.removeFromSuperview()
let previousFrame = videoNode.frame
let previousSuperview = videoNode.view.superview
addToTransitionSurface(videoNode.view)
videoNode.view.superview?.bringSubviewToFront(videoNode.view)
if let previousSuperview = previousSuperview {
videoNode.frame = previousSuperview.convert(previousFrame, to: videoNode.view.superview)
transformedSuperFrame = transformedSuperFrame.offsetBy(dx: videoNode.position.x - previousFrame.center.x, dy: videoNode.position.y - previousFrame.center.y)
}
let initialScale = min(videoNode.layer.bounds.width / node.0.view.bounds.width, videoNode.layer.bounds.height / node.0.view.bounds.height)
let targetScale = max(transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height)
videoNode.backgroundColor = .clear
if let bubbleDecoration = interactiveMediaNode.decoration as? ChatBubbleVideoDecoration, let decoration = videoNode.decoration as? GalleryVideoDecoration {
transformedSuperFrame = transformedSuperFrame.offsetBy(dx: bubbleDecoration.corners.extendedEdges.right / 2.0 - bubbleDecoration.corners.extendedEdges.left / 2.0, dy: 0.0)
if let item = self.item {
let size = item.content.dimensions.aspectFilled(bubbleDecoration.contentContainerNode.frame.size)
videoNode.updateLayout(size: size, transition: .immediate)
videoNode.bounds = CGRect(origin: CGPoint(), size: size)
boundsCompleted = false
decoration.updateCorners(bubbleDecoration.corners)
decoration.updateClippingFrame(bubbleDecoration.contentContainerNode.bounds, completion: {
boundsCompleted = true
intermediateCompletion()
})
}
}
let transformScale: CGFloat = initialScale * targetScale
fromTransform = CATransform3DScale(videoNode.layer.transform, initialScale, initialScale, 1.0)
toTransform = CATransform3DScale(videoNode.layer.transform, transformScale, transformScale, 1.0)
if videoNode.hasAttachedContext {
if self.isPaused || !self.keepSoundOnDismiss {
videoNode.continuePlayingWithoutSound()
}
}
} else {
videoNode.allowsGroupOpacity = true
videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in
videoNode?.allowsGroupOpacity = false
})
fromTransform = videoNode.layer.transform
toTransform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0)
}
videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
videoNode.layer.animate(from: NSValue(caTransform3D: fromTransform), to: NSValue(caTransform3D: toTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
transformCompleted = true
intermediateCompletion()
})
if let pictureInPictureNode = self.pictureInPictureNode {
let transformedPlaceholderFrame = node.0.view.convert(node.0.view.bounds, to: pictureInPictureNode.view)
let pictureInPictureTransform = CATransform3DScale(pictureInPictureNode.layer.transform, transformedPlaceholderFrame.size.width / pictureInPictureNode.layer.bounds.size.width, transformedPlaceholderFrame.size.height / pictureInPictureNode.layer.bounds.size.height, 1.0)
pictureInPictureNode.layer.animate(from: NSValue(caTransform3D: pictureInPictureNode.layer.transform), to: NSValue(caTransform3D: pictureInPictureTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
pictureInPictureNode.layer.animatePosition(from: pictureInPictureNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
pictureInPictureNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
}
func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) {
guard let videoNode = self.videoNode else {
completion()
return
}
var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view)
let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview)
let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view)
let transformedSelfTargetSuperFrame = videoNode.view.convert(videoNode.view.bounds, to: node.view.superview)
var positionCompleted = false
var boundsCompleted = false
var copyCompleted = false
var nodeCompleted = false
let copyView = node.view.snapshotContentTree()!
videoNode.isHidden = true
copyView.frame = transformedSelfFrame
let intermediateCompletion = { [weak copyView] in
if positionCompleted && boundsCompleted && copyCompleted && nodeCompleted {
copyView?.removeFromSuperview()
completion()
}
}
copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
copyCompleted = true
intermediateCompletion()
})
videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
})
self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false)
transformedFrame.origin = CGPoint()
let videoTransform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0)
videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: videoTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})
if let pictureInPictureNode = self.pictureInPictureNode {
pictureInPictureNode.isHidden = true
}
let nodeTransform = CATransform3DScale(node.layer.transform, videoNode.layer.bounds.size.width / transformedFrame.size.width, videoNode.layer.bounds.size.height / transformedFrame.size.height, 1.0)
node.layer.animatePosition(from: CGPoint(x: transformedSelfTargetSuperFrame.midX, y: transformedSelfTargetSuperFrame.midY), to: node.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
node.layer.animate(from: NSValue(caTransform3D: nodeTransform), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
nodeCompleted = true
intermediateCompletion()
})
}
override func title() -> Signal<String, NoError> {
return self._title.get()
}
override func titleView() -> Signal<UIView?, NoError> {
return self._titleView.get()
}
override func rightBarButtonItems() -> Signal<[UIBarButtonItem]?, NoError> {
return self._rightBarButtonItems.get()
}
@objc func statusButtonPressed() {
if let videoNode = self.videoNode {
if let fetchStatus = self.fetchStatus, case .Local = fetchStatus {
self.toggleControlsVisibility()
}
if let fetchStatus = self.fetchStatus {
switch fetchStatus {
case .Local:
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: self.actionAtEnd)
case .Remote:
if self.requiresDownload {
self.fetchControls?.fetch()
} else {
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: self.actionAtEnd)
}
case .Fetching:
self.fetchControls?.cancel()
}
} else {
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: self.actionAtEnd)
}
}
}
private func expandIntoCustomPiP() {
if let item = self.item, let videoNode = self.videoNode, let customUnembedWhenPortrait = customUnembedWhenPortrait {
self.customUnembedWhenPortrait = nil
videoNode.setContinuePlayingWithoutSoundOnLostAudioSession(false)
let context = self.context
let baseNavigationController = self.baseNavigationController()
let mediaManager = self.context.sharedContext.mediaManager
var expandImpl: (() -> Void)?
let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: {
expandImpl?()
}, close: { [weak mediaManager] in
mediaManager?.setOverlayVideoNode(nil)
})
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<ChatLocationContextHolder?>(value: nil)), 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)
}), 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)
}
return nil
}))
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)
})
}
}
}
if customUnembedWhenPortrait(overlayNode) {
self.beginCustomDismiss()
self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
self?.completeCustomDismiss()
})
}
}
}
@objc func pictureInPictureButtonPressed() {
if let item = self.item, let videoNode = self.videoNode {
videoNode.setContinuePlayingWithoutSoundOnLostAudioSession(false)
let context = self.context
let baseNavigationController = self.baseNavigationController()
let mediaManager = self.context.sharedContext.mediaManager
var expandImpl: (() -> Void)?
let overlayNode = OverlayUniversalVideoNode(postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, expand: {
expandImpl?()
}, close: { [weak mediaManager] in
mediaManager?.setOverlayVideoNode(nil)
})
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<ChatLocationContextHolder?>(value: nil)), 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)
}), 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)
}
return nil
}))
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()
})
}
}
}
@objc func openStickersButtonPressed() {
if let content = self.item?.content as? NativeVideoContent {
let media = content.fileReference.abstract
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
guard let strongSelf = self else {
return EmptyDisposable
}
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
(strongSelf.baseNavigationController()?.topViewController as? ViewController)?.present(controller, in: .window(.root), with: nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let signal = stickerPacksAttachedToMedia(account: self.context.account, media: media)
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
let _ = (signal
|> deliverOnMainQueue).start(next: { [weak self] packs in
guard let strongSelf = self, !packs.isEmpty else {
return
}
let baseNavigationController = strongSelf.baseNavigationController()
baseNavigationController?.view.endEditing(true)
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil)
(baseNavigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root), with: nil)
})
}
}
override func adjustForPreviewing() {
super.adjustForPreviewing()
self.scrubberView.isHidden = true
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, nil))
}
}