Swiftgram/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift
2024-10-01 21:25:11 +08:00

3299 lines
168 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Display
import Postbox
import TelegramPresentationData
import UniversalMediaPlayer
import AccountContext
import RadialStatusNode
import TelegramUniversalVideoContent
import PresentationDataUtils
import OverlayStatusController
import StickerPackPreviewUI
import AppBundle
import AnimationUI
import ContextUI
import SaveToCameraRoll
import UndoUI
import TelegramUIPreferences
import OpenInExternalAppUI
import AVKit
import TextFormat
import SliderContextItem
import Pasteboard
import AdUI
import AdsInfoScreen
import AdsReportScreen
public enum UniversalVideoGalleryItemContentInfo {
case message(Message, Int?)
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 description: NSAttributedString?
let credit: NSAttributedString?
let displayInfoOnTop: Bool
let hideControls: Bool
let fromPlayingVideo: Bool
let isSecret: Bool
let landscape: Bool
let timecode: Double?
let peerIsCopyProtected: Bool
let playbackRate: () -> Double?
let configuration: GalleryConfiguration?
let playbackCompleted: () -> Void
let performAction: (GalleryControllerInteractionTapAction) -> Void
let openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void
let storeMediaPlaybackState: (MessageId, Double?, Double) -> Void
let present: (ViewController, Any?) -> Void
public init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, description: NSAttributedString? = nil, credit: NSAttributedString? = nil, displayInfoOnTop: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, peerIsCopyProtected: Bool = false, playbackRate: @escaping () -> Double?, configuration: GalleryConfiguration? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?, 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.description = description
self.credit = credit
self.displayInfoOnTop = displayInfoOnTop
self.hideControls = hideControls
self.fromPlayingVideo = fromPlayingVideo
self.isSecret = isSecret
self.landscape = landscape
self.timecode = timecode
self.peerIsCopyProtected = peerIsCopyProtected
self.playbackRate = playbackRate
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)").string))
} else if case let .message(message, _) = self.contentInfo, let _ = message.adAttribute {
node._title.set(.single(self.presentationData.strings.Gallery_Ad))
}
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)").string))
}
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, mediaIndex) = contentInfo {
if let paidContent = message.paidContent {
var mediaReference: AnyMediaReference?
let mediaIndex = mediaIndex ?? 0
if case let .full(fullMedia) = paidContent.extendedMedia[Int(mediaIndex)], let m = fullMedia as? TelegramMediaFile {
mediaReference = .message(message: MessageReference(message), media: m)
}
if let mediaReference = mediaReference {
if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, userLocation: .peer(message.id.peerId), mediaReference: mediaReference) {
return (0, item)
}
}
} else 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, userLocation: .peer(message.id.peerId), 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, userLocation: .other, 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 moreButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: .white)
private let placeholderFont = Font.regular(16.0)
private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode {
enum Mode {
case pictureInPicture
case airplay
}
private let iconNode: ASImageNode
private let textNode: ASTextNode
init(strings: PresentationStrings, mode: Mode) {
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
let text: String
switch mode {
case .pictureInPicture:
text = strings.Embed_PlayingInPIP
case .airplay:
text = strings.Gallery_AirPlayPlaceholder
}
self.textNode.attributedText = NSAttributedString(string: text, font: placeholderFont, textColor: UIColor(rgb: 0x8e8e93))
super.init()
self.backgroundColor = UIColor(rgb: 0x333335)
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 fullscreenImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Fullscreen"), color: .white)
private let minimizeImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Minimize"), color: .white)
private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentNode {
private let wrapperNode: ASDisplayNode
private let fullscreenNode: HighlightableButtonNode
private var validLayout: (CGSize, LayoutMetrics, UIEdgeInsets)?
var action: ((Bool) -> Void)?
override init() {
self.wrapperNode = ASDisplayNode()
self.wrapperNode.alpha = 0.0
self.fullscreenNode = HighlightableButtonNode()
self.fullscreenNode.setImage(fullscreenImage, for: .normal)
self.fullscreenNode.setImage(minimizeImage, for: .selected)
self.fullscreenNode.setImage(minimizeImage, for: [.selected, .highlighted])
super.init()
self.addSubnode(self.wrapperNode)
self.wrapperNode.addSubnode(self.fullscreenNode)
self.fullscreenNode.addTarget(self, action: #selector(self.toggleFullscreenPressed), forControlEvents: .touchUpInside)
}
override func updateLayout(size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets, isHidden: Bool, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, metrics, insets)
let isLandscape = size.width > size.height
self.fullscreenNode.isSelected = isLandscape
let iconSize: CGFloat = 42.0
let inset: CGFloat = 4.0
let buttonFrame = CGRect(origin: CGPoint(x: size.width - iconSize - inset - insets.right, y: size.height - iconSize - inset - insets.bottom), size: CGSize(width: iconSize, height: iconSize))
transition.updateFrame(node: self.wrapperNode, frame: buttonFrame)
transition.updateFrame(node: self.fullscreenNode, frame: CGRect(origin: CGPoint(), size: buttonFrame.size))
}
override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) {
if !self.visibilityAlpha.isZero {
transition.updateAlpha(node: self.wrapperNode, alpha: 1.0)
}
}
override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
transition.updateAlpha(node: self.wrapperNode, alpha: 0.0)
}
override func setVisibilityAlpha(_ alpha: CGFloat) {
super.setVisibilityAlpha(alpha)
self.updateFullscreenButtonVisibility()
}
func updateFullscreenButtonVisibility() {
self.wrapperNode.alpha = self.visibilityAlpha
if let validLayout = self.validLayout {
self.updateLayout(size: validLayout.0, metrics: validLayout.1, insets: validLayout.2, isHidden: false, transition: .animated(duration: 0.3, curve: .easeInOut))
}
}
@objc func toggleFullscreenPressed() {
var toLandscape = false
if let (size, _, _) = self.validLayout, size.width < size.height {
toLandscape = true
}
if toLandscape {
self.wrapperNode.alpha = 0.0
}
self.action?(toLandscape)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.wrapperNode.frame.contains(point) {
return nil
}
return super.hitTest(point, with: event)
}
}
private struct FetchControls {
let fetch: () -> Void
let cancel: () -> Void
}
func optionsBackgroundImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: dark ? 0x1c1c1e : 0x2c2c2e).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: 14, topCapHeight: 14)
}
func optionsCircleImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor.white.cgColor)
let lineWidth: CGFloat = 1.3
context.setLineWidth(lineWidth)
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth, dy: lineWidth))
})
}
private func optionsRateImage(rate: String, isLarge: Bool, color: UIColor = .white) -> UIImage? {
return generateImage(isLarge ? CGSize(width: 30.0, height: 30.0) : CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
if let image = generateTintedImage(image: UIImage(bundleImageName: isLarge ? "Chat/Context Menu/Playspeed30" : "Chat/Context Menu/Playspeed24"), color: .white) {
image.draw(at: CGPoint(x: 0.0, y: 0.0))
}
let string = NSMutableAttributedString(string: rate, font: Font.with(size: isLarge ? 11.0 : 10.0, design: .round, weight: .semibold), textColor: color)
var offset = CGPoint(x: 1.0, y: 0.0)
if rate.count >= 3 {
if rate == "0.5x" {
string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
offset.x += -0.5
} else {
string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
offset.x += -0.3
}
} else {
offset.x += -0.3
}
if !isLarge {
offset.x *= 0.5
offset.y *= 0.5
}
let boundingRect = string.boundingRect(with: size, options: [], context: nil)
string.draw(at: CGPoint(x: offset.x + floor((size.width - boundingRect.width) / 2.0), y: offset.y + floor((size.height - boundingRect.height) / 2.0)))
UIGraphicsPopContext()
})
}
final class MoreHeaderButton: HighlightableButtonNode {
enum Content {
case image(UIImage?)
case more(UIImage?)
}
let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
private var animationNode: AnimationNode?
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
private let wide: Bool
init(wide: Bool = false) {
self.wide = wide
self.referenceNode = ContextReferenceContentNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.animateScale = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.contentMode = .scaleToFill
super.init()
self.containerNode.addSubnode(self.referenceNode)
self.referenceNode.addSubnode(self.iconNode)
self.addSubnode(self.containerNode)
self.containerNode.shouldBegin = { [weak self] location in
guard let strongSelf = self, let _ = strongSelf.contextAction else {
return false
}
return true
}
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 44.0))
self.referenceNode.frame = self.containerNode.bounds
self.iconNode.image = optionsCircleImage(dark: false)
if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0)
}
private var content: Content?
func setContent(_ content: Content, animated: Bool = false) {
if case .more = content, self.animationNode == nil {
let iconColor = UIColor(rgb: 0xffffff)
let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor,
"Point 3.Group 1.Fill 1": iconColor,
"Point 1.Group 1.Fill 1": iconColor], scale: 1.0)
let animationSize = CGSize(width: 22.0, height: 22.0)
animationNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - animationSize.width) / 2.0), y: floor((self.containerNode.bounds.height - animationSize.height) / 2.0)), size: animationSize)
self.addSubnode(animationNode)
self.animationNode = animationNode
}
if animated {
if let snapshotView = self.referenceNode.view.snapshotContentTree() {
snapshotView.frame = self.referenceNode.frame
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false)
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3)
self.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.animationNode?.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3)
}
switch content {
case let .image(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = false
}
} else {
self.content = content
switch content {
case let .image(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
if let image = image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size)
}
self.iconNode.image = image
self.iconNode.isHidden = false
self.animationNode?.isHidden = false
}
}
}
override func didLoad() {
super.didLoad()
self.view.isOpaque = false
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: wide ? 32.0 : 22.0, height: 44.0)
}
func onLayout() {
}
func play() {
self.animationNode?.playOnce()
}
}
@available(iOS 15.0, *)
private final class PictureInPictureContentImpl: NSObject, PictureInPictureContent, AVPictureInPictureControllerDelegate {
private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate {
private let node: UniversalVideoNode
private var statusDisposable: Disposable?
private var status: MediaPlayerStatus?
weak var pictureInPictureController: AVPictureInPictureController?
private var previousIsPlaying = false
init(node: UniversalVideoNode) {
self.node = node
super.init()
var invalidatedStateOnce = false
self.statusDisposable = (self.node.status
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let strongSelf = self else {
return
}
strongSelf.status = status
if let status {
let isPlaying = status.status == .playing
if !invalidatedStateOnce {
invalidatedStateOnce = true
strongSelf.pictureInPictureController?.invalidatePlaybackState()
} else if strongSelf.previousIsPlaying != isPlaying {
strongSelf.previousIsPlaying = isPlaying
strongSelf.pictureInPictureController?.invalidatePlaybackState()
}
}
}).strict()
}
deinit {
self.statusDisposable?.dispose()
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) {
self.node.togglePlayPause()
}
public func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange {
guard let status = self.status else {
return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)))
}
return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: status.duration - status.timestamp, preferredTimescale: CMTimeScale(30.0)))
}
public func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
guard let status = self.status else {
return false
}
switch status.status {
case .playing:
return false
case .buffering, .paused:
return true
}
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) {
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) {
let node = self.node
let _ = (self.node.status
|> take(1)
|> deliverOnMainQueue).start(next: { [weak node] status in
if let node = node, let timestamp = status?.timestamp, let duration = status?.duration {
let nextTimestamp = timestamp + skipInterval.seconds
if nextTimestamp > duration {
node.seek(0.0)
node.pause()
} else {
node.seek(min(duration, nextTimestamp))
}
}
completionHandler()
})
}
public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
return false
}
}
private weak var overlayController: OverlayMediaController?
private weak var mediaManager: MediaManager?
private var pictureInPictureController: AVPictureInPictureController?
private var contentDelegate: PlaybackDelegate?
private let node: UniversalVideoNode
private let willBegin: (PictureInPictureContentImpl) -> Void
private let didEnd: (PictureInPictureContentImpl) -> Void
private let expand: (@escaping () -> Void) -> Void
private var pictureInPictureTimer: SwiftSignalKit.Timer?
private var didExpand: Bool = false
private var hiddenMediaManagerIndex: Int?
private var messageRemovedDisposable: Disposable?
private var isNativePictureInPictureActiveDisposable: Disposable?
init(context: AccountContext, overlayController: OverlayMediaController, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (PictureInPictureContentImpl) -> Void, didEnd: @escaping (PictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) {
self.overlayController = overlayController
self.mediaManager = mediaManager
self.node = videoNode
self.willBegin = willBegin
self.didEnd = didEnd
self.expand = expand
self.node.setCanPlaybackWithoutHierarchy(true)
super.init()
if let videoLayer = videoNode.getVideoLayer() {
let contentDelegate = PlaybackDelegate(node: self.node)
self.contentDelegate = contentDelegate
let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoLayer, playbackDelegate: contentDelegate))
self.pictureInPictureController = pictureInPictureController
contentDelegate.pictureInPictureController = pictureInPictureController
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false
pictureInPictureController.requiresLinearPlayback = !canSkip
pictureInPictureController.delegate = self
self.pictureInPictureController = pictureInPictureController
let timer = SwiftSignalKit.Timer(timeout: 0.005, repeat: true, completion: { [weak self] in
guard let strongSelf = self, let pictureInPictureController = strongSelf.pictureInPictureController else {
return
}
if pictureInPictureController.isPictureInPicturePossible {
strongSelf.pictureInPictureTimer?.invalidate()
strongSelf.pictureInPictureTimer = nil
pictureInPictureController.startPictureInPicture()
}
}, queue: .mainQueue())
self.pictureInPictureTimer = timer
timer.start()
} else {
var currentIsNativePictureInPictureActive = false
self.isNativePictureInPictureActiveDisposable = (videoNode.isNativePictureInPictureActive
|> deliverOnMainQueue).startStrict(next: { [weak self] isNativePictureInPictureActive in
guard let self else {
return
}
if currentIsNativePictureInPictureActive == isNativePictureInPictureActive {
return
}
currentIsNativePictureInPictureActive = isNativePictureInPictureActive
if isNativePictureInPictureActive {
Queue.mainQueue().after(0.0, { [weak self] in
guard let self else {
return
}
self.willBegin(self)
if let overlayController = self.overlayController {
overlayController.setPictureInPictureContentHidden(content: self, isHidden: true)
}
self.didEnd(self)
})
} else {
self.expand { [weak self] in
guard let self else {
return
}
self.didExpand = true
if let overlayController = self.overlayController {
overlayController.setPictureInPictureContentHidden(content: self, isHidden: false)
self.node.alpha = 0.02
}
guard let overlayController = self.overlayController else {
return
}
overlayController.removePictureInPictureContent(content: self)
self.node.canAttachContent = false
if self.didExpand {
return
}
self.node.continuePlayingWithoutSound()
}
}
})
let _ = videoNode.enterNativePictureInPicture()
}
if let hiddenMedia = hiddenMedia {
self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(Signal<(MessageId, Media)?, NoError>.single(hiddenMedia)
|> map { messageIdAndMedia in
if let (messageId, media) = messageIdAndMedia {
return .chat(accountId, messageId, media)
} else {
return nil
}
})
}
if let (messageId, _) = hiddenMedia {
self.messageRemovedDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> map { message -> Bool in
if let _ = message {
return false
} else {
return true
}
}
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let strongSelf = self else {
return
}
overlayController.removePictureInPictureContent(content: strongSelf)
strongSelf.node.canAttachContent = false
})
}
}
deinit {
self.messageRemovedDisposable?.dispose()
self.isNativePictureInPictureActiveDisposable?.dispose()
self.pictureInPictureTimer?.invalidate()
self.node.setCanPlaybackWithoutHierarchy(false)
if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex, let mediaManager = self.mediaManager {
mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
}
}
func invalidatePlaybackState() {
self.pictureInPictureController?.invalidatePlaybackState()
}
var videoNode: ASDisplayNode {
return self.node
}
public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
Queue.mainQueue().after(0.1, { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.willBegin(strongSelf)
if let overlayController = strongSelf.overlayController {
overlayController.setPictureInPictureContentHidden(content: strongSelf, isHidden: true)
}
})
}
public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.didEnd(self)
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
print(error)
}
public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
}
public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
guard let overlayController = self.overlayController else {
return
}
overlayController.removePictureInPictureContent(content: self)
self.node.canAttachContent = false
if self.didExpand {
return
}
self.node.continuePlayingWithoutSound()
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
self.expand { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.didExpand = true
if let overlayController = strongSelf.overlayController {
overlayController.setPictureInPictureContentHidden(content: strongSelf, isHidden: false)
strongSelf.node.alpha = 0.02
}
completionHandler(true)
}
}
}
final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext
private let presentationData: PresentationData
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 var scrubberView: ChatVideoGalleryItemScrubberView?
private let footerContentNode: ChatItemGalleryFooterContentNode
private let overlayContentNode: UniversalVideoGalleryItemOverlayNode
private let moreBarButton: MoreHeaderButton
private var moreBarButtonRate: Double = 1.0
private var moreBarButtonRateTimestamp: Double?
private let settingsBarButton: MoreHeaderButton
private var videoNode: UniversalVideoNode?
private var videoNodeUserInteractionEnabled: Bool = false
private var videoFramePreview: FramePreview?
private var pictureInPictureNode: UniversalVideoGalleryItemPictureInPictureNode?
private var disablePictureInPicturePlaceholder: Bool = false
private let statusButtonNode: HighlightableButtonNode
private let statusNode: RadialStatusNode
private var statusNodeShouldBeHidden = true
private var isCentral: Bool?
private var _isVisible: Bool?
private var initiallyActivated = false
private var hideStatusNodeUntilCentrality = false
private var playOnContentOwnership = false
private var skipInitialPause = false
private var ignorePauseStatus = 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 pictureInPictureButton: UIBarButtonItem?
private var requiresDownload = false
private var item: UniversalVideoGalleryItem?
private var playbackRate: Double?
private var videoQuality: UniversalVideoContentVideoQuality = .auto
private let playbackRatePromise = ValuePromise<Double>()
private let statusDisposable = MetaDisposable()
private let moreButtonStateDisposable = 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?
private var isPlaying = false
private let isPlayingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let isInteractingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let controlsVisiblePromise = ValuePromise<Bool>(true, ignoreRepeated: true)
private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let hasExpandedCaptionPromise = Promise<Bool>()
private var hideControlsDisposable: Disposable?
var playbackCompleted: (() -> Void)?
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
private var pictureInPictureContent: AnyObject?
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData, present: present)
self.hasExpandedCaptionPromise.set(self.footerContentNode.hasExpandedCaption)
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(""))
self.moreBarButton = MoreHeaderButton()
self.moreBarButton.isUserInteractionEnabled = true
self.moreBarButton.setContent(.more(optionsCircleImage(dark: false)))
self.settingsBarButton = MoreHeaderButton()
self.settingsBarButton.isUserInteractionEnabled = true
super.init()
self.clipsToBounds = true
self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside)
self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside)
self.footerContentNode.interacting = { [weak self] value in
self?.isInteractingPromise.set(value)
}
self.overlayContentNode.action = { [weak self] toLandscape in
guard let self else {
return
}
self.updateControlsVisibility(!toLandscape)
self.updateOrientation(toLandscape ? .landscapeRight : .portrait)
}
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] delta 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 - delta))
}
})
}
}
self.footerContentNode.seekForward = { [weak self] delta 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 + delta
if nextTimestamp > duration {
strongVideoNode.seek(0.0)
strongVideoNode.pause()
} else {
strongVideoNode.seek(min(duration, timestamp + delta))
}
}
})
}
}
self.footerContentNode.setPlayRate = { [weak self] rate in
if let strongSelf = self, let videoNode = strongSelf.videoNode {
videoNode.setBaseRate(rate)
if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(rate)
}
}
}
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, .Paused:
fetchControls.fetch()
case .Local:
break
}
}
self.footerContentNode.toggleFullscreen = { [weak self] in
guard let strongSelf = self else {
return
}
var toLandscape = false
let size = strongSelf.bounds.size
if size.width < size.height {
toLandscape = true
}
strongSelf.updateControlsVisibility(!toLandscape)
strongSelf.updateOrientation(toLandscape ? .landscapeRight : .portrait)
}
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)
}
}).strict()
self.alternativeDismiss = { [weak self] in
guard let strongSelf = self, strongSelf.hasPictureInPicture else {
return false
}
strongSelf.pictureInPictureButtonPressed()
return true
}
self.moreBarButton.contextAction = { [weak self] sourceNode, gesture in
guard let self else {
return
}
self.openMoreMenu(sourceNode: self.moreBarButton.referenceNode, gesture: gesture, isSettings: false)
}
self.titleContentView = GalleryTitleView(frame: CGRect())
self._titleView.set(.single(self.titleContentView))
let shouldHideControlsSignal: Signal<Void, NoError> = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get(), self.hasExpandedCaptionPromise.get())
|> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, hasExpandedCaptionPromise -> Signal<Void, NoError> in
if isShowingContextMenu || hasExpandedCaptionPromise {
return .complete()
}
if isPlaying && !isInteracting && controlsVisible {
return .single(Void())
|> delay(4.0, queue: Queue.mainQueue())
} else {
return .complete()
}
}
self.hideControlsDisposable = (shouldHideControlsSignal
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self {
strongSelf.updateControlsVisibility(false)
}
}).strict()
}
deinit {
self.statusDisposable.dispose()
self.moreButtonStateDisposable.dispose()
self.mediaPlaybackStateDisposable.dispose()
self.scrubbingFrameDisposable?.dispose()
self.hideControlsDisposable?.dispose()
}
override func ready() -> Signal<Void, NoError> {
return self._ready.get()
}
override func contentTapAction() -> Bool {
if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute {
self.item?.performAction(.ad(message.id))
return true
}
return false
}
override func screenFrameUpdated(_ frame: CGRect) {
let center = frame.midX - self.frame.width / 2.0
self.subnodeTransform = CATransform3DMakeTranslation(-center * 0.16, 0.0, 0.0)
}
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
}
let hadLayout = self.validLayout != nil
self.validLayout = (layout, navigationBarHeight)
if !hadLayout {
self.zoomableContent = zoomableContent
}
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 {
var placeholderSize = item.content.dimensions.fitted(layout.size)
placeholderSize.height += 2.0
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()
}
}
func setupItem(_ item: UniversalVideoGalleryItem) {
if self.item?.content.id != item.content.id {
var chapters = parseMediaPlayerChapters(item.caption)
if chapters.isEmpty, let description = item.description {
chapters = parseMediaPlayerChapters(description)
}
let scrubberView = ChatVideoGalleryItemScrubberView(chapters: chapters)
self.scrubberView = scrubberView
scrubberView.seek = { [weak self] timecode in
self?.videoNode?.seek(timecode)
}
scrubberView.updateScrubbing = { [weak self] timecode in
guard let strongSelf = self else {
return
}
strongSelf.isInteractingPromise.set(timecode != nil)
if let videoFramePreview = strongSelf.videoFramePreview {
if let timecode = timecode {
if !strongSelf.scrubbingFrames {
strongSelf.scrubbingFrames = true
strongSelf.scrubbingFrame.set(videoFramePreview.generatedFrames
|> map(Optional.init))
}
videoFramePreview.generateFrame(at: timecode)
} else {
strongSelf.isInteractingPromise.set(false)
strongSelf.scrubbingFrame.set(.single(nil))
videoFramePreview.cancelPendingFrames()
strongSelf.scrubbingFrames = false
}
}
}
self.footerContentNode.scrubberView = scrubberView
self.isPlaying = false
self.isPlayingPromise.set(false)
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
} else if let content = item.content as? HLSVideoContent {
hasLinkedStickers = content.fileReference.media.hasLinkedStickers
}
var disablePictureInPicture = false
var disablePlayerControls = false
var forceEnablePiP = false
var forceEnableUserInteraction = false
var isAnimated = false
var isEnhancedWebPlayer = false
var isAdaptive = false
if let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
self.videoFramePreview = MediaPlayerFramePreview(postbox: item.context.account.postbox, userLocation: content.userLocation, userContentType: .video, 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:
isEnhancedWebPlayer = true
forceEnableUserInteraction = true
disablePictureInPicture = !(item.configuration?.youtubePictureInPictureEnabled ?? false)
self.videoFramePreview = YoutubeEmbedFramePreview(context: item.context, content: content)
case .vimeo:
isEnhancedWebPlayer = true
case .iframe:
disablePlayerControls = true
default:
break
}
} else if let _ = item.content as? PlatformVideoContent {
disablePlayerControls = true
forceEnablePiP = true
} else if let _ = item.content as? HLSVideoContent {
isAdaptive = true
}
if isAdaptive {
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
} else {
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettings"), color: .white)))
}
let dimensions = item.content.dimensions
if dimensions.height > 0.0 {
if dimensions.width / dimensions.height < 1.33 || isAnimated {
self.overlayContentNode.isHidden = 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(accountId: item.context.account.id, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery)
let videoScale: CGFloat
if item.content is WebEmbedVideoContent {
videoScale = 1.0
} else {
videoScale = 2.0
}
let videoSize = CGSize(width: item.content.dimensions.width * videoScale, height: item.content.dimensions.height * videoScale)
let actualVideoSize = CGSize(width: item.content.dimensions.width, height: item.content.dimensions.height)
videoNode.updateLayout(size: videoSize, actualSize: actualVideoSize, 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)
}
if let playbackRate = strongSelf.playbackRate {
strongSelf.videoNode?.setBaseRate(playbackRate)
}
}
}
}
self.videoNode = videoNode
self.videoNodeUserInteractionEnabled = disablePlayerControls || forceEnableUserInteraction
videoNode.isUserInteractionEnabled = disablePlayerControls || forceEnableUserInteraction
videoNode.backgroundColor = UIColor.black
if item.fromPlayingVideo {
videoNode.canAttachContent = false
} else {
self.updateDisplayPlaceholder()
}
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)
}
})
scrubberView.setBufferingStatusSignal(videoNode.bufferingStatus)
self.requiresDownload = true
var mediaFileStatus: Signal<MediaResourceStatus?, NoError> = .single(nil)
var hintSeekable = false
if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo {
if message.paidContent != nil {
disablePictureInPicture = true
} else if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.id.namespace == Namespaces.Message.Local {
disablePictureInPicture = true
} else {
let throttledSignal = videoNode.status
|> mapToThrottled { next -> Signal<MediaPlayerStatus?, NoError> in
return .single(next) |> then(.complete() |> delay(0.5, queue: Queue.concurrentDefaultQueue()))
}
self.mediaPlaybackStateDisposable.set((throttledSignal
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let strongSelf = self, let videoNode = strongSelf.videoNode, videoNode.ownsContentNode else {
return
}
if let status = status, status.duration >= 60.0 * 10.0 {
var timestamp: Double?
if status.timestamp > 5.0 && status.timestamp < status.duration - 5.0 {
timestamp = status.timestamp
}
item.storeMediaPlaybackState(message.id, timestamp, status.baseRate)
}
}))
}
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 {
for attribute in file.attributes {
if case let .Video(duration, _, _, _, _, _) = attribute, duration >= 30 {
hintSeekable = true
break
}
}
let status = messageMediaFileStatus(context: item.context, messageId: message.id, file: file)
if !isWebpage && message.adAttribute == nil && !NativeVideoContent.isHLSVideo(file: file) {
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.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(),
self.playbackRatePromise.get(),
self.isShowingContextMenuPromise.get()
).start(next: { [weak self] playbackRate, isShowingContextMenu in
guard let strongSelf = self else {
return
}
let effectiveBaseRate: Double
if isShowingContextMenu {
effectiveBaseRate = 1.0
} else {
effectiveBaseRate = playbackRate
}
if abs(effectiveBaseRate - strongSelf.moreBarButtonRate) > 0.01 {
strongSelf.moreBarButtonRate = effectiveBaseRate
let animated: Bool
if let moreBarButtonRateTimestamp = strongSelf.moreBarButtonRateTimestamp {
animated = CFAbsoluteTimeGetCurrent() > (moreBarButtonRateTimestamp + 0.2)
} else {
animated = false
}
strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent()
if abs(effectiveBaseRate - 1.0) > 0.01 {
var stringValue = String(format: "%.1fx", effectiveBaseRate)
if stringValue.hasSuffix(".0x") {
stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
}
strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: stringValue, isLarge: true)), animated: animated)
} else {
strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated)
}
} else {
if strongSelf.moreBarButtonRateTimestamp == nil {
strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent()
}
}
}))*/
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 isPlaying = false
var isPaused = true
var seekable = hintSeekable
var hasStarted = false
var displayProgress = true
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
isPlaying = true
strongSelf.ignorePauseStatus = false
case let .buffering(_, whilePlaying, _, display):
displayProgress = display
initialBuffering = !whilePlaying
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
}
} else if strongSelf.actionAtEnd == .stop {
strongSelf.isPlayingPromise.set(false)
strongSelf.isPlaying = false
if strongSelf.isCentral == true {
if !item.isSecret {
strongSelf.updateControlsVisibility(true)
}
}
}
}
if !value.duration.isZero {
seekable = value.duration >= 30.0
}
}
if !disablePlayerControls && strongSelf.isCentral == true && isPlaying {
strongSelf.isPlayingPromise.set(true)
strongSelf.isPlaying = true
} else if !isPlaying {
strongSelf.isPlayingPromise.set(false)
strongSelf.isPlaying = false
}
var fetching = false
if initialBuffering {
if displayProgress {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: false, completion: {})
} else {
strongSelf.statusNode.transitionToState(.none, 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 !isPlaying {
fetching = true
isPaused = true
}
state = .progress(color: .white, lineWidth: nil, value: CGFloat(progress), cancelEnabled: true, animateRotation: true)
default:
break
}
}
}
strongSelf.statusNode.transitionToState(state, animated: false, completion: {})
}
strongSelf.isPaused = isPaused
strongSelf.fetchStatus = fetchStatus
if !item.hideControls {
strongSelf.statusNodeShouldBeHidden = strongSelf.ignorePauseStatus || (!initialBuffering && (strongSelf.didPause || !isPaused) && !fetching)
strongSelf.statusButtonNode.isHidden = strongSelf.hideStatusNodeUntilCentrality || strongSelf.statusNodeShouldBeHidden
}
if isAnimated || disablePlayerControls {
strongSelf.footerContentNode.content = .info
} else if isPaused && !strongSelf.ignorePauseStatus && strongSelf.isCentral == true {
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, seekable: seekable)
}
} 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))
rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_Stickers
barButtonItems.append(rightBarButtonItem)
}
if forceEnablePiP || (!isAnimated && !disablePlayerControls && !disablePictureInPicture) {
let rightBarButtonItem = UIBarButtonItem(image: pictureInPictureButtonImage, style: .plain, target: self, action: #selector(self.pictureInPictureButtonPressed))
rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_PictureInPicture
self.pictureInPictureButton = rightBarButtonItem
barButtonItems.append(rightBarButtonItem)
self.hasPictureInPicture = true
} else {
self.hasPictureInPicture = false
}
if let contentInfo = item.contentInfo, case let .message(message, mediaIndex) = contentInfo {
var file: TelegramMediaFile?
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
break
} else if let paidContent = message.paidContent {
let mediaIndex = mediaIndex ?? 0
let media = paidContent.extendedMedia[mediaIndex]
if case let .full(fullMedia) = media, let m = fullMedia as? TelegramMediaFile {
file = m
}
break
}
}
var hasMoreButton = false
if isEnhancedWebPlayer {
hasMoreButton = true
} else if let file = file, !file.isAnimated {
hasMoreButton = true
}
if let _ = message.paidContent, message.id.namespace == Namespaces.Message.Local {
hasMoreButton = false
}
if let _ = message.adAttribute {
hasMoreButton = true
}
if !isAnimated && !disablePlayerControls {
let settingsMenuItem = UIBarButtonItem(customDisplayNode: self.settingsBarButton)!
settingsMenuItem.accessibilityLabel = self.presentationData.strings.Settings_Title
barButtonItems.append(settingsMenuItem)
}
if hasMoreButton {
let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)!
moreMenuItem.accessibilityLabel = self.presentationData.strings.Common_More
barButtonItems.append(moreMenuItem)
}
}
self._rightBarButtonItems.set(.single(barButtonItems))
videoNode.playbackCompleted = { [weak self, weak videoNode] in
Queue.mainQueue().async {
item.playbackCompleted()
if let strongSelf = self, !isAnimated {
if #available(iOS 15.0, *) {
if let pictureInPictureContent = strongSelf.pictureInPictureContent as? PictureInPictureContentImpl {
pictureInPictureContent.invalidatePlaybackState()
}
}
if let snapshotView = videoNode?.view.snapshotView(afterScreenUpdates: false) {
videoNode?.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
videoNode?.seek(0.0)
if strongSelf.actionAtEnd == .stop && strongSelf.isCentral == true {
strongSelf.isPlayingPromise.set(false)
strongSelf.isPlaying = false
if !item.isSecret {
strongSelf.updateControlsVisibility(true)
}
}
}
}
}
self._ready.set(videoNode.ready)
}
self.item = item
if let _ = item.content as? NativeVideoContent {
self.playbackRate = item.playbackRate()
} else if let _ = item.content as? HLSVideoContent {
self.playbackRate = item.playbackRate()
} else if let _ = item.content as? WebEmbedVideoContent {
self.playbackRate = item.playbackRate()
}
self.playbackRatePromise.set(self.playbackRate ?? 1.0)
var isAd = false
if let contentInfo = item.contentInfo {
switch contentInfo {
case let .message(message, _):
isAd = message.adAttribute != nil
self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected)
case let .webPage(webPage, media, _):
self.footerContentNode.setWebPage(webPage, media: media)
}
}
self.footerContentNode.setup(origin: item.originData, caption: item.caption, isAd: isAd)
}
override func controlsVisibilityUpdated(isVisible: Bool) {
self.controlsVisiblePromise.set(isVisible)
self.videoNode?.isUserInteractionEnabled = isVisible ? self.videoNodeUserInteractionEnabled : false
self.videoNode?.notifyPlaybackControlsHidden(!isVisible)
}
private func updateDisplayPlaceholder() {
self.updateDisplayPlaceholder(!(self.videoNode?.ownsContentNode ?? true) || self.isAirPlayActive)
}
private func updateDisplayPlaceholder(_ displayPlaceholder: Bool) {
if displayPlaceholder && !self.disablePictureInPicturePlaceholder {
if self.pictureInPictureNode == nil {
let pictureInPictureNode = UniversalVideoGalleryItemPictureInPictureNode(strings: self.presentationData.strings, mode: self.isAirPlayActive ? .airplay : .pictureInPicture)
pictureInPictureNode.isUserInteractionEnabled = false
self.pictureInPictureNode = pictureInPictureNode
self.insertSubnode(pictureInPictureNode, aboveSubnode: self.scrollNode)
if let validLayout = self.validLayout {
if let item = self.item {
var placeholderSize = item.content.dimensions.fitted(validLayout.0.size)
placeholderSize.height += 2.0
pictureInPictureNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.0.size.width - placeholderSize.width) / 2.0), y: floorToScreenPixels((validLayout.0.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)
pictureInPictureNode.updateLayout(placeholderSize, transition: .immediate)
}
}
}
} else if let pictureInPictureNode = self.pictureInPictureNode {
self.pictureInPictureNode = nil
pictureInPictureNode.removeFromSupernode()
self.videoNode?.backgroundColor = .black
}
self.pictureInPictureButton?.isEnabled = self.pictureInPictureNode == nil
}
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? HLSVideoContent {
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
} else if let item = self.item, let content = item.content as? HLSVideoContent {
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)
videoNode.setBaseRate(self.playbackRate ?? 1.0)
}
} else {
if isAnimated {
self.playOnContentOwnership = true
} else if self.shouldAutoplayOnCentrality() {
self.playOnContentOwnership = true
}
}
} else {
self.isPlayingPromise.set(false)
self.isPlaying = false
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 {
self.ignorePauseStatus = true
videoNode.pause()
videoNode.seek(0.0)
}
} else {
videoNode.continuePlayingWithoutSound()
}
self.updateDisplayPlaceholder()
} else if !item.fromPlayingVideo {
videoNode.canAttachContent = isVisible
self.updateDisplayPlaceholder()
}
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):
self.scrubberView?.animateTo(timecode)
videoNode.seek(timecode)
}
}
override func activateAsInitial() {
if let videoNode = self.videoNode, self.isCentral == true {
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 content = item.content as? HLSVideoContent {
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)
}
}
}
videoNode.setBaseRate(self.playbackRate ?? 1.0)
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)
Queue.mainQueue().after(1.0, {
if let item = self.item, item.isSecret, !self.isPlaying {
videoNode.playOnceWithSound(playAndRecord: false, seek: .start, actionAtEnd: self.actionAtEnd)
}
})
}
}
}
private var actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd {
if let item = self.item {
if !item.isSecret, let content = item.content as? NativeVideoContent, content.duration <= 30 {
return .loop
}
if !item.isSecret, let content = item.content as? HLSVideoContent, 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()
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()
}
}
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, .Paused:
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(accountId: self.context.account.id, 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)
})
let playbackRate = self.playbackRate
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(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil)), playbackRate: playbackRate, 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(false)
self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
self?.completeCustomDismiss()
})
}
}
}
@objc func pictureInPictureButtonPressed() {
var isNativePictureInPictureSupported = false
switch self.item?.contentInfo {
case let .message(message, _):
for media in message.media {
if let media = media as? TelegramMediaFile, media.isVideo {
if message.id.namespace == Namespaces.Message.Cloud {
isNativePictureInPictureSupported = true
}
}
}
default:
break
}
if let item = self.item, let videoNode = self.videoNode, let overlayController = self.context.sharedContext.mediaManager.overlayMediaManager.controller {
videoNode.setContinuePlayingWithoutSoundOnLostAudioSession(false)
let context = self.context
let baseNavigationController = self.baseNavigationController()
let playbackRate = self.playbackRate
if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported(), isNativePictureInPictureSupported {
self.disablePictureInPicturePlaceholder = true
let overlayVideoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay)
let absoluteRect = videoNode.view.convert(videoNode.view.bounds, to: nil)
overlayVideoNode.frame = absoluteRect
overlayVideoNode.updateLayout(size: absoluteRect.size, transition: .immediate)
overlayVideoNode.canAttachContent = true
var hiddenMedia: (MessageId, Media)? = nil
switch item.contentInfo {
case let .message(message, _):
for media in message.media {
if let media = media as? TelegramMediaImage {
hiddenMedia = (message.id, media)
} else if let media = media as? TelegramMediaFile, media.isVideo {
hiddenMedia = (message.id, media)
}
}
default:
break
}
let content = PictureInPictureContentImpl(context: self.context, overlayController: overlayController, mediaManager: self.context.sharedContext.mediaManager, accountId: self.context.account.id, hiddenMedia: hiddenMedia, videoNode: overlayVideoNode, canSkip: true, willBegin: { [weak self] content in
guard let strongSelf = self else {
return
}
strongSelf.beginCustomDismiss(true)
}, didEnd: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.completeCustomDismiss()
}, expand: { [weak baseNavigationController] completion in
guard let contentInfo = item.contentInfo else {
return
}
switch contentInfo {
case let .message(message, _):
let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil)), playbackRate: playbackRate, replaceRootController: { [weak baseNavigationController] controller, ready in
if let baseNavigationController = baseNavigationController {
baseNavigationController.replaceTopController(controller, animated: false, ready: ready)
}
}, baseNavigationController: baseNavigationController)
gallery.temporaryDoNotWaitForReady = true
gallery.useSimpleAnimation = true
baseNavigationController?.view.endEditing(true)
(baseNavigationController?.topViewController as? ViewController)?.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(transitionArguments: { id, media in
return nil
}))
gallery.onDidAppear = {
completion()
}
case .webPage:
break
}
})
self.pictureInPictureContent = content
self.context.sharedContext.mediaManager.overlayMediaManager.controller?.setPictureInPictureContent(content: content, absoluteRect: absoluteRect)
} else {
let context = self.context
let baseNavigationController = self.baseNavigationController()
let mediaManager = self.context.sharedContext.mediaManager
var expandImpl: (() -> Void)?
let shouldBeDismissed: Signal<Bool, NoError>
if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo {
shouldBeDismissed = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: message.id))
|> map { message -> Bool in
if let _ = message {
return false
} else {
return true
}
}
|> distinctUntilChanged
} else {
shouldBeDismissed = .single(false)
}
let overlayNode = OverlayUniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, content: item.content, shouldBeDismissed: shouldBeDismissed, expand: {
expandImpl?()
}, close: { [weak mediaManager] in
mediaManager?.setOverlayVideoNode(nil)
})
let playbackRate = self.playbackRate
expandImpl = { [weak overlayNode] in
guard let contentInfo = item.contentInfo, let overlayNode = overlayNode else {
return
}
switch contentInfo {
case let .message(message, messageIndex):
let source: GalleryControllerItemSource
if let _ = message.paidContent {
source = .standaloneMessage(message, messageIndex)
} else {
source = .peerMessagesAtId(messageId: message.id, chatLocation: .peer(id: message.id.peerId), customTag: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil))
}
let gallery = GalleryController(context: context, source: source, playbackRate: playbackRate, 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(false)
self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
self?.completeCustomDismiss()
})
}
}
}
}
private func contentInfo() -> (message: Message, file: TelegramMediaFile?, isWebpage: Bool)? {
guard let item = self.item else {
return nil
}
if let contentInfo = item.contentInfo, case let .message(message, mediaIndex) = contentInfo {
var file: TelegramMediaFile?
var isWebpage = false
for m in message.media {
if let paidContent = m as? TelegramMediaPaidContent {
let media = paidContent.extendedMedia[mediaIndex ?? 0]
if case let .full(fullMedia) = media, let fullMedia = fullMedia as? TelegramMediaFile, fullMedia.isVideo {
file = fullMedia
}
break
} else if let m = m as? TelegramMediaFile, m.isVideo {
file = m
break
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content {
if let f = content.file, f.isVideo {
file = f
}
isWebpage = true
break
}
}
return (message, file, isWebpage)
}
return nil
}
private func canDelete() -> Bool {
guard let (message, _, _) = self.contentInfo() else {
return false
}
var canDelete = false
if let peer = message.peers[message.id.peerId] {
if peer is TelegramUser || peer is TelegramSecretChat {
canDelete = true
} else if let _ = peer as? TelegramGroup {
canDelete = true
} else if let channel = peer as? TelegramChannel {
if message.flags.contains(.Incoming) {
canDelete = channel.hasPermission(.deleteAllMessages)
} else {
canDelete = true
}
} else {
canDelete = false
}
} else {
canDelete = false
}
return canDelete
}
@objc private func moreButtonPressed() {
self.moreBarButton.play()
self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil)
}
private func openMoreMenu(sourceNode: ContextReferenceContentNode, gesture: ContextGesture?, isSettings: Bool) {
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
return
}
var dismissImpl: (() -> Void)?
let items: Signal<[ContextMenuItem], NoError>
if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute {
items = self.adMenuMainItems()
} else {
items = self.contextMenuMainItems(isSettings: isSettings, dismiss: {
dismissImpl?()
})
}
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
self.isShowingContextMenuPromise.set(true)
controller.presentInGlobalOverlay(contextController)
dismissImpl = { [weak contextController] in
contextController?.dismiss()
}
contextController.dismissed = { [weak self] in
Queue.mainQueue().after(0.1, {
self?.isShowingContextMenuPromise.set(false)
})
}
}
private func speedList(strings: PresentationStrings) -> [(String, String, Double)] {
let speedList: [(String, String, Double)] = [
("0.5x", "0.5x", 0.5),
(strings.PlaybackSpeed_Normal, "1x", 1.0),
("1.5x", "1.5x", 1.5),
("2x", "2x", 2.0)
]
return speedList
}
private func adMenuMainItems() -> Signal<[ContextMenuItem], NoError> {
guard case let .message(message, _) = self.item?.contentInfo, let adAttribute = message.adAttribute else {
return .single([])
}
let context = self.context
let presentationData = self.presentationData
var actions: [ContextMenuItem] = []
if adAttribute.canReport {
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_AboutAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, action: { [weak self] _, f in
f(.dismissWithoutContent)
if let navigationController = self?.baseNavigationController() as? NavigationController {
navigationController.pushViewController(AdsInfoScreen(context: context, forceDark: true))
}
})))
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_ReportAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, action: { [weak self] _, f in
f(.default)
let _ = (context.engine.messages.reportAdMessage(peerId: message.id.peerId, opaqueId: adAttribute.opaqueId, option: nil)
|> deliverOnMainQueue).start(next: { [weak self] result in
if case let .options(title, options) = result {
if let navigationController = self?.baseNavigationController() as? NavigationController {
navigationController.pushViewController(
AdsReportScreen(
context: context,
peerId: message.id.peerId,
opaqueId: adAttribute.opaqueId,
title: title,
options: options,
forceDark: true,
completed: {
if let navigationController = self?.baseNavigationController() as? NavigationController, let chatController = navigationController.viewControllers.last as? ChatController {
chatController.removeAd(opaqueId: adAttribute.opaqueId)
}
}
)
)
}
}
})
})))
actions.append(.separator)
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_RemoveAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, action: { [weak self] c, _ in
c?.dismiss(completion: {
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: true, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: true, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
if let navigationController = self?.baseNavigationController() as? NavigationController {
navigationController.pushViewController(controller)
}
})
})))
} else {
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Info, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, action: { [weak self] _, f in
f(.dismissWithoutContent)
if let navigationController = self?.baseNavigationController() as? NavigationController {
navigationController.pushViewController(AdInfoScreen(context: context, forceDark: true))
}
})))
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
if !context.isPremium && !premiumConfiguration.isPremiumDisabled {
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor)
}, iconSource: nil, action: { [weak self] c, _ in
c?.dismiss(completion: {
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: true, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: true, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
if let navigationController = self?.baseNavigationController() as? NavigationController {
navigationController.pushViewController(controller)
}
})
})))
}
if !message.text.isEmpty {
actions.append(.separator)
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
var messageEntities: [MessageTextEntity]?
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
}
}
storeMessageTextInPasteboard(message.text, entities: messageEntities)
Queue.mainQueue().after(0.2, {
guard let self, let controller = self.galleryController() else {
return
}
controller.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_MessageCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
f(.default)
})))
}
}
return .single(actions)
}
private func contextMenuMainItems(isSettings: Bool, dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
guard let videoNode = self.videoNode, let item = self.item else {
return .single([])
}
let peer: Signal<EnginePeer?, NoError>
if let (message, _, _) = self.contentInfo() {
peer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId))
} else {
peer = .single(nil)
}
return combineLatest(queue: Queue.mainQueue(), videoNode.status, peer)
|> take(1)
|> map { [weak self] status, peer -> [ContextMenuItem] in
guard let status = status, let strongSelf = self else {
return []
}
var items: [ContextMenuItem] = []
if isSettings {
var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal
var speedIconText: String = "1x"
var didSetSpeedValue = false
for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
if abs(speed - status.baseRate) < 0.01 {
speedValue = text
speedIconText = iconText
didSetSpeedValue = true
break
}
}
if !didSetSpeedValue && status.baseRate != 1.0 {
speedValue = String(format: "%.1fx", status.baseRate)
speedIconText = speedValue
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
}, action: { c, _ in
guard let strongSelf = self else {
c?.dismiss(completion: nil)
return
}
c?.pushItems(items: strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) })
})))
if let videoQualityState = strongSelf.videoNode?.videoQualityState(), !videoQualityState.available.isEmpty {
items.append(.separator)
//TODO:localize
let qualityText: String
switch videoQualityState.preferred {
case .auto:
if videoQualityState.current != 0 {
qualityText = "Auto (\(videoQualityState.current)p)"
} else {
qualityText = "Auto"
}
case let .quality(value):
qualityText = "\(value)p"
}
items.append(.action(ContextMenuActionItem(text: "Video Quality", textLayout: .secondLineWithValue(qualityText), icon: { _ in
return nil
}, action: { c, _ in
guard let strongSelf = self else {
c?.dismiss(completion: nil)
return
}
c?.pushItems(items: .single(ContextController.Items(content: .list(strongSelf.contextMenuVideoQualityItems(dismiss: dismiss)))))
})))
}
} else {
if let (message, _, _) = strongSelf.contentInfo() {
let context = strongSelf.context
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
guard let strongSelf = self, let peer = peer else {
return
}
if let navigationController = strongSelf.baseNavigationController() {
strongSelf.beginCustomDismiss(true)
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false)))
Queue.mainQueue().after(0.3) {
strongSelf.completeCustomDismiss()
}
}
f(.default)
})))
}
// if #available(iOS 11.0, *) {
// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
// f(.default)
// guard let strongSelf = self else {
// return
// }
// strongSelf.beginAirPlaySetup()
// })))
// }
if let (message, _, _) = strongSelf.contentInfo() {
for media in message.media {
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
let url = content.url
let item = OpenInItem.url(url: url)
let openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
items.append(.action(ContextMenuActionItem(text: openText, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
if let strongSelf = self, let controller = strongSelf.galleryController() {
var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
if !presentationData.theme.overallDarkAppearance {
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {})
}
})
controller.present(actionSheet, in: .window(.root))
}
})))
break
}
}
}
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil && !(item.content is HLSVideoContent) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in
f(.default)
if let strongSelf = self {
switch strongSelf.fetchStatus {
case .Local:
let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file))
|> deliverOnMainQueue).start(completed: {
guard let strongSelf = self else {
return
}
guard let controller = strongSelf.galleryController() else {
return
}
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
default:
guard let controller = strongSelf.galleryController() else {
return
}
controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
}
}
})))
}
if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
if let self, let navigationController = self.baseNavigationController() {
self.beginCustomDismiss(true)
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true)))
Queue.mainQueue().after(0.3) {
self.completeCustomDismiss()
}
}
f(.default)
})))
}
if strongSelf.canDelete() {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
f(.default)
if let strongSelf = self {
strongSelf.footerContentNode.deleteButtonPressed()
}
})))
}
}
return items
}
}
private func contextMenuSpeedItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
guard let videoNode = self.videoNode else {
return .single([])
}
return videoNode.status
|> take(1)
|> deliverOnMainQueue
|> map { [weak self] status -> [ContextMenuItem] in
guard let status = status, let strongSelf = self else {
return []
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, iconPosition: .left, action: { c, _ in
c?.popItems()
})))
let sliderValuePromise = ValuePromise<Double?>(nil)
items.append(.custom(SliderContextItem(minValue: 0.2, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, _ in
guard let strongSelf = self, let videoNode = strongSelf.videoNode else {
return
}
let newValue = normalizeValue(newValue)
videoNode.setBaseRate(newValue)
if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(newValue)
}
sliderValuePromise.set(newValue)
}), true))
items.append(.separator)
for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
let isSelected = abs(status.baseRate - rate) < 0.01
items.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: sliderValuePromise.get()
|> map { value in
if isSelected && value == nil {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}), action: { _, f in
f(.default)
guard let strongSelf = self, let videoNode = strongSelf.videoNode else {
return
}
videoNode.setBaseRate(rate)
if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(rate)
}
})))
}
return items
}
}
private func contextMenuVideoQualityItems(dismiss: @escaping () -> Void) -> [ContextMenuItem] {
guard let videoNode = self.videoNode else {
return []
}
guard let qualityState = videoNode.videoQualityState(), !qualityState.available.isEmpty else {
return []
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, iconPosition: .left, action: { c, _ in
c?.popItems()
})))
do {
let isSelected = qualityState.preferred == .auto
let qualityText: String
if qualityState.current != 0 {
qualityText = "Auto (\(qualityState.current)p)"
} else {
qualityText = "Auto"
}
items.append(.action(ContextMenuActionItem(text: qualityText, icon: { _ in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
guard let self, let videoNode = self.videoNode else {
return
}
videoNode.setVideoQuality(.auto)
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white)))
/*if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(rate)
}*/
})))
}
for quality in qualityState.available {
//TODO:release
let isSelected = qualityState.preferred == .quality(quality)
items.append(.action(ContextMenuActionItem(text: "\(quality)p", icon: { _ in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
guard let self, let videoNode = self.videoNode else {
return
}
videoNode.setVideoQuality(.quality(quality))
if quality >= 700 {
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQHD"), color: .white)))
} else {
self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQSD"), color: .white)))
}
/*if let controller = strongSelf.galleryController() as? GalleryController {
controller.updateSharedPlaybackRate(rate)
}*/
})))
}
return items
}
private var isAirPlayActive = false
private var externalVideoPlayer: ExternalVideoPlayer?
func beginAirPlaySetup() {
guard let content = self.item?.content as? NativeVideoContent else {
return
}
if #available(iOS 11.0, *) {
self.externalVideoPlayer = ExternalVideoPlayer(context: self.context, content: content)
self.externalVideoPlayer?.openRouteSelection()
self.externalVideoPlayer?.isActiveUpdated = { [weak self] isActive in
if let strongSelf = self {
if strongSelf.isAirPlayActive && !isActive {
strongSelf.externalVideoPlayer = nil
}
strongSelf.isAirPlayActive = isActive
strongSelf.updateDisplayPlaceholder()
}
}
}
}
@objc func openStickersButtonPressed() {
guard let content = self.item?.content as? NativeVideoContent else {
return
}
let context = self.context
let media = content.fileReference.abstract
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let topController = (self.baseNavigationController()?.topViewController as? ViewController)
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
topController?.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()
self.isInteractingPromise.set(true)
let signal = self.context.engine.stickers.stickerPacksAttachedToMedia(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, actionPerformed: { actions in
if let (info, items, action) = actions.first {
let animateInAsReplacement = false
switch action {
case .add:
topController?.present(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { _ in
return true
}), in: .window(.root))
case let .remove(positionInList):
topController?.present(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: animateInAsReplacement, action: { action in
if case .undo = action {
let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).start()
}
return true
}), in: .window(.root))
}
}
}, dismissed: { [weak self] in
self?.isInteractingPromise.set(false)
})
(baseNavigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root), with: nil)
})
}
@objc private func settingsButtonPressed() {
self.openMoreMenu(sourceNode: self.settingsBarButton.referenceNode, gesture: nil, isSettings: true)
}
override func adjustForPreviewing() {
super.adjustForPreviewing()
self.scrubberView?.isHidden = true
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, nil))
}
func updatePlaybackRate(_ playbackRate: Double?) {
self.playbackRate = playbackRate
if let playbackRate = self.playbackRate {
self.videoNode?.setBaseRate(playbackRate)
}
self.playbackRatePromise.set(self.playbackRate ?? 1.0)
}
func updateVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
self.videoQuality = videoQuality
self.videoNode?.setVideoQuality(videoQuality)
}
public func seekToStart() {
self.videoNode?.seek(0.0)
self.videoNode?.play()
}
override var keyShortcuts: [KeyShortcut] {
let strings = self.presentationData.strings
var keyShortcuts: [KeyShortcut] = []
keyShortcuts.append(
KeyShortcut(
title: self.isPlaying ? strings.KeyCommand_Pause : strings.KeyCommand_Play,
input: " ",
modifiers: [],
action: { [weak self] in
self?.footerContentNode.playbackControl?()
}
)
)
keyShortcuts.append(
KeyShortcut(
title: strings.KeyCommand_SeekBackward,
input: UIKeyCommand.inputLeftArrow,
modifiers: [.shift],
action: { [weak self] in
self?.footerContentNode.seekBackward?(5)
}
)
)
keyShortcuts.append(
KeyShortcut(
title: strings.KeyCommand_SeekForward,
input: UIKeyCommand.inputRightArrow,
modifiers: [.shift],
action: { [weak self] in
self?.footerContentNode.seekForward?(5)
}
)
)
keyShortcuts.append(
KeyShortcut(
title: strings.KeyCommand_Share,
input: "S",
modifiers: [.command],
action: { [weak self] in
self?.footerContentNode.actionButtonPressed()
}
)
)
if self.hasPictureInPicture {
keyShortcuts.append(
KeyShortcut(
title: strings.KeyCommand_SwitchToPIP,
input: "P",
modifiers: [.command],
action: { [weak self] in
self?.pictureInPictureButtonPressed()
}
)
)
}
if self.canDelete() {
keyShortcuts.append(
KeyShortcut(
input: "\u{8}",
modifiers: [],
action: { [weak self] in
self?.footerContentNode.deleteButtonPressed()
}
)
)
}
return keyShortcuts
}
}
final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ContextReferenceContentNode
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
self.controller = controller
self.sourceNode = sourceNode
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
private func normalizeValue(_ value: CGFloat) -> CGFloat {
return round(value * 10.0) / 10.0
}